From d45c3040c7806b876481f8a71acd827e00af0ca7 Mon Sep 17 00:00:00 2001 From: Nathan Sarang-Walters Date: Thu, 1 Oct 2026 09:53:57 -0700 Subject: [PATCH 1/4] Replace contact form with email support links --- next.config.ts | 5 + package.json | 1 - pnpm-lock.yaml | 49 ------ src/components/ContactUsForm.tsx | 206 ------------------------ src/components/EmailContact.module.scss | 19 +++ src/components/EmailContact.tsx | 31 ++++ src/components/Footer.tsx | 3 - src/pages/api/contact.tsx | 41 ----- src/pages/contact/index.module.scss | 6 - src/pages/contact/index.tsx | 45 ------ src/pages/pricing/index.tsx | 8 +- src/pages/security/index.mdx | 2 +- src/pages/support/index.tsx | 43 ++--- 13 files changed, 74 insertions(+), 385 deletions(-) delete mode 100644 src/components/ContactUsForm.tsx create mode 100644 src/components/EmailContact.module.scss create mode 100644 src/components/EmailContact.tsx delete mode 100644 src/pages/api/contact.tsx delete mode 100644 src/pages/contact/index.module.scss delete mode 100644 src/pages/contact/index.tsx diff --git a/next.config.ts b/next.config.ts index 3dda3600..19797fec 100644 --- a/next.config.ts +++ b/next.config.ts @@ -23,6 +23,11 @@ const nextConfig: NextConfig = { pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], async redirects() { return [ + { + source: "/contact", + destination: "/support#contact", + permanent: true, + }, { source: "/slack", destination: diff --git a/package.json b/package.json index 677eaafc..4ba07daf 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "katex": "^0.16.32", "next": "^16.1.6", "next-mdx-remote": "^6.0.0", - "node-fetch": "^3.3.2", "prism-react-renderer": "^2.4.1", "react": "^19.2.4", "react-bootstrap": "^2.10.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e579740..1f521ad6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,9 +56,6 @@ importers: next-mdx-remote: specifier: ^6.0.0 version: 6.0.0(@types/react@19.2.17)(react@19.2.4) - node-fetch: - specifier: ^3.3.2 - version: 3.3.2 prism-react-renderer: specifier: ^2.4.1 version: 2.4.1(react@19.2.4) @@ -1174,10 +1171,6 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1494,10 +1487,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1521,10 +1510,6 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - framer-motion@12.34.3: resolution: {integrity: sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==} peerDependencies: @@ -2180,11 +2165,6 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -2193,10 +2173,6 @@ packages: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.47: resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} engines: {node: '>=18'} @@ -2778,10 +2754,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3805,8 +3777,6 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-uri-to-buffer@4.0.1: {} - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -4282,11 +4252,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4311,10 +4276,6 @@ snapshots: dependencies: is-callable: 1.2.7 - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - framer-motion@12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: motion-dom: 12.40.0 @@ -5320,8 +5281,6 @@ snapshots: node-addon-api@7.1.1: optional: true - node-domexception@1.0.0: {} - node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -5336,12 +5295,6 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-releases@2.0.47: {} object-assign@4.1.1: {} @@ -6192,8 +6145,6 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/src/components/ContactUsForm.tsx b/src/components/ContactUsForm.tsx deleted file mode 100644 index 3fbba428..00000000 --- a/src/components/ContactUsForm.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React from "react"; - -import Stack from "./Stack"; - -type HTMLInputProps = React.AllHTMLAttributes; - -interface InputProps { - component?: "input" | "textarea"; - id: string; - label: string; - onChange: (value: string) => void; - type?: HTMLInputProps["type"]; - value: string; -} - -const Input: React.FC = ({ - component: Component = "input", - id, - label, - onChange, - type, - value, -}) => ( -
- - , - ) => onChange(e.target.value)} - type={type} - id={id} - /> -
-); - -interface SelectProps { - id: string; - label: string; - onChange: (value: string) => void; - options: string[]; - value: string; -} - -const Select: React.FC = ({ - id, - label, - onChange, - options, - value, -}) => ( -
- - -
-); - -type SubmissionState = "unsubmitted" | "submitting" | "submitted" | "error"; - -interface ContactUsFormProps { - showHeader?: boolean; -} - -export const ContactUsForm: React.FC = ({ - showHeader = true, -}) => { - const [firstName, setFirstName] = React.useState(""); - const [lastName, setLastName] = React.useState(""); - const [email, setEmail] = React.useState(""); - const [organization, setOrganization] = React.useState(""); - const [role, setRole] = React.useState(""); - const [message, setMessage] = React.useState(""); - - const [submissionState, setSubmissionState] = - React.useState("unsubmitted"); - - const submit = () => { - setSubmissionState("submitting"); - fetch("/api/contact", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - firstName, - lastName, - email, - organization, - role, - message, - url: document.location.href, - }), - }) - .then((data) => setSubmissionState(data.ok ? "submitted" : "error")) - .catch(() => setSubmissionState("error")); - }; - - return ( -
-
- {submissionState !== "submitted" && ( - - {showHeader && ( -
-

Contact us

-

- Tell us how we can help and we'll be in touch soon. -

-
- )} -
-
- -
-
- -
-
- - - -
- -
- {submissionState === "error" && ( -
- Sorry, something went wrong. Try again in a moment. -
- )} -
- )} - {submissionState === "submitted" && ( -
-

Thanks!

-

- Someone from our team will be in touch with you soon. -

-
- )} -
-
- ); -}; diff --git a/src/components/EmailContact.module.scss b/src/components/EmailContact.module.scss new file mode 100644 index 00000000..a22adac5 --- /dev/null +++ b/src/components/EmailContact.module.scss @@ -0,0 +1,19 @@ +@import "~bootstrap/scss/_functions.scss"; +@import "~bootstrap/scss/_variables.scss"; + +.card { + height: 100%; + border-color: $blue-100; + background: linear-gradient(135deg, $blue-100, $white 75%); +} + +.address { + width: fit-content; + color: $blue-700; + overflow-wrap: anywhere; + text-underline-offset: 0.2em; + + &:hover { + color: $blue-900; + } +} diff --git a/src/components/EmailContact.tsx b/src/components/EmailContact.tsx new file mode 100644 index 00000000..f4f0c90a --- /dev/null +++ b/src/components/EmailContact.tsx @@ -0,0 +1,31 @@ +import classnames from "classnames"; + +import { Heading } from "./Heading"; +import styles from "./EmailContact.module.scss"; + +export function EmailContact() { + return ( +
+
+ Contact us +

+ Have a question about pricing, getting started, or using PrairieLearn + or PrairieTest? Our team is here to help. +

+ + Email support +