Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Page/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Page = ({ children, title, 'data-cy': dataCy }: PageProps) => {
sx={{
color: '#60718c',
maxHeight: '55vh',
width: '100%',
overflowY: 'auto',
gap: '2.5rem'
}}
Expand Down
213 changes: 181 additions & 32 deletions src/components/Pages/Contact.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,191 @@
import { useState } from 'react';
import { Box, CircularProgress } from '@mui/material';
import {
Checkbox,
Stack,
Button,
FormControlLabel,
Alert,
Collapse,
Typography
} from '@mui/material';
import { FormProvider, useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { addFeedback } from 'services/db';
import Page from 'components/Page/Page';
import feedbackFormSchema, {
type FeedbackFormValues
} from 'schemas/feedbackFormSchema';
import FormTextField from 'components/forms/FormTextField/FormTextField';
import ClearFormIcon from 'icons/ClearFormIcon';

type FormValues = FeedbackFormValues;

const TITLE = 'Contact Us';
const PRIMARY_COLOR = '#10b6ff';
const textFieldFocus = {
'& .MuiOutlinedInput-root': {
'&.Mui-focused fieldset': {
borderColor: PRIMARY_COLOR
}
},
'& .MuiInputLabel-root.Mui-focused': {
color: PRIMARY_COLOR
}
};
const SCHEMA = feedbackFormSchema;

const Contact = () => {
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');

const methods = useForm<FormValues>({
defaultValues: {
name: '',
email: '',
feedback: '',
interest: false
},
resolver: zodResolver(SCHEMA)
});

const {
handleSubmit,
control,
reset,
formState: { isSubmitting }
} = methods;

const onSubmit = async (data: FormValues) => {
setStatus('idle');
try {
await addFeedback(data);
setStatus('success');
reset();
} catch (error) {
console.error(error);
setStatus('error');
}
};

const handleLoading = () => {
setLoading(false);
const handleClear = () => {
reset();
setStatus('idle');
};

return (
<Box sx={{ height: '460px', width: '100%', position: 'relative' }}>
{loading && (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<CircularProgress />
</Box>
)}

<iframe
title="Contact Us"
className="airtable-embed"
src="https://airtable.com/embed/appyNdhZZn3gpovFh/pagDtKnlb6n3mCpgd/form"
width="100%"
height="460px"
style={{ background: 'transparent', border: 'none' }}
onLoad={handleLoading}
/>
</Box>
<Page title={TITLE} data-cy="contact-us">
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} style={{ width: '75%' }}>
<Stack gap={2}>
<Collapse in={status !== 'idle'}>
{status === 'success' && (
<Alert severity="success" onClose={() => setStatus('idle')}>
Thank you! Your feedback has been received!
</Alert>
)}
{status === 'error' && (
<Alert severity="error" onClose={() => setStatus('idle')}>
Something went wrong please try again after sometime.
</Alert>
)}
</Collapse>

<FormTextField<FormValues>
name="name"
label="Name"
placeholder="Enter Your Name"
required
fullWidth
sx={textFieldFocus}
/>

<FormTextField<FormValues>
name="email"
label="Email"
placeholder="example@email.com"
required
fullWidth
sx={textFieldFocus}
/>

<FormTextField<FormValues>
name="feedback"
label="Feedback"
placeholder="Share your feedback and thoughts"
multiline
minRows={3}
fullWidth
sx={textFieldFocus}
helperText="Please do not include any sensitive personal information."
slotProps={{
formHelperText: {
sx: {
color: 'gray',
fontSize: '0.75rem',
fontStyle: 'italic',
marginLeft: '1px'
}
}
}}
/>

<Controller
name="interest"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
{...field}
checked={field.value}
sx={{
'&.Mui-checked': { color: PRIMARY_COLOR }
}}
/>
}
label={
<Typography
sx={{ fontSize: 14, fontWeight: 500, lineHeight: '24px' }}
>
I'm interested in helping PHLASK with future research
</Typography>
}
/>
)}
/>

<Stack direction="row" gap={5} mt={2}>
<Button
variant="text"
onClick={handleClear}
sx={{ color: PRIMARY_COLOR }}
startIcon={<ClearFormIcon />}
>
clear form
</Button>
<Button
variant="contained"
type="submit"
disabled={isSubmitting || status === 'success'}
sx={{
fontWeight: 'bold',
backgroundColor: PRIMARY_COLOR,
'&:hover': {
backgroundColor: PRIMARY_COLOR,
boxShadow: `0 0 10px ${PRIMARY_COLOR}80`
}
}}
>
{isSubmitting
? 'Submitting...'
: status === 'success'
? 'Submitted'
: 'Submit'}
</Button>
</Stack>
</Stack>
</form>
</FormProvider>
</Page>
);
};

