mirror of https://github.com/mkrhere/pw2
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
288 lines
6.3 KiB
288 lines
6.3 KiB
2 months ago
|
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<HTMLInputElement>(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<string | null>(null);
|
||
|
|
||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||
|
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 (
|
||
|
<section
|
||
|
aria-hidden={!toggle.on}
|
||
|
onClick={() => toggle.set(false)}
|
||
|
className={cx(
|
||
|
containerStyle,
|
||
|
css`
|
||
|
opacity: ${toggle.on ? 1 : 0};
|
||
|
pointer-events: ${toggle.on ? "auto" : "none"};
|
||
|
`,
|
||
|
)}>
|
||
|
<main
|
||
|
onClick={e => e.stopPropagation()}
|
||
|
style={{
|
||
|
translate: toggle.on ? "0 0" : "100% 0",
|
||
|
}}>
|
||
|
<div className="contact-container">
|
||
|
<div className="contact-header">
|
||
|
<h3>Let's talk!</h3>
|
||
|
<button className="close-button" onClick={() => toggle.set(false)}>
|
||
|
<CloseIcon />
|
||
|
</button>
|
||
|
</div>
|
||
|
<form onSubmit={handleSubmit}>
|
||
|
<input
|
||
|
disabled={disabled}
|
||
|
ref={nameRef}
|
||
|
type="text"
|
||
|
name="name"
|
||
|
placeholder="Name"
|
||
|
value={name}
|
||
|
onChange={e => setName(e.target.value)}
|
||
|
className={inputStyle}
|
||
|
/>
|
||
|
<input
|
||
|
disabled={disabled}
|
||
|
type="email"
|
||
|
name="email"
|
||
|
placeholder="Email"
|
||
|
value={email}
|
||
|
onChange={e => setEmail(e.target.value)}
|
||
|
className={inputStyle}
|
||
|
/>
|
||
|
<textarea
|
||
|
disabled={disabled}
|
||
|
name="message"
|
||
|
placeholder="Message"
|
||
|
value={message}
|
||
|
onChange={e => setMessage(e.target.value)}
|
||
|
className={inputStyle}
|
||
|
/>
|
||
|
<Button disabled={disabled} type="submit" ref={setupCursorTracking}>
|
||
|
<div className="dynamic-gradient" />
|
||
|
{sending ? "Sending..." : sent ? "Sent ✅" : "Send"}
|
||
|
</Button>
|
||
|
{sent && (
|
||
|
<p className="success">
|
||
|
I've received your message! I'll get back to you as soon as
|
||
|
possible. Thank you for reaching out!
|
||
|
</p>
|
||
|
)}
|
||
|
{error && <p className="error">{error}</p>}
|
||
|
</form>
|
||
|
<article>
|
||
|
<p>
|
||
|
Hey there! I live in{" "}
|
||
|
<a
|
||
|
href="https://www.worldtimebuddy.com/?pl=1&lid=1264527,2643743,5128581,5391959,1819729,1850147&h=1264527&hf=1"
|
||
|
target="_blank"
|
||
|
rel="noreferrer">
|
||
|
Chennai, South India (UTC+05:30)
|
||
|
</a>
|
||
|
.
|
||
|
</p>
|
||
|
<p>
|
||
|
I usually respond within 24 hours. I'd love to hear about new
|
||
|
opportunities or collaborations. Or just drop by to say hi!
|
||
|
</p>
|
||
|
<p>
|
||
|
You can also contact me via{" "}
|
||
|
<a href="https://t.me/mkrhere" target="_blank" rel="noreferrer">
|
||
|
Telegram/MKRhere
|
||
|
</a>
|
||
|
.
|
||
|
</p>
|
||
|
</article>
|
||
|
</div>
|
||
|
</main>
|
||
|
</section>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default ContactForm;
|