src/pages/TagGeneratorWizard/index.js
/* eslint-disable max-lines-per-function */
/* eslint-disable complexity */
import React, { useState, useEffect, useRef } from 'react';
import Link from '../../components/common/Link';
import {
useQueryParam,
StringParam,
ArrayParam,
withDefault,
} from 'use-query-params';
import axios from 'axios';
import Box from '@material-ui/core/Box';
import CircularProgress from '@material-ui/core/CircularProgress';
import Container from '@material-ui/core/Container';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import { GenericHeaderSection } from '../../components';
import { AffiliationQuestionSection } from './AffilationQuestionSection';
import {
OrgNameSection,
OrganizationSelectorSection,
OrgChange,
} from './Organization';
import {
ProjectRepositorySection,
ProjectRepositoryInput,
} from './ProjectRepository';
import {
AddTopicTagSection,
AddTagsQuestion,
NewTags,
CopyPasteTags,
CurrentTopicTagSection,
} from './TopicTagSection';
import useTheme from '@material-ui/core/styles/useTheme';
import BottomSection from './BottomSection';
import StepComponent from './StepComponent';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
containerPadding: {
paddingLeft: 100,
paddingRight: 100,
[theme.breakpoints.down('sm')]: {
paddingLeft: 40,
paddingRight: 40,
},
[theme.breakpoints.down('xs')]: {
padding: '0px 16px',
},
},
stepGrid: {
paddingTop: '50px',
},
}));
/**
* By removing matched text, we allow leeway in entering repository URL,
* including how it might be pasted from GitHub. All these would work:
* https://github.com/civictechindex/CTI-website-frontend.git
* git@github.com:civictechindex/CTI-website-frontend.git
* github.com/civictechindex/CTI-website-frontend
* civictechindex / CTI-website-frontend
*/
const getRepositoryUrlPath = (repositoryUrl) => {
let result = repositoryUrl.replace(/ /g, '');
const prefix = /^(?:https?:\/\/)?github\.com\/|git@github\.com:/;
const suffix = /\.git$/;
result = result.replace(prefix, '');
result = result.replace(suffix, '');
const arr = result.split('/');
if (arr.length < 2) {
result = 'error';
} else {
result = arr.slice(0, 2).join('/');
}
return result;
};
// checks if URL is deifferent
const usePrevious = (refValue) => {
const ref = useRef();
useEffect(() => {
ref.current = refValue;
});
return ref.current;
};
// eslint-disable-next-line max-lines-per-function
const TagGeneratorWizard = () => {
const theme = useTheme();
const classes = useStyles();
const [changeValue, setChangeValue] = useQueryParam(
'changeValue',
withDefault(StringParam, '')
);
const [currentTags, setCurrentTags] = useQueryParam(
'currentTags',
withDefault(ArrayParam, [])
);
const [displayState, setDisplayState] = useQueryParam(
'displayState',
withDefault(StringParam, '')
);
const [orgName, setOrgName] = useQueryParam(
'orgName',
withDefault(StringParam, '')
);
const [orgTags, setOrgTags] = useQueryParam(
'orgTags',
withDefault(ArrayParam, [])
);
const [repositoryName, setRepositoryName] = useQueryParam(
'repositoryName',
withDefault(StringParam, '')
);
const [repositoryUrl, setRepositoryUrl] = useQueryParam(
'repositoryUrl',
withDefault(StringParam, '')
);
const [fullRepositoryUrl, setFullRepositoryUrl] = useQueryParam(
'fullRepositoryUrl',
withDefault(StringParam, '')
);
const [tagsToAdd, setTagsToAdd] = useQueryParam(
'tagsToAdd',
withDefault(ArrayParam, [])
);
const [topicSearchError, setTopicSearchError] = useQueryParam(
'topicSearchError',
withDefault(StringParam, '')
);
const [userTags, setUserTags] = useQueryParam(
'userTags',
withDefault(ArrayParam, [])
);
const [value, setValue] = useQueryParam(
'value',
withDefault(StringParam, '')
);
const [loadingTags, setLoadingTags] = useState(false);
const [options, setOptions] = useState([]);
const [repoChangeAlert, setRepoChangeAlert] = useState('');
const [displayTypo, setDisplayTypo] = useState(true);
const breadCrumbLinks = [
{ href: '/home', name: 'Home' },
{ href: '/join-index/tag-generator-wizard', name: 'Tag Generator Wizard' },
];
const resetForm = () => {
setValue('');
setOrgName('');
setRepositoryUrl('');
setRepositoryName('');
setTopicSearchError('');
setTagsToAdd([]);
setCurrentTags([]);
setUserTags([]);
setOrgTags([]);
setChangeValue('');
setDisplayState('');
setRepoChangeAlert('');
};
useEffect(() => {
let active = true;
axios
.get(`${process.env.REACT_APP_API_URL}/api/organizations/`)
.then((res) => {
const orgs = res.data.map((org) => org.name);
if (active) {
setOptions(orgs);
}
});
return () => {
active = false;
};
}, []);
useEffect(() => {
const civicName = [];
if (!currentTags.includes('civictechindex')) {
civicName.push('civictechindex');
}
if (orgTags.length !== 0 && currentTags.length !== 0) {
const result = orgTags.filter((ot) => !currentTags.includes(ot));
setTagsToAdd([...civicName, ...result]);
} else {
setTagsToAdd([...civicName, ...orgTags]);
}
}, [orgTags, currentTags, setTagsToAdd]);
useEffect(() => {
if (value === 'no') {
setOrgName('');
setOrgTags([]);
}
}, [setOrgName, setOrgTags, value]);
const handleEnter = (event) => {
if (event.key === 'Enter') {
handleSubmit();
}
};
const handleChange = (event) => {
setValue(event.target.value);
};
const handleChangeProjectRepository = () => {
if (changeValue === 'TopicTag') {
setDisplayState('TopicTag');
} else if (changeValue === 'GenerateTags') {
setDisplayState('GenerateTags');
} else if (changeValue === 'CopyPasteTags') {
setDisplayState('CopyPasteTags');
} else {
setChangeValue('TopicTag');
setDisplayState('TopicTag');
}
};
const prevRefUrl = usePrevious(repositoryUrl);
const handleSubmit = (event) => {
const urlPath = getRepositoryUrlPath(repositoryUrl);
// Setting Repository Name
const patt = /[a-z]+\//g;
const repName = urlPath.replace(patt, '');
setRepositoryName(repName);
// Return error message if no url present
if (urlPath.length === 0) {
return setTopicSearchError('Please enter a URL');
}
if (urlPath === 'error') {
return setTopicSearchError('Please enter a valid URL');
}
// Fetches Tags from API only if URL is changed
if (prevRefUrl !== repositoryUrl) {
setLoadingTags(true);
axios
.get('https://api.github.com/repos/' + urlPath + '/topics', {
headers: { Accept: 'application/vnd.github.mercy-preview+json' },
})
.then((res) => {
setCurrentTags(res.data.names);
setFullRepositoryUrl('https://github.com/' + urlPath);
setTopicSearchError('');
})
.catch((e) => {
/*
* This should store the error state.
* Component should check for error state and resolve the correct response.
*/
if (e) {
setCurrentTags([]);
setFullRepositoryUrl('');
setTopicSearchError(
'Cannot find repository. Please check the name and try again'
);
setDisplayState('ProjectUrl');
}
})
.finally(() => {
setLoadingTags(false);
});
if (userTags.length !== 0) {
setRepoChangeAlert(
'It looks like you have changed your repository, please check your tags'
);
setDisplayState('ChangeTags');
} else {
handleChangeProjectRepository();
}
} else if (
topicSearchError ===
'Cannot find repository. Please check the name and try again'
) {
setDisplayState('ProjectUrl');
} else {
handleChangeProjectRepository();
}
};
const handleAdd = (chip) => {
chip = chip.toLowerCase();
chip = chip.replace(/['`~!@#$%^&*()_|+=?;:'".<>\\{\\}\\[\]\\\\/]/gi, '');
while (chip.startsWith('-')) {
chip = chip.slice(1);
}
while (chip.endsWith('-')) {
chip = chip.slice(0, -1);
}
if (chip.includes(',')) {
let inputChipArr = chip.split(',');
inputChipArr = inputChipArr.filter((i) => i);
setUserTags([...new Set([...userTags, ...inputChipArr])]);
} else setUserTags([...new Set([...userTags, chip])]);
};
const handleDelete = (deletedChip) => {
setUserTags(userTags.filter((c) => c !== deletedChip));
};
const linkStyles = {
fontWeight: '400',
color: theme.palette.secondary.main,
};
const OrgProjSection = () => {
return (
<>
<Grid className={classes.stepGrid}>
<StepComponent
step='Step 1 of 4'
stepHeading='Select Project Affiliation'
/>
</Grid>
<OrgNameSection
setDisplayState={setDisplayState}
orgName={orgName}
linkStyles={linkStyles}
/>
<StepComponent step='Step 2 of 4' stepHeading='Find Project' />
<ProjectRepositorySection
repositoryUrl={fullRepositoryUrl}
setDisplayState={setDisplayState}
linkStyles={linkStyles}
/>
</>
);
};
const RadioYes = ({ value, setOrgName }) => {
return (
<Grid container id='container-affiliated'>
<OrganizationSelectorSection
orgName={orgName}
setOrgName={setOrgName}
options={options}
setOptions={setOptions}
/>
<OrgChange
value={value}
orgName={orgName}
setOrgName={setOrgName}
setOrgTags={setOrgTags}
changeValue={changeValue}
setDisplayState={setDisplayState}
linkStyles={linkStyles}
/>
</Grid>
);
};
// eslint-disable-next-line complexity
const renderCurrentState = () => {
switch (displayState) {
case 'ProjectUrl':
return (
<>
<Grid className={classes.stepGrid}>
<StepComponent
step='Step 1 of 4'
stepHeading='Select Project Affiliation'
/>
</Grid>
<OrgNameSection
setDisplayState={setDisplayState}
orgName={orgName}
linkStyles={linkStyles}
/>
<StepComponent step='Step 2 of 4' stepHeading='Find Project' />
<ProjectRepositoryInput
repositoryUrl={repositoryUrl}
handleEnter={handleEnter}
setRepositoryUrl={setRepositoryUrl}
topicSearchError={topicSearchError}
setTopicSearchError={setTopicSearchError}
handleSubmit={handleSubmit}
/>
</>
);
case 'TopicTag':
return (
<>
<OrgProjSection />
{loadingTags ? (
<Box display='flex' alignItems='center' justifyContent='center'>
<CircularProgress />
</Box>
) : (
<>
<CurrentTopicTagSection
currentTags={currentTags}
repositoryName={repositoryName}
/>
<StepComponent
step='Step 3 of 4'
stepHeading='Create Topic Tags'
/>
<AddTagsQuestion
userTags={userTags}
displayState={displayState}
setDisplayState={setDisplayState}
changeValue={changeValue}
setChangeValue={setChangeValue}
resetForm={resetForm}
handleAdd={handleAdd}
handleDelete={handleDelete}
repoChangeAlert={repoChangeAlert}
setRepoChangeAlert={setRepoChangeAlert}
displayTypo={displayTypo}
setDisplayTypo={setDisplayTypo}
/>
</>
)}
</>
);
case 'GenerateTags':
return (
<>
<OrgProjSection />
<StepComponent step='Step 3 of 4' stepHeading='Create Topic Tags' />
<CurrentTopicTagSection
currentTags={currentTags}
repositoryName={repositoryName}
/>
<NewTags
tagsToAdd={tagsToAdd}
setDisplayState={setDisplayState}
setChangeValue={setChangeValue}
resetForm={resetForm}
linkStyles={linkStyles}
userTags={userTags}
setUserTags={setUserTags}
handleAdd={handleAdd}
handleDelete={handleDelete}
displayTypo={displayTypo}
/>
</>
);
case 'ChangeTags':
return (
<>
<OrgProjSection />
<StepComponent step='Step 3 of 4' stepHeading='Create Topic Tags' />
<CurrentTopicTagSection
currentTags={currentTags}
repositoryName={repositoryName}
/>
<AddTopicTagSection
userTags={userTags}
displayState={displayState}
setDisplayState={setDisplayState}
changeValue={changeValue}
setChangeValue={setChangeValue}
resetForm={resetForm}
handleAdd={handleAdd}
handleDelete={handleDelete}
repoChangeAlert={repoChangeAlert}
setRepoChangeAlert={setRepoChangeAlert}
/>
</>
);
case 'CopyPasteTags':
return (
<>
<OrgProjSection />
<StepComponent step='Step 3 of 4' stepHeading='Create Topic Tags' />
<CurrentTopicTagSection
currentTags={currentTags}
repositoryName={repositoryName}
/>
<CopyPasteTags
tagsToAdd={tagsToAdd}
setDisplayState={setDisplayState}
userTags={userTags}
repositoryName={repositoryName}
repositoryUrl={fullRepositoryUrl}
linkStyles={linkStyles}
/>
</>
);
default:
return (
<>
<Grid className={classes.stepGrid}>
<StepComponent
step='Step 1 of 4'
stepHeading='Select Project Affiliation'
/>
</Grid>
<AffiliationQuestionSection
value={value}
handleChange={handleChange}
>
<Typography variant='body1'>
Are you affiliated with an
<Link to='/organizations'>organization</Link>?
</Typography>
</AffiliationQuestionSection>
{value === 'yes' ? (
<RadioYes value={value} setOrgName={setOrgName} />
) : null}
{value === 'no' ? (
<OrgChange
value={value}
orgName={orgName}
setOrgName={setOrgName}
setOrgTags={setOrgTags}
changeValue={changeValue}
setDisplayState={setDisplayState}
/>
) : null}
</>
);
}
};
return (
<Box>
<Box className='boxBackground'>
<Container className='containerDefault'>
<GenericHeaderSection
mainTitle='Tag Generator Wizard'
breadCrumbLinks={breadCrumbLinks}
>
<Typography className='genSubheadTypo'>
Join the Civic Tech Index by submitting your open-source project.
<br /> This process takes less than one minute to complete.
</Typography>
</GenericHeaderSection>
</Container>
</Box>
<Box className='containerGray' style={{ paddingBottom: '30px' }}>
<Container className={classes.containerPadding}>
{renderCurrentState()}
</Container>
</Box>
{displayState === 'CopyPasteTags' ? <BottomSection /> : null}
</Box>
);
};
export default TagGeneratorWizard;