Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
251 changes: 221 additions & 30 deletions src/components/Pages/Contact.tsx
Comment thread
AnilKumar3494 marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -1,42 +1,233 @@
import { useState } from 'react';
import { Box, CircularProgress } from '@mui/material';
import { useState, useEffect, useMemo } from 'react';
import ReplayIcon from '@mui/icons-material/Replay';
import {
Checkbox,
Stack,
TextField,
Button,
FormControlLabel,
Alert,
Collapse
} from '@mui/material';
import { addFeedback } from 'services/db';

import styles from './Pages.module.scss';

const PRIMARY_COLOR = '#10b6ff';
type FormStatus =
| 'idle'
| 'success'
| 'input_error'
| 'email_error'
| 'submission_error';

const Contact = () => {
const [loading, setLoading] = useState(true);
const [form, setForm] = useState({
name: '',
email: '',
feedback: '',
interest: false
});

const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<FormStatus>('idle');

useEffect(() => {
let timer: NodeJS.Timeout;
if (status === 'success') {
timer = setTimeout(() => {
setStatus('idle');
}, 5000);
}
return () => clearTimeout(timer);
}, [status]);

const handleChange =
(field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const value =
field === 'interest' ? event.target.checked : event.target.value;
setForm(prev => ({ ...prev, [field]: value }));
if (status === 'input_error' || status === 'submission_error') {
setStatus('idle');
}
};

const validateEmail = (email: string) => {
return /\S+@\S+\.\S+/.test(email);
};

const handleLoading = () => {
setLoading(false);
const handleSubmit = async () => {
if (!form.name.trim() || !form.email.trim()) {
setStatus('input_error');
return;
}
if (!validateEmail(form.email)) {
setStatus('email_error');
return;
}

try {
setLoading(true);

await addFeedback({
name: form.name,
email: form.email,
feedback: form.feedback || null,
interest: form.interest
});

setStatus('success');

setForm({
name: '',
email: '',
feedback: '',
interest: false
});
} catch (error) {
console.error(error);
setStatus('submission_error');
} finally {
setLoading(false);
}
};

const handleReset = () => {
setForm({ name: '', email: '', feedback: '', interest: false });
setStatus('idle');
};

const textFieldFocusSX = useMemo(
() => ({
'& .MuiOutlinedInput-root': {
'&.Mui-focused fieldset': {
borderColor: PRIMARY_COLOR
}
},
'& .MuiInputLabel-root.Mui-focused': {
color: PRIMARY_COLOR
}
}),
[]
);

return (
<Box sx={{ height: '600px', width: '100%', position: 'relative' }}>
{loading && (
<Box
<Stack gap={2} sx={{ position: 'relative' }}>
<h2 className={styles.pageHeader}>Contact Us</h2>

<Collapse in={status !== 'idle'}>
{status === 'success' && (
<Alert severity="success" onClose={() => setStatus('idle')}>
Thank you! Your feedback has been received!
</Alert>
)}
{status === 'input_error' && (
<Alert severity="error" onClose={() => setStatus('idle')}>
Please fill the required (*) fields and try again.
</Alert>
)}
{status === 'email_error' && (
<Alert severity="error" onClose={() => setStatus('idle')}>
Please enter a valid email.
</Alert>
)}
{status === 'submission_error' && (
<Alert severity="error" onClose={() => setStatus('idle')}>
Something went wrong please try again after sometime.
</Alert>
)}
</Collapse>

<TextField
required
label="Name"
placeholder="Enter Your Name"
value={form.name}
onChange={handleChange('name')}
sx={textFieldFocusSX}
autoComplete="name"
/>

<TextField
required
type="email"
label="Email"
placeholder="[email protected]"
value={form.email}
onChange={handleChange('email')}
sx={textFieldFocusSX}
autoComplete="email"
/>

<TextField
label="Feedback"
multiline
rows={3}
placeholder="Share your feedback and thoughts"
value={form.feedback}
onChange={handleChange('feedback')}
sx={textFieldFocusSX}
helperText="Please do not include any sensitive personal information."
slotProps={{
formHelperText: {
sx: {
color: 'gray',
fontSize: '0.75rem',
fontStyle: 'italic',
marginLeft: '1px'
}
}
}}
/>

<FormControlLabel
control={
<Checkbox
checked={form.interest}
onChange={handleChange('interest')}
sx={{
'&.Mui-checked': { color: PRIMARY_COLOR }
}}
/>
}
label={
<span className={styles.pageText}>
I'm interested in helping PHLASK with future research
</span>
}
/>

<Stack direction="row" gap={5}>
<Button
variant="text"
onClick={handleReset}
sx={{ color: PRIMARY_COLOR }}
>
<ReplayIcon sx={{ mr: 0.5 }} />
clear form
</Button>

<Button
variant="contained"
onClick={handleSubmit}
disabled={loading || status === 'success'}
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
fontWeight: 'bold',
backgroundColor: PRIMARY_COLOR,
'&:hover': {
backgroundColor: PRIMARY_COLOR,
boxShadow: `0 0 10px ${PRIMARY_COLOR}80`
}
}}
>
<CircularProgress />
</Box>
)}

<iframe
title="Contact Us"
className="airtable-embed"
src="https://airtable.com/embed/appyNdhZZn3gpovFh/pagDtKnlb6n3mCpgd/form"
width="100%"
height="600px"
style={{ background: 'transparent', border: 'none' }}
onLoad={handleLoading}
/>
</Box>
{loading
? 'Submitting...'
: status === 'success'
? 'Submitted'
: 'Submit'}
</Button>
</Stack>
</Stack>
);
};

