diff --git a/src/AppContext.ts b/src/AppContext.ts new file mode 100644 index 0000000..63b0fce --- /dev/null +++ b/src/AppContext.ts @@ -0,0 +1,10 @@ +import { Toggle } from "./util"; +import { createContext } from "react"; + +export interface AppContext { + menu: Toggle; + contact: Toggle; +} + +// state will be set upon initialisation +export const AppContext = createContext({} as AppContext); diff --git a/src/components/ButtonOrAnchor.tsx b/src/components/ButtonOrAnchor.tsx new file mode 100644 index 0000000..6ba0602 --- /dev/null +++ b/src/components/ButtonOrAnchor.tsx @@ -0,0 +1,81 @@ +import React, { forwardRef } from "react"; +import { css, cx } from "@emotion/css"; + +const anchorStyle = css` + display: inline; + color: var(--text-colour); + font-weight: 800; + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + + &:hover { + color: var(--primary-colour); + } +`; + +const buttonStyle = css` + display: inline; + background: var(--card-tags); + border: 0; + border-radius: 0.5rem; + color: var(--text-colour); + + cursor: pointer; + font-size: 1rem; + text-align: center; + + gap: 1rem; + padding: 0.8rem 1.25rem; + width: 100%; + + position: relative; + + &:disabled { + background-color: var(--bg-colour); + cursor: not-allowed; + } +`; + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + styled?: "button" | "anchor"; +} + +export const Button = forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + ); + }, +); + +export interface AnchorProps + extends React.AnchorHTMLAttributes { + styled?: "button" | "anchor"; +} + +export const Anchor = forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + {children} + + ); + }, +); diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx new file mode 100644 index 0000000..3ddfccf --- /dev/null +++ b/src/components/ContactForm.tsx @@ -0,0 +1,287 @@ +import React, { useEffect, useRef, useState } from "react"; +import { css, cx } from "@emotion/css"; +import { setupCursorTracking, Toggle } from "../util"; + +import { ReactComponent as CloseIcon } from "../assets/cross.svg"; +import { Button } from "./ButtonOrAnchor"; + +const containerStyle = css` + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 1000; + background-color: rgba(from var(--card-bg) r g b / 0.9); + transition: opacity 300ms; + + & main { + position: absolute; + top: 0; + right: 0; + width: 50rem; + max-width: 90vw; + height: 100%; + background-color: var(--bg-colour); + translate: 100% 0; + transition: translate 300ms, opacity 300ms; + display: grid; + place-items: center; + overflow-y: auto; + + & > .contact-container { + display: flex; + flex-direction: column; + padding: 2rem; + gap: 2rem; + width: 100%; + max-width: 35rem; + + & > .contact-header { + display: flex; + justify-content: space-between; + align-items: center; + + & h3 { + font-size: 1.5rem; + font-weight: 700; + } + + & .close-button { + background: none; + border: none; + cursor: pointer; + color: var(--text-subdued); + + &:hover, + &:focus { + color: var(--text-colour); + } + } + } + + & form { + display: flex; + flex-direction: column; + gap: 1rem; + } + + & article { + color: var(--text-subdued); + margin-bottom: 1rem; + display: flex; + flex-direction: column; + gap: 1.2rem; + font-size: 0.9rem; + } + + & .success { + color: var(--text-success); + } + + & .error { + color: var(--text-error); + } + } + } +`; + +const inputStyle = css` + width: 100%; + padding: 0.8rem; + border: 1px solid var(--table-border); + background-color: var(--card-bg); + color: var(--text-colour); + font-size: 1rem; + border-radius: 0.5rem; + + textarea& { + height: 100%; + max-height: 20vh; + } + + &:disabled { + opacity: 0.8; + cursor: not-allowed; + } +`; + +const ContactForm: React.FC<{ + toggle: Toggle; +}> = ({ toggle }) => { + const nameRef = useRef(null); + const [sending, setSending] = useState(false); + const [sent, setSent] = useState(false); + + useEffect(() => { + if (nameRef.current && toggle.on) nameRef.current.focus(); + + if (!toggle.on) { + setSent(false); + setSending(false); + setError(null); + } + + // remember current page + const href = window.location.href; + + // hijack back button and close the form instead + const handleBack = (e: PopStateEvent) => { + if (toggle.on) { + e.preventDefault(); + toggle.set(false); + // not sure of a cleaner way to do this + // this resets the history to the current page + window.history.replaceState(null, "", href); + } + }; + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") toggle.set(false); + }; + + window.addEventListener("keydown", handleEscape); + window.addEventListener("popstate", handleBack); + return () => { + window.removeEventListener("keydown", handleEscape); + window.removeEventListener("popstate", handleBack); + }; + }, [toggle.on]); + + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [message, setMessage] = useState(""); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!name || !email || !message) { + return setError("Please fill in all fields."); + } + + setError(null); + setSending(true); + try { + await fetch("https://api.feathers.studio/send", { + method: "POST", + body: JSON.stringify({ + type: "website-contact", + name, + email, + message, + }), + }); + setName(""); + setEmail(""); + setMessage(""); + setSending(false); + setSent(true); + + // reset if the form was closed before the request was sent + if (!toggle.on) setSent(false); + } catch (e) { + console.error(e); + setError( + "Failed to send message. Try again later, or notify me via email or Telegram, thanks!", + ); + setSending(false); + setSent(false); + } + }; + + const disabled = sending || sent; + + return ( +
toggle.set(false)} + className={cx( + containerStyle, + css` + opacity: ${toggle.on ? 1 : 0}; + pointer-events: ${toggle.on ? "auto" : "none"}; + `, + )}> +
e.stopPropagation()} + style={{ + translate: toggle.on ? "0 0" : "100% 0", + }}> +
+
+

Let's talk!

+ +
+
+ setName(e.target.value)} + className={inputStyle} + /> + setEmail(e.target.value)} + className={inputStyle} + /> +