-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Andrés Vidal <[email protected]>
- Loading branch information
1 parent
16acb40
commit c7ab71b
Showing
8 changed files
with
374 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { FC } from "react"; | ||
import Button from "./Button"; | ||
import { CONTACT_MODAL_KEY } from "./ContactModal"; | ||
import { useURLQueryModal } from "./Modal"; | ||
|
||
const ContactButton: FC = () => { | ||
const modal = useURLQueryModal(CONTACT_MODAL_KEY); | ||
|
||
return ( | ||
<Button variant="secondary" label="Contact" onClick={() => modal.open()} /> | ||
); | ||
}; | ||
|
||
export { ContactButton }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { FC, FormEvent } from "react"; | ||
import { useForm } from "utils/useForm"; | ||
import { z } from "zod"; | ||
import { Article } from "./Article"; | ||
import Button from "./Button"; | ||
import { Modal, useURLQueryModal } from "./Modal"; | ||
import { useNotify } from "./Notifications"; | ||
import TextArea from "./TextArea"; | ||
import TextInput from "./TextInput"; | ||
|
||
const CONTACT_MODAL_KEY = "contact"; | ||
|
||
const Contact = z.object({ | ||
name: z.string().min(1, "Required"), | ||
institution: z.string().optional(), | ||
address: z.string().min(1, "Required"), | ||
subject: z.string().min(1, "Required"), | ||
message: z.string().min(1, "Required"), | ||
}); | ||
|
||
type Contact = z.infer<typeof Contact>; | ||
|
||
const SENDER_INTRODUCTION = `Please share your contact details to enable us to address your query promptly. Your full name and email address are essential for us to send you a response. If you're associated with an institution and you think it's relevant to your message, feel free to include that information as well, though it's completely optional. We assure you that your details will be kept confidential and used solely for communication purposes related to your inquiry.`; | ||
const MESSAGE_INTRODUCTION = `Kindly provide a concise description of your purpose for reaching out. Whether you're interested in collaborating with our research, offering feedback on our data, or sharing insights about our web application, we invite you to outline your thoughts succinctly. This initial communication is an important step in establishing a productive dialogue. We're committed to understanding your perspective and exploring how we can effectively respond or engage in further discussions.`; | ||
|
||
const ContactForm: FC = () => { | ||
const { close } = useURLQueryModal(CONTACT_MODAL_KEY); | ||
const notify = useNotify(); | ||
|
||
const [contact, setContact, errors] = useForm(Contact); | ||
|
||
const { | ||
name = "", | ||
institution = "", | ||
subject = "", | ||
address = "", | ||
message = "", | ||
} = contact; | ||
|
||
async function handleSubmit(event: FormEvent) { | ||
event.preventDefault(); | ||
|
||
const { success } = Contact.safeParse(contact); | ||
|
||
if (!success) { | ||
setContact({ | ||
name, | ||
institution, | ||
subject, | ||
address, | ||
message, | ||
}); | ||
return; | ||
} | ||
|
||
notify({ | ||
level: "success", | ||
message: "Your message has been sent!", | ||
}); | ||
|
||
close(); | ||
} | ||
|
||
function handleChange(key: keyof Contact) { | ||
return (value: string) => { | ||
setContact({ ...contact, [key]: value }); | ||
}; | ||
} | ||
|
||
return ( | ||
<form | ||
className="py-4 space-y-5 text-sm sm:text-base" | ||
onSubmit={handleSubmit} | ||
> | ||
<section className="space-y-6"> | ||
<p>{SENDER_INTRODUCTION}</p> | ||
<fieldset className="space-y-6"> | ||
<TextInput | ||
label="Name" | ||
value={name} | ||
onChange={handleChange("name")} | ||
error={errors.name} | ||
/> | ||
<TextInput | ||
label="Email" | ||
value={address} | ||
onChange={handleChange("address")} | ||
error={errors.address} | ||
/> | ||
<TextInput | ||
label="Institution" | ||
value={institution} | ||
onChange={handleChange("institution")} | ||
error={errors.institution} | ||
/> | ||
</fieldset> | ||
</section> | ||
|
||
<section className="space-y-6"> | ||
<p>{MESSAGE_INTRODUCTION}</p> | ||
<fieldset className="space-y-6"> | ||
<TextInput | ||
label="Subject" | ||
value={subject} | ||
onChange={handleChange("subject")} | ||
error={errors.subject} | ||
/> | ||
<TextArea | ||
label="Write your message here..." | ||
value={message} | ||
onChange={handleChange("message")} | ||
error={errors.message} | ||
/> | ||
</fieldset> | ||
</section> | ||
|
||
<footer className="flex justify-end gap-2"> | ||
<Button label="Cancel" variant="outline" onClick={close} /> | ||
<Button type="submit" label="Send" /> | ||
</footer> | ||
</form> | ||
); | ||
}; | ||
|
||
const ContactModal: FC = () => { | ||
const { isOpen, close } = useURLQueryModal(CONTACT_MODAL_KEY); | ||
return ( | ||
<Modal isOpen={isOpen} onClose={close}> | ||
<Article heading={<div>Contact Us</div>} content={<ContactForm />} /> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export { CONTACT_MODAL_KEY, ContactModal }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import c from "classnames"; | ||
import { | ||
ChangeEvent, | ||
forwardRef, | ||
HTMLProps, | ||
ReactNode, | ||
Ref, | ||
useId, | ||
} from "react"; | ||
|
||
type Props = Omit< | ||
HTMLProps<HTMLTextAreaElement>, | ||
"value" | "onChange" | "className" | "label" | ||
> & { | ||
value: string; | ||
error?: string; | ||
onChange?: (value: string) => void; | ||
left?: ReactNode; | ||
right?: ReactNode; | ||
inputRef?: Ref<HTMLTextAreaElement>; | ||
label?: string; | ||
}; | ||
|
||
export default forwardRef<HTMLDivElement, Props>(function TextArea( | ||
{ value, error, left, right, inputRef, onChange, label, ...props }, | ||
ref, | ||
) { | ||
const labelId = useId(); | ||
const errorId = useId(); | ||
|
||
function handleChange(event: ChangeEvent<HTMLTextAreaElement>) { | ||
onChange?.(event.target.value); | ||
} | ||
|
||
return ( | ||
<div | ||
ref={ref} | ||
className={c( | ||
"relative group", | ||
"w-full gap-1 inline-flex items-center rounded h-full scrollbar scrollbar-none", | ||
"error-within:shadow-sm focus-within:error-within:bg-red-400/80, error-within:bg-red-300/40", | ||
label | ||
? "mt-2 px-2 py-2 bg-gray-active shadow-sm focus-within:bg-indigo-500/10" | ||
: "text-xs p-1 overflow-x-scroll focus-within:bg-gray-active focus-within:shadow-sm", | ||
)} | ||
> | ||
{left} | ||
<textarea | ||
{...props} | ||
ref={inputRef} | ||
value={value} | ||
className="w-full px-1 bg-transparent outline-none min-h-40 peer shrink grow placeholder:text-xs error:focus:text-white error:placeholder-white" | ||
onChange={handleChange} | ||
data-error={Boolean(error)} | ||
placeholder={label ? "" : props.placeholder} | ||
aria-labelledby={labelId} | ||
aria-errormessage={errorId} | ||
aria-invalid={Boolean(error)} | ||
/> | ||
{label && ( | ||
<label | ||
id={labelId} | ||
className={c( | ||
"absolute transition-all pointer-events-none", | ||
"peer-placeholder-shown:text-gray-600 peer-placeholder-shown:text-sm text-xs peer-focus:text-xs text-indigo-600 peer-focus:text-indigo-600", | ||
"peer-placeholder-shown:left-2 left-0 peer-focus:left-0 ", | ||
"peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-4 -translate-y-full peer-focus:-translate-y-full top-0 peer-focus:top-0", | ||
"peer-error:text-red-600 peer-error:-translate-y-full peer-error:top-0 peer-error:left-0 peer-error:text-xs", | ||
)} | ||
> | ||
{label} {error && <span id={errorId}>({error})</span>} | ||
</label> | ||
)} | ||
{right} | ||
</div> | ||
); | ||
}); |
Oops, something went wrong.