wrap up contact slideover; closes #41

This commit is contained in:
Nathan Wang 2020-07-13 22:49:38 -07:00
parent 4f3ff46267
commit afe0397a9b
5 changed files with 254 additions and 298 deletions

View file

@ -0,0 +1,222 @@
import * as React from 'react';
import { ModuleInfo } from '../module';
import { divisionLabels } from '../../content/ordering';
import SlideoverForm from './Slideover/SlideoverForm';
import { useState } from 'react';
import useStickyState from '../hooks/useStickyState';
const Field = ({ label, id, value, onChange }) => {
return (
<div className="space-y-1">
<label
htmlFor={id}
className="block text-sm font-medium leading-5 text-gray-900"
>
{label}
</label>
<div className="relative rounded-md shadow-sm">
<input
id={id}
className="form-input block w-full sm:text-sm sm:leading-5 transition ease-in-out duration-150"
value={value}
onChange={onChange}
/>
</div>
</div>
);
};
export default function ContactUsSlideover({
isOpen,
onClose,
activeModule,
}: {
isOpen: boolean;
onClose: any;
activeModule: ModuleInfo;
}) {
const [name, setName] = useStickyState('', 'contact_form_name');
const [email, setEmail] = useStickyState('', 'contact_form_email');
const [location, setLocation] = useState('');
const [topic, setTopic] = useStickyState('', 'contact_form_topic');
const topics = [
'Typo',
'Broken Link',
'Unclear Explanation',
'Suggestion',
'Website Bug',
'Other',
];
const [message, setMessage] = useStickyState('', 'contact_form_message');
const [showSuccess, setShowSuccess] = useState(false);
const [submitEnabled, setSubmitEnabled] = useState(true);
React.useEffect(() => {
if (activeModule)
setLocation(
`${activeModule.title} - ${divisionLabels[activeModule.division]}`
);
else setLocation('');
}, [activeModule]);
React.useEffect(() => {
if (isOpen) {
setShowSuccess(false);
setSubmitEnabled(true);
}
}, [isOpen]);
const handleSubmit = async e => {
e.preventDefault();
let data = new FormData();
data.append('name', name);
data.append('email', email);
data.append('location', location);
data.append('topic', topic);
data.append('message', message);
setSubmitEnabled(false);
try {
await fetch(
'https://formsubmit.co/ajax/da14a047686367521c3c8c5a79b03c97',
{
method: 'POST',
mode: 'no-cors',
body: data,
}
);
setTopic('');
setMessage('');
setShowSuccess(true);
} catch (e) {
setSubmitEnabled(true);
alert('Form submission failed: ' + e.message);
}
};
return (
<SlideoverForm
isOpen={isOpen}
onClose={onClose}
title="Contact Us"
subtitle="Contact us about anything: suggestions, bugs, assistance, and more! Don't submit any personal information through this form."
footerButtons={
<>
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
className="py-2 px-4 border border-gray-300 rounded-md text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition duration-150 ease-in-out"
onClick={onClose}
>
Cancel
</button>
</span>
<span className="inline-flex rounded-md shadow-sm">
<button
type="submit"
disabled={!submitEnabled}
className={`inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white transition duration-150 ease-in-out ${
submitEnabled
? 'bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700'
: 'bg-blue-400 focus:outline-none cursor-default'
}`}
>
Contact Us
</button>
</span>
</>
}
onSubmit={handleSubmit}
>
<div className="px-4 sm:px-6">
{showSuccess && (
<>
<p className="pt-6">
We've received your message! We will try our best to respond (if
one is needed) within a week.
</p>
<p className="pt-2">
For urgent requests, please feel free to email{' '}
<a
href="mailto:nathan.r.wang@gmail.com"
className="underline text-blue-600"
>
nathan.r.wang@gmail.com
</a>
.
</p>
</>
)}
{!showSuccess && (
<div className="space-y-6 pt-6 pb-5">
<Field
label="Name"
id="contact_name"
value={name}
onChange={e => setName(e.target.value)}
/>
<Field
label="Email"
id="contact_email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<Field
label="Module (If Applicable)"
id="contact_module"
value={location}
onChange={e => setLocation(e.target.value)}
/>
<fieldset className="space-y-2">
<legend className="text-sm leading-5 font-medium text-gray-900">
Topic
</legend>
<div className="space-y-3">
{topics.map((t, idx) => (
<div key={idx}>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id={`contact_topic_${idx}`}
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
checked={topic === t}
onChange={() => setTopic(t)}
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor={`contact_topic_${idx}`}
className="font-medium text-gray-900"
>
{t}
</label>
</div>
</div>
</div>
))}
</div>
</fieldset>
<div className="space-y-1">
<label
htmlFor="contact_message"
className="block text-sm font-medium leading-5 text-gray-900"
>
Message
</label>
<div className="relative rounded-md shadow-sm">
<textarea
id="contact_message"
rows={4}
className="form-input block w-full sm:text-sm sm:leading-5 transition ease-in-out duration-150"
value={message}
onChange={e => setMessage(e.target.value)}
/>
</div>
</div>
</div>
)}
</div>
</SlideoverForm>
);
}

