Skip to content

Commit

Permalink
Merge pull request #292 from wyeworks/contact-form-tweaks
Browse files Browse the repository at this point in the history
Contact form tweaks
  • Loading branch information
andres-vidal authored Jun 4, 2024
2 parents ec020a0 + 40549bd commit cc12069
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 43 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@
"prettier.requireConfig": true,
"[elixir]": {
"editor.defaultFormatter": "JakeBecker.elixir-ls"
},
"[svg]": {
"editor.defaultFormatter": "jock.svg"
}
}
2 changes: 1 addition & 1 deletion backend/lib/richard_burton/mailer_smtp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule RichardBurton.Mailer.SMTP do
username: System.get_env("SMTP_USER"),
password: System.get_env("SMTP_PASS"),
port: System.get_env("SMTP_PORT"),
tls: String.to_atom(System.get_env("SMTP_TLS")),
tls: System.get_env("SMTP_TLS"),
retries: 1,
no_mx_lookups: false
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule RichardBurtonWeb.EmailController do
use Phoenix.Controller
require Logger
alias RichardBurton.Email

@spec contact(Plug.Conn.t(), any()) :: Plug.Conn.t()
Expand All @@ -13,8 +12,7 @@ defmodule RichardBurtonWeb.EmailController do
conn |> put_status(400) |> json(%{issues: issues})

{:error, reason} ->
reason |> IO.inspect() |> Logger.error()
conn |> put_status(500) |> json(%{message: "Could not send email."})
throw(reason)
end
end
end
16 changes: 16 additions & 0 deletions frontend/assets/logo-outlined-animated-thumb.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions frontend/assets/logo-outlined-animated.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions frontend/components/AppLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FloatingPortal } from "@floating-ui/react";
import LogoOutlinedAnimatedThumb from "assets/logo-outlined-animated-thumb.svg";
import { motion } from "framer-motion";
import { FC } from "react";

const AppLoader: FC = () => {
return (
<FloatingPortal>
<div aria-modal="true" className="fixed inset-0 z-50 bg-indigo-900/30">
<motion.div
role="presentation"
className="absolute flex flex-col justify-between h-32 p-2 pb-6 bg-white rounded-full shadow shadow-indigo-200 bottom-4 right-4 w-30"
initial={{ scale: 0.6 }}
animate={{ scale: 1 }}
>
<LogoOutlinedAnimatedThumb className="text-indigo-700 size-fit" />
<div className="text-sm font-normal text-indigo-700">Loading...</div>
</motion.div>
</div>
</FloatingPortal>
);
};

export default AppLoader;
19 changes: 14 additions & 5 deletions frontend/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Props = HTMLProps<HTMLButtonElement> & {
width?: "full" | "fixed" | "fit";
labelSrOnly?: boolean;
type?: "button" | "submit" | "reset";
loading?: boolean;
};

