diff --git a/app/components/calculator.tsx b/app/components/calculator.tsx new file mode 100644 index 0000000..dc094ed --- /dev/null +++ b/app/components/calculator.tsx @@ -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> + ); +} diff --git a/app/components/navigation.tsx b/app/components/navigation.tsx new file mode 100644 index 0000000..71744be --- /dev/null +++ b/app/components/navigation.tsx @@ -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> + ); +} diff --git a/app/root.tsx b/app/root.tsx index 81181cc..58c58e8 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -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> + ); } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 64163f8..b3f37da 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,23 +1,41 @@ import type { V2_MetaFunction } from "@remix-run/node"; +import ccImage from "../../public/cc-img.png"; +import Navigation from "../components/navigation"; +import Calculator from "../components/calculator"; export const meta: V2_MetaFunction = () => { - return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, - ]; + return [ + { title: "Captioning Calculator" }, + { + name: "description", + content: + "A prototype of a closed caption calculator that can be used to capture leads for a Closed Captioning Project", + }, + ]; }; export default function Index() { - return ( - <div> - <h1 className="text-3xl font-bold underline">Hello world!</h1> - {/* - // Use the form below if you wan to go remix style, or just use fetch in an event handler. - */} - <form action="/api/leads" method="POST"> - <label htmlFor="name">Name</label> - <input id="name" name="name" placeholder="Pat Smith" /> - </form> - </div> - ); + return ( + <> + <Navigation /> + <div> + <h1 className="lg:text-6xl text-3xl text-center lg:my-14 my-10">CAPTIONING CALCULATOR</h1> + <div className="grid lg:grid-cols-3 gap-4 lg:mx-80 mx-10"> + <div className="lg:col-span-2 lg:mx-24"> + <h2 className="lg:text-3xl text-xl font-bold my-8"> + How many captioning minutes will you need? + </h2> + <p className="text-xl"> + The Cablecast Captioning Calculator is a tool to understand how many cablecast Captioning + Minutes you will need for a year of programming. + </p> + </div> + <div> + <img className="my-0 mx-auto lg:ml-0" src={ccImage} alt="closed captioning image" /> + </div> + </div> + </div> + <Calculator /> + </> + ); } diff --git a/app/routes/api.leads.tsx b/app/routes/api.leads.tsx index 4fa1b24..14ec039 100644 --- a/app/routes/api.leads.tsx +++ b/app/routes/api.leads.tsx @@ -9,16 +9,7 @@ interface Lead { needsTranslations: boolean; } -export async function action( { request }: ActionArgs) { - - try { - const requestJson = request.clone(); - const jsonData = await requestJson.json(); - return await handleJsonBody(jsonData); - } catch { - // Nothing todo here. Just try form data next. - } - +export async function action({ request }: ActionArgs) { try { const requestFormData = request.clone(); const formData = await requestFormData.formData(); @@ -27,40 +18,33 @@ export async function action( { request }: ActionArgs) { // Nothing todo here. We'll error before } - throw json({ message: "Unsupported content type. Supported values are application/json and application/x-www-form-urlencoded."}, {status: 401}); -} - -async function handleJsonBody(jsonData: any) { - const lead: Lead = { - name: jsonData.name, - email: jsonData.email, - averageProgramsPerMonth: jsonData.averageProgramsPerMonth, - averageLengthOfProgramsInHours: jsonData.averageLengthOfProgramsInHours, - needsTranslations: jsonData.needsTranslations - }; - saveLead(lead); - return json({json: 'ok', lead}) + throw json( + { + message: + "Unsupported content type. Supported values are application/json and application/x-www-form-urlencoded.", + }, + { status: 401 } + ); } async function handleFormData(formData: FormData) { - const name = formData.get('name') as string; - const email = formData.get('email') as string; - const averageProgramsPerMonth = Number(formData.get('averageProgramsPerMonth')) ?? 0; - const averageLengthOfProgramsInHours = Number(formData.get('averageLengthOfProgramsInHours')) ?? 0; - const needsTranslations = formData.get('needsTranslations') == "YES"; + const name = formData.get("name") as string; + const email = formData.get("email") as string; + const averageProgramsPerMonth = Number(formData.get("averageProgramsPerMonth")) ?? 0; + const averageLengthOfProgramsInHours = Number(formData.get("averageLengthOfProgramsInHours")) ?? 0; + const needsTranslations = formData.get("needsTranslations") == "on"; const lead: Lead = { name, email, averageProgramsPerMonth, averageLengthOfProgramsInHours, - needsTranslations + needsTranslations, }; - await saveLead(lead); - return redirect('/'); + await saveLead(lead); + return redirect("/"); } async function saveLead(lead: Lead) { console.log(lead); } - diff --git a/app/tailwind.css b/app/tailwind.css index bd6213e..2e8dba0 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -1,3 +1,9 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@layer base { + html { + font-family: "Nunito Sans", sans-serif; + } +} diff --git a/package-lock.json b/package-lock.json index ff8abab..dcd0258 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "web-dev-hw", + "name": "captioning-calculator", "lockfileVersion": 3, "requires": true, "packages": { @@ -9,9 +9,11 @@ "@remix-run/node": "^1.17.1", "@remix-run/react": "^1.17.1", "@remix-run/serve": "^1.17.1", + "@remix-validated-form/with-zod": "^2.0.6", "isbot": "^3.6.8", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "remix-validated-form": "^5.0.2" }, "devDependencies": { "@remix-run/dev": "^1.17.1", @@ -3012,6 +3014,15 @@ "web-streams-polyfill": "^3.1.1" } }, + "node_modules/@remix-validated-form/with-zod": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@remix-validated-form/with-zod/-/with-zod-2.0.6.tgz", + "integrity": "sha512-i8H0PPFSSKIMGPVO/8cUMO1QoGa2bBQZb6RH3DoXGVE1heu52d1vwrFVsYYQB8Vc8lp5BGQk1kbxZuN9RzH1OA==", + "peerDependencies": { + "remix-validated-form": "^4.x || ^5.x", + "zod": "^3.11.x" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -7488,6 +7499,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8369,6 +8389,11 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9589,7 +9614,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, "funding": [ { "type": "github", @@ -10993,6 +11017,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remeda": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-1.23.0.tgz", + "integrity": "sha512-1y0jygsAc3opoFQW5BtA/QYOboai0u5IwdvwtbRAd1eJ2D9NmvZpDfV819LdSmrIQ0LONbp/dE9Uo/rGxUshPw==" + }, + "node_modules/remix-validated-form": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/remix-validated-form/-/remix-validated-form-5.0.2.tgz", + "integrity": "sha512-jM3uuvCP6AO9G117MAEGgTb3x/aOy5qVrOM85XdDhWnpJFt7WC6u1gBQ/tSd1UBCTxDSFwU6TZPCJex73D9rjQ==", + "dependencies": { + "immer": "^9.0.12", + "lodash.get": "^4.4.2", + "nanoid": "3.3.6", + "remeda": "^1.2.0", + "tiny-invariant": "^1.2.0", + "zustand": "^4.3.0" + }, + "peerDependencies": { + "@remix-run/react": ">= 1.15.0", + "@remix-run/server-runtime": "1.x", + "react": "^17.0.2 || ^18.0.0" + } + }, "node_modules/require-like": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", @@ -12068,6 +12115,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -12546,6 +12598,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -12993,6 +13053,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 376c099..4866b68 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "@remix-run/node": "^1.17.1", "@remix-run/react": "^1.17.1", "@remix-run/serve": "^1.17.1", + "@remix-validated-form/with-zod": "^2.0.6", "isbot": "^3.6.8", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "remix-validated-form": "^5.0.2" }, "devDependencies": { "@remix-run/dev": "^1.17.1", diff --git a/public/cablecast-logo.png b/public/cablecast-logo.png new file mode 100644 index 0000000..4c61601 Binary files /dev/null and b/public/cablecast-logo.png differ diff --git a/public/cc-img.png b/public/cc-img.png new file mode 100644 index 0000000..eee29fa Binary files /dev/null and b/public/cc-img.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 8830cf6..8a70edd 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/tailwind.config.ts b/tailwind.config.ts index 3a1b56f..734e4af 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,10 +1,17 @@ -import type { Config } from 'tailwindcss' +import type { Config } from "tailwindcss"; export default { - content: ['./app/**/*.{js,jsx,ts,tsx}'], - theme: { - extend: {}, - }, - plugins: [], -} satisfies Config - + content: ["./app/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + colors: { + "dark-grey": "#535B6F", + "cta-blue": "#2F87CA", + "logo-green": "#26A35C", + "light-grey": "#EDEEF1", + "medium-grey": "#8E98A9", + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json index 20f8a38..ecae7d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,23 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "resolveJsonModule": true, - "target": "ES2019", - "strict": true, - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "noImplicitAny": false, + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, - // Remix takes care of building everything in `remix build`. - "noEmit": true - } + // Remix takes care of building everything in `remix build`. + "noEmit": true + } }