View file

@ -11,7 +11,7 @@ import ModuleOrdering, {
ModuleOrderingItem,
} from '../../../content/ordering';
import Dots from '../Dots';
import ReportIssueSlideover from '../ReportIssueSlideover';
import ContactUsSlideover from '../ContactUsSlideover';
import MarkCompleteButton from './MarkCompleteButton';
import ModuleConfetti from './ModuleConfetti';
import Asterisk from '../tooltip/Asterisk';
@ -107,7 +107,7 @@ const Breadcrumbs = ({
</nav>
);
const SidebarBottomButtons = ({ onReportIssue, onGetHelp }) => {
const SidebarBottomButtons = ({ onContactUs }) => {
const languages = {
cpp: 'C++',
java: 'Java',
@ -145,7 +145,7 @@ const SidebarBottomButtons = ({ onReportIssue, onGetHelp }) => {
<div className="flex-shrink-0 border-t border-gray-200 flex">
<button
className="group flex-1 flex items-center p-4 text-sm leading-5 font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-100 transition ease-in-out duration-150"
onClick={onReportIssue}
onClick={onContactUs}
>
<svg
className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-500 transition ease-in-out duration-150"
@ -161,44 +161,6 @@ const SidebarBottomButtons = ({ onReportIssue, onGetHelp }) => {
Contact Us
</button>
</div>
{/*<div className="flex-shrink-0 border-t border-gray-200 flex">*/}
{/* <button*/}
{/* className="group flex-1 flex items-center p-4 text-sm leading-5 font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-100 transition ease-in-out duration-150"*/}
{/* onClick={onReportIssue}*/}
{/* >*/}
{/* <svg*/}
{/* className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-500 transition ease-in-out duration-150"*/}
{/* fill="none"*/}
{/* viewBox="0 0 24 24"*/}
{/* stroke="currentColor"*/}
{/* strokeLinecap="round"*/}
{/* strokeLinejoin="round"*/}
{/* strokeWidth="2"*/}
{/* >*/}
{/* <path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />*/}
{/* </svg>*/}
{/* Complain*/}
{/* </button>*/}
{/*</div>*/}
{/*<div className="flex-shrink-0 border-t border-gray-200 flex">*/}
{/* <button*/}
{/* className="group flex-1 flex items-center p-4 text-sm leading-5 font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-100 transition ease-in-out duration-150"*/}
{/* onClick={onGetHelp}*/}
{/* >*/}
{/* <svg*/}
{/* className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-500 transition ease-in-out duration-150"*/}
{/* fill="none"*/}
{/* viewBox="0 0 24 24"*/}
{/* stroke="currentColor"*/}
{/* strokeLinecap="round"*/}
{/* strokeLinejoin="round"*/}
{/* strokeWidth="2"*/}
{/* >*/}
{/* <path d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />*/}
{/* </svg>*/}
{/* Get Help*/}
{/* </button>*/}
{/*</div>*/}
</>
);
};
@ -339,8 +301,7 @@ export default function ModuleLayout({
children: React.ReactNode;
}) {
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
const [isReportIssueActive, setIsReportIssueActive] = useState(false);
const [isGetHelpActive, setIsGetHelpActive] = useState(false);
const [isContactUsActive, setIsContactUsActive] = useState(false);
const [isConfettiActive, setIsConfettiActive] = useState(false);
const [moduleProgress, setModuleProgress] = useState('Not Started');
@ -461,13 +422,9 @@ export default function ModuleLayout({
</nav>
</div>
<SidebarBottomButtons
onReportIssue={() => {
onContactUs={() => {
setIsMobileNavOpen(false);
setIsReportIssueActive(true);
}}
onGetHelp={() => {
setIsMobileNavOpen(false);
setIsGetHelpActive(true);
setIsContactUsActive(true);
}}
/>
</div>
@ -491,8 +448,7 @@ export default function ModuleLayout({
</nav>
</div>
<SidebarBottomButtons
onReportIssue={() => setIsReportIssueActive(true)}
onGetHelp={() => setIsGetHelpActive(true)}
onContactUs={() => setIsContactUsActive(true)}
/>
</div>
</div>
@ -588,9 +544,9 @@ export default function ModuleLayout({
</div>
</main>
</div>
<ReportIssueSlideover
isOpen={isReportIssueActive}
onClose={() => setIsReportIssueActive(false)}
<ContactUsSlideover
isOpen={isContactUsActive}
onClose={() => setIsContactUsActive(false)}
activeModule={module}
/>
</>

View file

@ -1,241 +0,0 @@
import * as React from 'react';
import Transition from './Transition';
import { ModuleInfo } from '../module';
import { divisionLabels } from '../../content/ordering';
import Slideover from './Slideover/Slideover';
export default function ReportIssueSlideover({
isOpen,
onClose,
activeModule,
}: {
isOpen: boolean;
onClose: any;
activeModule: ModuleInfo;
}) {
const [issueLocation, setIssueLocation] = React.useState('');
React.useEffect(() => {
if (activeModule)
setIssueLocation(
`${activeModule.title} - ${divisionLabels[activeModule.division]}`
);
else setIssueLocation('');
}, [activeModule]);
return (
<Slideover
isOpen={isOpen}
onClose={onClose}
title="Complaint Form"
subtitle="If you encounter an error while using the website, please fill out the form below. Thank you!"
footerButtons={
<>
<span className="inline-flex rounded-md shadow-sm">
<button
type="button"
className="py-2 px-4 border border-gray-300 rounded-md text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition duration-150 ease-in-out"
onClick={onClose}
>
Cancel
</button>
</span>
<span className="inline-flex rounded-md shadow-sm">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition duration-150 ease-in-out"
>
Submit Complaint
</button>
</span>
</>
}
>
<div className="px-4 divide-y divide-gray-200 sm:px-6">
<div className="space-y-6 pt-6 pb-5">
<div className="space-y-1">
<label
htmlFor="issue_name"
className="block text-sm font-medium leading-5 text-gray-900"
>
Name (Optional)
</label>
<div className="relative rounded-md shadow-sm">
<input
id="issue_name"
className="form-input block w-full sm:text-sm sm:leading-5 transition ease-in-out duration-150"
/>
</div>
</div>
<div className="space-y-1">
<label
htmlFor="issue_email"
className="block text-sm font-medium leading-5 text-gray-900"
>
Email (Optional)
</label>
<div className="relative rounded-md shadow-sm">
<input
id="issue_email"
className="form-input block w-full sm:text-sm sm:leading-5 transition ease-in-out duration-150"
type="email"
/>
</div>
</div>
<div className="space-y-1">
<label
htmlFor="issue_location"
className="block text-sm font-medium leading-5 text-gray-900"
>
Issue Location
</label>
<div className="relative rounded-md shadow-sm">
<input
id="issue_location"
className="form-input block w-full sm:text-sm sm:leading-5 transition ease-in-out duration-150"
value={issueLocation}
onChange={e => setIssueLocation(e.target.value)}
/>
</div>
</div>
<fieldset className="space-y-2">
<legend className="text-sm leading-5 font-medium text-gray-900">
Issue Type
</legend>
<div className="space-y-3">
<div>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id="type_typo"
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor="type_typo"
className="font-medium text-gray-900"
>
Typo
</label>
</div>
</div>
</div>
<div>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id="issue_broken-link"
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor="issue_broken-link"
className="font-medium text-gray-900"
>
Broken Link
</label>
</div>
</div>
</div>
<div>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id="issue_unclear-explanation"
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor="issue_unclear-explanation"
className="font-medium text-gray-900"
>
Unclear Explanation
</label>
</div>
</div>
</div>
<div>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id="issue_suggestion"
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor="issue_suggestion"
className="font-medium text-gray-900"
>
Suggestion
</label>
</div>
</div>
</div>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id="issue_website"
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor="issue_website"
className="font-medium text-gray-900"
>
Website Bug
</label>
</div>
</div>
<div className="relative flex items-start">
<div className="absolute flex items-center h-5">
<input
id="issue_other"
type="radio"
name="type"
className="form-radio h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
</div>
<div className="pl-7 text-sm leading-5">
<label
htmlFor="issue_other"
className="font-medium text-gray-900"
>
Other
</label>
</div>
</div>
</div>
</fieldset>
<div className="space-y-1">
<label
htmlFor="description"
className="block text-sm font-medium leading-5 text-gray-900"
>
Description
</label>
<div className="relative rounded-md shadow-sm">
<textarea
id="description"
rows={4}
className="form-input block w-full sm:text-sm sm:leading-5 transition ease-in-out duration-150"
/>
</div>
</div>
</div>
</div>
</Slideover>
);
}

View file

@ -1,5 +1,6 @@
import * as React from 'react';
import Transition from '../Transition';
import { FormEvent } from 'react';
type SlideoverProps = {
isOpen: boolean;
@ -8,9 +9,10 @@ type SlideoverProps = {
subtitle: React.ReactNode;
children: React.ReactNode;
footerButtons: React.ReactNode;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
};
export default function Slideover(props: SlideoverProps) {
export default function SlideoverForm(props: SlideoverProps) {
return (
<Transition show={props.isOpen} timeout={700}>
<div className="fixed inset-0 overflow-hidden">
@ -39,7 +41,10 @@ export default function Slideover(props: SlideoverProps) {
leaveTo="translate-x-full"
>
<div className="w-screen max-w-md">
<div className="h-full divide-y divide-gray-200 flex flex-col bg-white shadow-xl">
<form
className="h-full divide-y divide-gray-200 flex flex-col bg-white shadow-xl"
onSubmit={props.onSubmit}
>
<div className="flex-1 h-0 overflow-y-auto">
<header className="space-y-1 py-6 px-4 bg-blue-700 sm:px-6">
<div className="flex items-center justify-between space-x-3">
@ -81,7 +86,7 @@ export default function Slideover(props: SlideoverProps) {
<div className="flex-shrink-0 px-4 py-4 space-x-4 flex justify-end">
{props.footerButtons}
</div>
</div>
</form>
</div>
</Transition>
</section>

View file

@ -0,0 +1,14 @@
import * as React from 'react';
// source: https://joshwcomeau.com/react/persisting-react-state-in-localstorage/
export default function useStickyState(defaultValue, key) {
const [value, setValue] = React.useState(() => {
const stickyValue = window.localStorage.getItem(key);
return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
});
React.useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}