const Button = forwardRef<HTMLButtonElement, Props>(function Button(
Expand All @@ -22,6 +23,7 @@ const Button = forwardRef<HTMLButtonElement, Props>(function Button(
width = "full",
type = "button",
labelSrOnly,
loading,
...props
},
ref,
Expand All @@ -37,22 +39,28 @@ const Button = forwardRef<HTMLButtonElement, Props>(function Button(

return (
<button
disabled={loading}
{...props}
ref={ref}
className={c(
"flex py-1.5 px-2 transition-colors items-center rounded font-base shadow-sm text-xs group space-x-2 whitespace-nowrap",
"disabled:bg-gray-100 disabled:text-gray-300 disabled:hover:bg-gray-100",
{
"text-white bg-indigo-600 hover:bg-indigo-700": isPrimary,
"text-white bg-yellow-500 hover:bg-yellow-600": isSecondary,
"text-gray-700 bg-gray-100 hover:bg-gray-active": isOutline,
"text-white bg-red-500 hover:bg-red-600": isDanger,
"disabled:bg-gray-100 disabled:text-gray-300 disabled:hover:bg-gray-100":
!loading,
"text-white bg-indigo-600 hover:bg-indigo-700 loading:bg-indigo-700":
isPrimary,
"text-white bg-yellow-500 hover:bg-yellow-600 loading:bg-yellow-600":
isSecondary,
"text-gray-700 bg-gray-100 hover:bg-gray-active loading:bg-gray-active":
isOutline,
"text-white bg-red-500 hover:bg-red-600 loading:bg-red-600": isDanger,
"justify-center": isTextCentered,
"w-full": isFullWidth,
"w-36": isFixedWidth,
"w-fit": isFitWidth,
},
)}
data-loading={loading}
onClick={onClick}
type={type}
>
Expand All @@ -66,6 +74,7 @@ const Button = forwardRef<HTMLButtonElement, Props>(function Button(
) : (
Icon
)}

<span className={c({ "sr-only": labelSrOnly })}>{label}</span>
</button>
);
Expand Down
23 changes: 19 additions & 4 deletions frontend/components/ContactModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { GOOGLE_RECAPTCHA_SITEKEY, http } from "app";
import { isAxiosError } from "axios";
import { FC, useRef } from "react";
import { FC, useRef, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import { useForm } from "utils/useForm";
import { z } from "zod";
import AppLoader from "./AppLoader";
import { Article } from "./Article";
import Button from "./Button";
import { Modal, useURLQueryModal } from "./Modal";
Expand Down Expand Up @@ -31,13 +32,18 @@ const ContactForm: FC = () => {
const notify = useNotify();
const recaptchaRef = useRef<ReCAPTCHA>(null);

const [loading, setLoading] = useState(false);

const { inputs, form } = useForm(Contact, {
disabled: loading,
async onSubmit(values, { setErrors }) {
setLoading(true);
const recaptchaToken = await recaptchaRef.current!.executeAsync();

try {
await http.post("/contact", { ...values, recaptchaToken });
notify({ level: "success", message: "Your message has been sent!" });

close();
} catch (error) {
if (
Expand All @@ -51,12 +57,14 @@ const ContactForm: FC = () => {
}

notify({ level: "error", message: "Something went wrong." });
} finally {
setLoading(false);
}
},
});

return (
<form className="py-4 space-y-5 text-sm sm:text-base" {...form}>
<form className="relative py-4 space-y-5 text-sm sm:text-base" {...form}>
<section className="space-y-6">
<p>{SENDER_INTRODUCTION}</p>
<fieldset className="space-y-6">
Expand All @@ -75,8 +83,14 @@ const ContactForm: FC = () => {
</section>

<footer className="flex justify-end gap-2">
<Button label="Cancel" variant="outline" onClick={close} />
<Button type="submit" label="Send" />
{loading && <AppLoader />}
<Button
label="Cancel"
variant="outline"
onClick={close}
disabled={loading}
/>
<Button type="submit" label="Send" loading={loading} />
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
Expand All @@ -90,6 +104,7 @@ const ContactForm: FC = () => {

const ContactModal: FC = () => {
const { isOpen, close } = useURLQueryModal(CONTACT_MODAL_KEY);

return (
<Modal isOpen={isOpen} onClose={close}>
<Article heading={<div>Contact Us</div>} content={<ContactForm />} />
Expand Down
16 changes: 11 additions & 5 deletions frontend/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { FloatingPortal } from "@floating-ui/react";
import {
FloatingFocusManager,
FloatingPortal,
useFloating,
} from "@floating-ui/react";
import { Key } from "app";
import CloseIcon from "assets/close.svg";
import Logo from "assets/logo.svg";
import clsx from "classnames";
import FocusTrap from "focus-trap-react";
import { AnimatePresence, motion } from "framer-motion";
import { useRouter } from "next/router";
import {
Expand Down Expand Up @@ -83,12 +86,15 @@ const Modal: FC<Props> = ({ children, isOpen, onClose }) => {

const isWiderThanSmall = useMediaQuery({ query: "(min-width: 640px)" });

const { context, refs } = useFloating();

return (
<AnimatePresence>
{isOpen && (
<FloatingPortal>
<FocusTrap active={process.env.NODE_ENV !== "test"}>
<FloatingFocusManager context={context} initialFocus={refs.floating}>
<motion.div
ref={refs.setFloating}
aria-modal="true"
aria-label="Close modal"
className="fixed inset-0 z-50 bg-indigo-900/30"
Expand All @@ -102,7 +108,7 @@ const Modal: FC<Props> = ({ children, isOpen, onClose }) => {
open
role="dialog"
className={clsx(
"mb-5 bg-white sm:rounded-lg shadow-lg scrollbar-thin scrollbar-thumb-indigo-600",
"mb-5 sm:rounded-lg bg-white shadow-lg scrollbar-thin scrollbar-thumb-indigo-600",
"overflow-y-auto overflow-x-clip",
"absolute left-1/2 absolute-center-x",
"w-full sm:w-11/12 lg:w-2/3 xl:w-1/2",
Expand All @@ -119,7 +125,7 @@ const Modal: FC<Props> = ({ children, isOpen, onClose }) => {
{children}
</motion.dialog>
</motion.div>
</FocusTrap>
</FloatingFocusManager>
</FloatingPortal>
)}
</AnimatePresence>
Expand Down
23 changes: 0 additions & 23 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"axios": "^1.6.8",
"axios-case-converter": "^1.1.1",
"classnames": "^2.5.1",
"focus-trap-react": "^10.2.3",
"framer-motion": "^11.0.8",
"i18n-iso-countries": "^7.10.1",
"is-hotkey": "^0.2.0",
Expand Down
12 changes: 12 additions & 0 deletions frontend/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@ body {
.anchor {
@apply font-normal text-indigo-600 underline visited:text-indigo-700;
}

.absolute-center-x {
@apply transform -translate-x-1/2 left-1/2;
}

.absolute-center-y {
@apply transform -translate-y-1/2 top-1/2;
}

.absolute-center {
@apply absolute-center-x absolute-center-y;
}
2 changes: 2 additions & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ module.exports = {
addVariant("error", `&${error}`);
addVariant("error-within", `&:has(${error})`);
addVariant("peer-error", `:merge(.peer):is(${error}) ~ &`);

addVariant("loading", '&[data-loading="true"]');
}),
scrollbar({ preferredStrategy: "pseudoelements" }),
],
Expand Down
4 changes: 3 additions & 1 deletion frontend/utils/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface InputProps {
error?: string;
}
interface Options<T> {
disabled?: boolean;
onSubmit?: (
values: T,
misc: {
Expand All @@ -81,7 +82,7 @@ export function useForm<T extends ZodObject<ZodRawShape>>(
inputs: Record<keyof z.infer<T>, InputProps>;
form: { onSubmit: FormEventHandler };
} {
const { onSubmit } = options ?? {};
const { onSubmit, disabled } = options ?? {};

const [defaults, strict] = stripDefaults(schema);
const [values, setValues] = useState<Partial<z.infer<T>>>({});
Expand Down Expand Up @@ -122,6 +123,7 @@ export function useForm<T extends ZodObject<ZodRawShape>>(
(acc, [key]) => ({
...acc,
[key]: {
disabled,
value: input[key] ?? defaults[key],
error: errors[key],
onChange: handleChange(key),
Expand Down

0 comments on commit cc12069

Please sign in to comment.