Expand Down
20 changes: 17 additions & 3 deletions src/components/forms/FormTextField/FormTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TextField } from '@mui/material';
import { TextField, type TextFieldProps } from '@mui/material';
import type { ReactNode } from 'react';
import {
useFormContext,
Expand All @@ -10,23 +10,29 @@ import {
type FormTextFieldProps<Values extends FieldValues> = {
name: Path<Values>;
label: ReactNode;
placeholder?: string;
disabled?: boolean;
helperText?: ReactNode;
fullWidth?: boolean;
required?: boolean;
multiline?: boolean;
minRows?: number;
slotProps?: TextFieldProps['slotProps'];
sx?: TextFieldProps['sx'];
};

const FormTextField = <Values extends FieldValues>({
name,
label,
placeholder,
helperText,
disabled = false,
required = false,
fullWidth = false,
multiline = false,
minRows = undefined
minRows = undefined,
slotProps,
sx
}: FormTextFieldProps<Values>) => {
const { register, getFieldState } = useFormContext<Values>();
const formState = useFormState<Values>({
Expand All @@ -39,12 +45,20 @@ const FormTextField = <Values extends FieldValues>({
<TextField
{...register(name, { required, disabled })}
label={label}
placeholder={placeholder}
helperText={error?.message || helperText || ' '}
fullWidth={fullWidth}
slotProps={{ inputLabel: { required } }}
slotProps={{
...slotProps,
inputLabel: {
required,
...slotProps?.inputLabel
}
}}
error={invalid}
multiline={multiline}
minRows={minRows}
sx={sx}
/>
);
};
Expand Down
20 changes: 20 additions & 0 deletions src/icons/ClearFormIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { SVGProps } from 'react';

const SvgClearFormIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="0.7em"
height="0.7em"
viewBox="0 0 16 16"
fill="#10b6ff"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.9907 0c.8909 0 1.337.1077 1.337 1.1077L14.6978 1.7071l-.8422.8421c.5654.6073 1.0348 1.301 1.3892 2.0577.558 1.1914.8135 2.502.744 3.8158-.0695 1.3138-.462 2.5902-1.1426 3.7161-.6806 1.1258-1.6284 2.0665-2.7594 2.7385-1.131.6721-2.4103 1.0549-3.7245 1.1145-1.3143.0596-2.623-.2059-3.8101-.7729-1.1872-.5669-2.2162-1.418-2.9959-2.4776C.7766 11.6816.2703 10.446.0822 9.1439c-.079-.5466.3001-1.0537.8467-1.1327s1.0538.3001 1.1327.8467c.1411.9766.5209 1.9033 1.1057 2.6981.5848.7947 1.3565 1.433 2.2469 1.8582.8904.4252 1.8719.6243 2.8576.5797.9857-.0447 1.9452-.3318 2.7934-.8359.8482-.5041 1.5591-1.2095 2.0695-2.0539.5105-.8444.8049-1.8017.857-2.787.0521-.9853-.1395-1.9683-.558-2.8619-.2556-.5457-.591-1.0478-.9934-1.4906l-.7331.7332c-.63 1.263-1.7071.8168-1.7071-.0741V0h3.9907zM1.5 4c.5523 0 1 .4477 1 1s-.4477 1-1 1-1-.4477-1-1 .4477-1 1-1zm2.25-2.75c.5523 0 1 .4477 1 1s-.4477 1-1 1-1-.4477-1-1 .4477-1 1-1zm3.25-1.25c.5523 0 1 .4477 1 1s-.4477 1-1 1-1-.4477-1-1 .4477-1 1-1z"
/>
</svg>
);

export default SvgClearFormIcon;
34 changes: 21 additions & 13 deletions src/icons/JoinDataIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import type { SVGProps } from 'react';

const SvgJoinDataIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 256 256"
fill="none"
viewBox="0 0 30 31"
{...props}
>
<path
stroke="#2D3848"
<g
stroke="#000"
strokeWidth={12}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.622}
d="m22.047 4.288 4.174 4.175L7.517 27.167a.22.22 0 0 1-.321 0l-3.853-3.853a.22.22 0 0 1 0-.321zM27.424 2.158l.928.929a2.31 2.31 0 0 1 0 3.257l-2.121 2.121-4.174-4.174 2.121-2.122a2.31 2.31 0 0 1 3.257 0zM9.635 14.102l-7.93-7.93 4.93-4.93 7.395 7.395-2.027 2.01M18.864 19.26l2.886-2.887 7.38 7.363-4.93 4.93-7.931-7.93M7.121 5.766l2.011-2.011M8.679 9.058l2.887-2.886M22.122 20.768l2.011-2.027M23.858 24.24l2.887-2.888"
/>
<path
stroke="#2D3848"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.622}
d="m7.43 27.32-6.261 2.027 2.011-6.212"
/>
>
<path
fill="#000"
fillRule="evenodd"
d="M239.985 55.994c0 22.089-50.138 39.994-111.985 39.994S16.015 78.083 16.015 55.994 66.153 16 128 16s111.985 17.905 111.985 39.994z"
/>

<path d="M239.985 103.987c-13.998 21.648-55.992 34.637-111.985 34.637s-97.987-12.989-111.985-34.637" />

<path d="M239.985 151.981c-13.998 21.647-55.992 34.636-111.985 34.636s-97.987-12.989-111.985-34.636" />

<path d="M239.985 199.974c-13.998 21.648-55.992 34.637-111.985 34.637s-97.987-12.989-111.985-34.637" />

<path d="M16.015 55.994v143.98M239.985 55.994v143.98" />
</g>
</svg>
);

export default SvgJoinDataIcon;
11 changes: 11 additions & 0 deletions src/schemas/feedbackFormSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as z from 'zod';

const feedbackFormSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().min(1, 'Email is required').email('Invalid email'),
feedback: z.string().optional(),
interest: z.boolean()
});

export type FeedbackFormValues = z.infer<typeof feedbackFormSchema>;
export default feedbackFormSchema;
Loading