Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bulk CSV Upload / Added Account Validation #62

Merged
merged 12 commits into from
Sep 12, 2024
Merged
14,194 changes: 14,194 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@vitest/ui": "^1.5.0",
"autoprefixer": "^10.4.16",
"cypress": "^13.7.3",
"eslint": "^9.0.0",
"eslint": "^8.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
Expand Down
3 changes: 3 additions & 0 deletions public/template.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name,url,type
Equalify Single,https://equalify.app,single
Equalify Sitemap,https://equalify.app/sitemap.xml,sitemap
1 change: 1 addition & 0 deletions src/components/layout/announcer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const routeNames: Record<string, string> = {
'/signup': 'Signup',
'/forgot': 'Forgot Password',
'/reset': 'Reset Password',
'/accessibility': 'Equalify Accessibility Statement',
};


Expand Down
12 changes: 5 additions & 7 deletions src/components/layout/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,20 @@ const Footer = () => {
<ul className="flex justify-center gap-4">
<li>
<a
href="https://github.com/equalifyEverything/v1/issues"
href="https://docs.equalify.app/"
target="_blank"
rel="noopener noreferrer"
title="Report an Issue"
aria-label="Report an Issue on GitHub"
title="API Docs"
aria-label="API Documentation"
className="text-sm font-medium text-[#186121] underline underline-offset-8 hover:text-[#186121CC]"
>
Report an Issue
API Docs
<span className="sr-only">(Opens in a new tab)</span>
</a>
</li>
<li>
<a
href="https://github.com/EqualifyEverything/v1/blob/main/ACCESSIBILITY.md"
target="_blank"
rel="noopener noreferrer"
href="/accessibility/"
title="Accessibility Statement"
aria-label="Read the Accessibility Statement"
className="text-sm font-medium text-[#186121] underline underline-offset-8 hover:text-[#186121CC]"
Expand Down
7 changes: 7 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '~/amplify.config';
import { initAnalytics } from '~/analytics';
import { NotFound } from '~/components/layout';
import {
AccessibilityStatement,
Account,
AddProperty,
CreateReport,
Expand All @@ -33,6 +34,7 @@ import {
TagDetails,
Forgot,
Reset,
BulkProperty,
} from '~/routes';
import {
addPropertyAction,
Expand Down Expand Up @@ -107,6 +109,7 @@ const router = createBrowserRouter([
loader: authenticatedLoader(pageDetailsLoader(queryClient)),
},
{ path: 'account', element: <Account /> },
{ path: 'accessibility', element: <AccessibilityStatement /> },
{ path: 'scans', element: <Scans />, loader: authenticatedLoader(scansLoader(queryClient)) },
{
path: 'properties',
Expand All @@ -118,6 +121,10 @@ const router = createBrowserRouter([
element: <AddProperty />,
action: addPropertyAction(queryClient),
},
{
path: 'properties/bulk',
element: <BulkProperty />,
},
{
path: 'properties/:propertyId/edit',
element: <EditProperty />,
Expand Down
6 changes: 5 additions & 1 deletion src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as PageDetails } from './protected/reports/page-details';
// Properties
export { default as Properties } from './protected/properties/properties';
export { default as AddProperty } from './protected/properties/add-property';
export { default as BulkProperty } from './protected/properties/bulk-property';
export { default as EditProperty } from './protected/properties/edit-property';

// Scans
Expand All @@ -24,4 +25,7 @@ export { default as Scans } from './protected/scans';
export { default as Login } from './public/auth/login';
export { default as Signup } from './public/auth/signup';
export { default as Forgot } from './public/auth/forgot';
export { default as Reset } from './public/auth/reset';
export { default as Reset } from './public/auth/reset';

// Other
export {default as AccessibilityStatement} from './protected/accessibility-statement'
76 changes: 76 additions & 0 deletions src/routes/protected/accessibility-statement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { SEO } from '~/components/layout';

const AccessibilityStatement = () => {
return (
<>
<SEO
title="Accessibility Statement - Equalify"
description="Equalify Accessibility Statement."
url="https://dashboard.equalify.app/accessibility"
/>
<div className="w-8/12 mx-auto">
<h1
id="accessibility-heading"
className="text-2xl font-bold md:text-3xl"
>
Equalify Accessibility Statement
</h1>
<section aria-labelledby="general-info-heading" className="mt-7 space-y-6 rounded-lg bg-white p-6 shadow">
<h2 className="font-bold">How Can We Help?</h2>
<p>
We welcome any comments, questions, or feedback on our site. If you
notice aspects of our site that aren’t working for you or your
assistive technology, email <a href="mailto:support@equalify.app" className='text-[#186121] underline underline-offset-4 pl-1'>support@equalify.app</a>.
</p>
<h2 className="font-bold">Equalify is Committed to Digital Accessibility</h2>
<p>
This project is committed to delivering an excellent user experience
for everyone. Equalify's user interface is structured in a way that
allows those of all abilities to easily and quickly find the
information they need.
</p>
<h2 className="font-bold">Ongoing Efforts to Ensure Accessible Content</h2>
<p>
Equalify uses the Web Content Accessibility Guidelines (WCAG) version
2.2 as its guiding principle. As we develop new pages and
functionality, the principles of accessible design and development are
an integral part of conception and realization.
</p>
<p>
We continually test content and features for WCAG 2.2 Level AA
compliance and remediate any issues to ensure we meet or exceed the
standards. Testing of our digital content is performed by our
accessibility experts using automated testing software, screen
readers, a color contrast analyzer, and keyboard-only navigation
techniques.
</p>
<h2 className="font-bold">Summary of Accessibility Features</h2>
<ul className="list-disc list-inside">
<li>
All images and other non-text elements have alternative text
associated with them.
</li>
<li>Navigational aids are provided on all app pages. </li>
<li>
Structural markup to indicate headings and lists has been provided
to aid in page comprehension
</li>
<li>
Forms are associated with labels and instructions on filling in
forms are available to screen reader users
</li>
</ul>

<h2 className="font-bold">Project VPATs</h2>
<p>
To request a conformance report using the Voluntary Product
Accessibility Template (VPAT), please email
<a href="mailto:support@equalify.app" className='text-[#186121] underline underline-offset-4 pl-1'>support@equalify.app</a>.
</p>
</section>
</div>
</>
);
};

export default AccessibilityStatement;
18 changes: 12 additions & 6 deletions src/routes/protected/account.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExclamationTriangleIcon, PinRightIcon } from '@radix-ui/react-icons';
import { useNavigate } from 'react-router-dom';
import { toast } from '~/components/alerts';

import { toast } from '~/components/alerts';
import { Button } from '~/components/buttons';
import { DangerDialog } from '~/components/dialogs';
import { AccountForm } from '~/components/forms';
Expand All @@ -15,12 +15,18 @@ const Account = () => {
const handleDeleteAccount = async () => {
try {
await deleteUser();
toast.success({ title: 'Success', description: 'Account deleted successfully.' });
setTimeout(() => {
navigate('/login');
}, 1000);
setTimeout(()=>{
toast.success({
title: 'Success',
description: 'Account deleted successfully!',
});
},1000)

} catch (error) {
toast.error({ title: 'Error', description: 'Failed to delete account. Please try again.' });
toast.error({
title: 'Error',
description: 'Failed to delete account. Please try again.',
});
}
};

Expand Down
157 changes: 157 additions & 0 deletions src/routes/protected/properties/bulk-property.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from '~/components/alerts';
import { Button } from '~/components/buttons';
import { SEO } from '~/components/layout';
import { addProperty } from '~/services';
import { useStore } from '~/store';

const BulkProperty = () => {
const navigate = useNavigate();
const [isFormValid, setIsFormValid] = useState(false);
const [data, setData] = useState([]);
const { loading, setLoading } = useStore();
const [current, setCurrent] = useState(1);

const throwFileError = (event, message) => {
alert(message);
setData([]);
event.target.value = '';
return;
}

const onChange = (event) => {
const file = event.target.files[0];
if (file.type !== 'text/csv') {
return throwFileError(event, `You must select a CSV file`);
}
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
const rows = text.split('\n');
const parsedData = rows.map(row => row.split(',').map(cell => cell.trim()));
const headers = parsedData[0];
if (headers?.[0] !== 'name' || headers?.[1] !== 'url' || headers?.[2] !== 'type') {
return throwFileError(event, `You must have the first row headers set to "name", "url", and "type"`);
}
const parsedRows = parsedData.slice(1, parsedData.length);
for (const row of parsedRows) {
if (!['single', 'sitemap'].includes(row?.[2])) {
return throwFileError(event, `You must specify the "type" for each row (valid values are "single" or "sitemap")`);
}
try {
const url = new URL(row?.[1]);
if (!['http:', 'https:'].includes(url.protocol)) {
return throwFileError(event, `All "urls" must use begin with either "http" or "https"`);
}
if (!url.host.includes('.')) {
return throwFileError(event, `All "urls" must end with a valid domain extension (i.e. ".com", ".org", etc)`);
}
}
catch (err) {
return throwFileError(event, `You have an invalid URL in your CSV`);
}
}
setData(parsedData);
};
reader.readAsText(file);
return;
}

useEffect(() => {
if (data?.length > 0) {
setIsFormValid(true);
}
else {
setIsFormValid(false);
}
}, [data]);

const handleSubmit = async (e) => {
e.preventDefault();
if (!isFormValid) return;
setLoading(true);

try {
const successes = [];
const errors = [];
for (const [index, row] of data.slice(1, data.length).entries()) {
setCurrent(index);
try {
const response = await addProperty(row[0], row[1], row[2]);
console.log(response);
successes.push(row);
}
catch (err) {
errors.push(row);
}
}
setLoading(false);
toast.success({ title: 'Success', description: 'Properties added successfully!' });
navigate('/properties');
} catch (error) {
setLoading(false);
toast.error({ title: 'Error', description: 'An error occurred while adding the properties.' });
}
};

return (
<>
<SEO
title="Bulk Add Property - Equalify"
description="Bulk add new propeties to Equalify to start monitoring and improving its accessibility."
url="https://dashboard.equalify.app/properties/bulk"
/>
<h1 id="bulk-property-heading" className="text-2xl font-bold md:text-3xl">
Bulk Upload CSV
</h1>

<section
aria-labelledby="bulk-property-heading"
className="mt-7 space-y-6 rounded-lg bg-white p-6 shadow"
aria-live="polite"
>
<form onSubmit={handleSubmit} id="bulk-property-form" className='flex flex-col gap-4'>
<a target='_blank' className='underline' href='/template.csv'>Example Template CSV</a>
<input onChange={onChange} type='file' accept='.csv' />
{data.length > 0 && <div className='flex flex-col'>
<div>CSV successfully uploaded! Showing first 10 rows:</div>
{data.slice(0, 10).map((row, index) => <div key={index} className={`p-1 flex flex-row gap-2 ${index === 0 && 'bg-card'}`}>
{row.map((cell, index) => <div key={index} style={{ width: `${100 / data[0].length}%` }} className='truncate'>{cell}</div>)}
</div>)}
<div>...</div>
</div>}
<div className="space-x-6">
<Button
type='reset'
variant={'outline'}
className="w-fit"
onClick={() => navigate(-1)}
aria-label='Cancel adding property'
>
Cancel
</Button>
<Button
type="submit"
form="bulk-property-form"
className="w-fit bg-[#1D781D] text-white"
disabled={!isFormValid}
aria-disabled={!isFormValid}
aria-live="polite"
>
Submit
</Button>
</div>
</form>
</section>
{loading && <div
className="fixed top-0 left-0 w-full h-full bg-[#6666] flex flex-row gap-2 items-center justify-center"
>
<div className='animate-spin flex flex-row items-center justify-center text-center'>↻</div>
Adding {current} of {data.length - 1} properties...
</div>}
</>
);
};

export default BulkProperty;
2 changes: 2 additions & 0 deletions src/routes/protected/properties/edit-property.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ const EditProperty = () => {
const response = await sendToScan([propertyId!]);
if (response.status === 'success') {
toast.success({ title: 'Success', description: 'Property sent to scan successfully!' });
} else if (response.status === 'user_not_validated') {
toast.error({ title: 'Error', description: `We must validate your account before permitting sitemap scans.` });
} else {
toast.error({ title: 'Error', description: 'Failed to send property to scan.' });
}
Expand Down
Loading