Expand Down
4 changes: 2 additions & 2 deletions src/components/Pages/Pages.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ $extra-space: 25px; // Extra vertical space for the contact form
overflow-y: auto;
padding-right: 10px;
flex: 1;
max-height: 55vh;
max-height: 55dvh;
}

.pageHeader {
margin: 0;
margin-bottom: 40px;
margin-bottom: 20px;
color: #60718c;
font-family: 'Inter', sans-serif;
font-weight: 600;
Expand Down
17 changes: 16 additions & 1 deletion src/services/db.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createClient } from '@supabase/supabase-js';
import type { ResourceEntry } from 'types/ResourceEntry';
import type { FeedbackForm, ResourceEntry } from 'types/ResourceEntry';
import type { ResourceTypeOption } from 'hooks/useResourceType';
import type { Contributor } from 'types/Contributor';

Expand All @@ -11,6 +11,7 @@ const databaseApiKey =
import.meta.env.VITE_DB_API_KEY ||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndhbnR5Y2Zibnp6b2NzYnRocXpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzcwNDY2OTgsImV4cCI6MjA1MjYyMjY5OH0.yczsMOx3Y-zsWu-GjYEajIb0yw9fYWEIUglmmfM1zCY';
const contributorDatabaseName = 'contributors';
const feedbackDatabaseName = 'airtable_feedback';
Comment thread
AnilKumar3494 marked this conversation as resolved.
Outdated

const supabase = createClient(databaseUrl, databaseApiKey);

Expand Down Expand Up @@ -116,13 +117,27 @@ export const addResource = async (resource: ResourceEntry) => {
return data;
};

export const addFeedback = async (feedback: FeedbackForm) => {
const { data, error } = await supabase
.from(feedbackDatabaseName)
.insert(feedback)
.select();

if (error) {
throw error;
}
return data;
};

export const getContributors = async (): Promise<Contributor[]> => {
const { data, error } = await supabase
.from(contributorDatabaseName)
.select('*');
if (error) {
throw error;
}

console.table(data);
return data;
};

Expand Down
7 changes: 7 additions & 0 deletions src/types/ResourceEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,10 @@ export type ResourceEntry = {
/** If the resource_type is BATHROOM, the information about the bathroom resource. */
bathroom?: BathroomInfo | null;
};

export type FeedbackForm = {
Comment thread
AnilKumar3494 marked this conversation as resolved.
Outdated
name: string | null;
email: string | null;
feedback?: string | null;
interest?: boolean | null;
};