feat(client): add project links to certificate (#40071)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -6,6 +6,8 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import { Grid, Row, Col, Image, Button } from '@freecodecamp/react-bootstrap';
|
import { Grid, Row, Col, Image, Button } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
|
import ShowProjectLinks from './ShowProjectLinks';
|
||||||
import FreeCodeCampLogo from '../assets/icons/FreeCodeCampLogo';
|
import FreeCodeCampLogo from '../assets/icons/FreeCodeCampLogo';
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
import DonateForm from '../components/Donation/DonateForm';
|
import DonateForm from '../components/Donation/DonateForm';
|
||||||
@ -18,7 +20,9 @@ import {
|
|||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
usernameSelector,
|
usernameSelector,
|
||||||
isDonatingSelector,
|
isDonatingSelector,
|
||||||
executeGA
|
executeGA,
|
||||||
|
userByNameSelector,
|
||||||
|
fetchProfileForUser
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { certMap } from '../../src/resources/certAndProjectMap';
|
import { certMap } from '../../src/resources/certAndProjectMap';
|
||||||
import { createFlashMessage } from '../components/Flash/redux';
|
import { createFlashMessage } from '../components/Flash/redux';
|
||||||
@ -27,6 +31,7 @@ import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage';
|
|||||||
|
|
||||||
import RedirectHome from '../components/RedirectHome';
|
import RedirectHome from '../components/RedirectHome';
|
||||||
import { Loader, Spacer } from '../components/helpers';
|
import { Loader, Spacer } from '../components/helpers';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
cert: PropTypes.shape({
|
cert: PropTypes.shape({
|
||||||
@ -41,6 +46,7 @@ const propTypes = {
|
|||||||
certName: PropTypes.string,
|
certName: PropTypes.string,
|
||||||
createFlashMessage: PropTypes.func.isRequired,
|
createFlashMessage: PropTypes.func.isRequired,
|
||||||
executeGA: PropTypes.func,
|
executeGA: PropTypes.func,
|
||||||
|
fetchProfileForUser: PropTypes.func,
|
||||||
fetchState: PropTypes.shape({
|
fetchState: PropTypes.shape({
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
complete: PropTypes.bool,
|
complete: PropTypes.bool,
|
||||||
@ -52,6 +58,25 @@ const propTypes = {
|
|||||||
}),
|
}),
|
||||||
showCert: PropTypes.func.isRequired,
|
showCert: PropTypes.func.isRequired,
|
||||||
signedInUserName: PropTypes.string,
|
signedInUserName: PropTypes.string,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
completedChallenges: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
solution: PropTypes.string,
|
||||||
|
githubLink: PropTypes.string,
|
||||||
|
files: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
contents: PropTypes.string,
|
||||||
|
ext: PropTypes.string,
|
||||||
|
key: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
path: PropTypes.string
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
username: PropTypes.string
|
||||||
|
}),
|
||||||
userFetchState: PropTypes.shape({
|
userFetchState: PropTypes.shape({
|
||||||
complete: PropTypes.bool
|
complete: PropTypes.bool
|
||||||
}),
|
}),
|
||||||
@ -60,29 +85,37 @@ const propTypes = {
|
|||||||
validCertName: PropTypes.bool
|
validCertName: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requestedUserSelector = (state, { username = '' }) =>
|
||||||
|
userByNameSelector(username.toLowerCase())(state);
|
||||||
|
|
||||||
const validCertNames = certMap.map(cert => cert.slug);
|
const validCertNames = certMap.map(cert => cert.slug);
|
||||||
|
|
||||||
const mapStateToProps = (state, { certName }) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const validCertName = validCertNames.some(name => name === certName);
|
const validCertName = validCertNames.some(name => name === props.certName);
|
||||||
return createSelector(
|
return createSelector(
|
||||||
showCertSelector,
|
showCertSelector,
|
||||||
showCertFetchStateSelector,
|
showCertFetchStateSelector,
|
||||||
usernameSelector,
|
usernameSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isDonatingSelector,
|
isDonatingSelector,
|
||||||
(cert, fetchState, signedInUserName, userFetchState, isDonating) => ({
|
requestedUserSelector,
|
||||||
|
(cert, fetchState, signedInUserName, userFetchState, isDonating, user) => ({
|
||||||
cert,
|
cert,
|
||||||
fetchState,
|
fetchState,
|
||||||
validCertName,
|
validCertName,
|
||||||
signedInUserName,
|
signedInUserName,
|
||||||
userFetchState,
|
userFetchState,
|
||||||
isDonating
|
isDonating,
|
||||||
|
user
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators({ createFlashMessage, showCert, executeGA }, dispatch);
|
bindActionCreators(
|
||||||
|
{ createFlashMessage, showCert, fetchProfileForUser, executeGA },
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
const ShowCertification = props => {
|
const ShowCertification = props => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -104,9 +137,17 @@ const ShowCertification = props => {
|
|||||||
signedInUserName,
|
signedInUserName,
|
||||||
isDonating,
|
isDonating,
|
||||||
cert: { username = '' },
|
cert: { username = '' },
|
||||||
|
fetchProfileForUser,
|
||||||
|
user,
|
||||||
executeGA
|
executeGA
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
if (!signedInUserName || signedInUserName !== username) {
|
||||||
|
if (isEmpty(user) && username) {
|
||||||
|
fetchProfileForUser(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isDonationDisplayed &&
|
!isDonationDisplayed &&
|
||||||
userComplete &&
|
userComplete &&
|
||||||
@ -125,7 +166,15 @@ const ShowCertification = props => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isDonationDisplayed, props]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
isDonationDisplayed,
|
||||||
|
props.userFetchState,
|
||||||
|
props.signedInUserName,
|
||||||
|
props.isDonating,
|
||||||
|
props.cert,
|
||||||
|
props.executeGA
|
||||||
|
]);
|
||||||
|
|
||||||
const hideDonationSection = () => {
|
const hideDonationSection = () => {
|
||||||
setIsDonationDisplayed(false);
|
setIsDonationDisplayed(false);
|
||||||
@ -186,6 +235,7 @@ const ShowCertification = props => {
|
|||||||
certTitle,
|
certTitle,
|
||||||
completionTime
|
completionTime
|
||||||
} = cert;
|
} = cert;
|
||||||
|
const { user } = props;
|
||||||
|
|
||||||
const certDate = new Date(date);
|
const certDate = new Date(date);
|
||||||
const certYear = certDate.getFullYear();
|
const certYear = certDate.getFullYear();
|
||||||
@ -311,6 +361,8 @@ const ShowCertification = props => {
|
|||||||
</Row>
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
{signedInUserName === username ? shareCertBtns : ''}
|
{signedInUserName === username ? shareCertBtns : ''}
|
||||||
|
<Spacer size={2} />
|
||||||
|
<ShowProjectLinks user={user} name={userFullName} certName={certTitle} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
200
client/src/client-only-routes/ShowProjectLinks.js
Normal file
200
client/src/client-only-routes/ShowProjectLinks.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/* eslint-disable react/jsx-sort-props */
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import '../components/layouts/project-links.css';
|
||||||
|
import { maybeUrlRE } from '../utils';
|
||||||
|
import { Spacer, Link } from '../components/helpers';
|
||||||
|
import { projectMap, legacyProjectMap } from '../resources/certAndProjectMap';
|
||||||
|
import ProjectModal from '../components/SolutionViewer/ProjectModal';
|
||||||
|
import { find, first } from 'lodash';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
certName: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
completedChallenges: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
solution: PropTypes.string,
|
||||||
|
githubLink: PropTypes.string,
|
||||||
|
files: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
contents: PropTypes.string,
|
||||||
|
ext: PropTypes.string,
|
||||||
|
key: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
path: PropTypes.string
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
username: PropTypes.string
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSolutionState = {
|
||||||
|
projectTitle: '',
|
||||||
|
files: null,
|
||||||
|
solution: null,
|
||||||
|
isOpen: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShowProjectLinks = props => {
|
||||||
|
const [solutionState, setSolutionState] = useState(initSolutionState);
|
||||||
|
|
||||||
|
const handleSolutionModalHide = () => setSolutionState(initSolutionState);
|
||||||
|
|
||||||
|
const getProjectSolution = (projectId, projectTitle) => {
|
||||||
|
const {
|
||||||
|
user: { completedChallenges }
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const completedProject = find(
|
||||||
|
completedChallenges,
|
||||||
|
({ id }) => projectId === id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!completedProject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { solution, githubLink, files } = completedProject;
|
||||||
|
const onClickHandler = () =>
|
||||||
|
setSolutionState({
|
||||||
|
projectTitle,
|
||||||
|
files,
|
||||||
|
solution,
|
||||||
|
isOpen: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files && files.length) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClickHandler}
|
||||||
|
className='project-link-button-override'
|
||||||
|
>
|
||||||
|
solution
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (githubLink) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href={solution} rel='noopener noreferrer' target='_blank'>
|
||||||
|
solution
|
||||||
|
</a>
|
||||||
|
,{' '}
|
||||||
|
<a href={githubLink} rel='noopener noreferrer' target='_blank'>
|
||||||
|
source
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (maybeUrlRE.test(solution)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
block={'true'}
|
||||||
|
className='btn-invert'
|
||||||
|
href={solution}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
solution
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button className='project-link-button-override' onClick={onClickHandler}>
|
||||||
|
solution
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderProjectsFor = certName => {
|
||||||
|
if (certName === 'Legacy Full Stack') {
|
||||||
|
const legacyCerts = [
|
||||||
|
{ title: 'Responsive Web Design' },
|
||||||
|
{ title: 'JavaScript Algorithms and Data Structures' },
|
||||||
|
{ title: 'Front End Libraries' },
|
||||||
|
{ title: 'Data Visualization' },
|
||||||
|
{ title: 'APIs and Microservices' },
|
||||||
|
{ title: 'Legacy Information Security and Quality Assurance' }
|
||||||
|
];
|
||||||
|
return legacyCerts.map((cert, ind) => {
|
||||||
|
const mapToUse = projectMap[cert.title] || legacyProjectMap[cert.title];
|
||||||
|
const { superBlock } = first(mapToUse);
|
||||||
|
const certLocation = `/certification/${username}/${superBlock}`;
|
||||||
|
return (
|
||||||
|
<li key={ind}>
|
||||||
|
<a
|
||||||
|
href={certLocation}
|
||||||
|
className='btn-invert project-link'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{cert.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (projectMap[certName] || legacyProjectMap[certName]).map(
|
||||||
|
({ link, title, id }) => (
|
||||||
|
<li key={id}>
|
||||||
|
<Link to={link} className='project-link'>
|
||||||
|
{title}
|
||||||
|
</Link>
|
||||||
|
: {getProjectSolution(id, title)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
certName,
|
||||||
|
name,
|
||||||
|
user: { username }
|
||||||
|
} = props;
|
||||||
|
const { files, isOpen, projectTitle, solution } = solutionState;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{certName === 'Legacy Full Stack'
|
||||||
|
? `As part of this Legacy Full Stack certification, ${name} completed the following certifications:`
|
||||||
|
: `As part of this certification, ${name} built the following projects and got all automated test suites to pass:`}
|
||||||
|
<Spacer />
|
||||||
|
<ul>{renderProjectsFor(certName)}</ul>
|
||||||
|
<Spacer />
|
||||||
|
{isOpen ? (
|
||||||
|
<ProjectModal
|
||||||
|
files={files}
|
||||||
|
handleSolutionModalHide={handleSolutionModalHide}
|
||||||
|
isOpen={isOpen}
|
||||||
|
projectTitle={projectTitle}
|
||||||
|
solution={solution}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
If you suspect that any of these projects violate the{' '}
|
||||||
|
<a
|
||||||
|
href='https://www.freecodecamp.org/news/academic-honesty-policy/'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
academic honesty policy
|
||||||
|
</a>
|
||||||
|
, please{' '}
|
||||||
|
<a
|
||||||
|
href={`/user/${username}/report-user`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
report this to our team
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShowProjectLinks.propTypes = propTypes;
|
||||||
|
ShowProjectLinks.displayName = 'ShowProjectLinks';
|
||||||
|
|
||||||
|
export default ShowProjectLinks;
|
59
client/src/components/SolutionViewer/ProjectModal.js
Normal file
59
client/src/components/SolutionViewer/ProjectModal.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import SolutionViewer from './SolutionViewer';
|
||||||
|
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
files: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
contents: PropTypes.string,
|
||||||
|
ext: PropTypes.string,
|
||||||
|
key: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
path: PropTypes.string
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handleSolutionModalHide: PropTypes.func,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
projectTitle: PropTypes.string,
|
||||||
|
solution: PropTypes.string,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectModal = props => {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
projectTitle,
|
||||||
|
files,
|
||||||
|
solution,
|
||||||
|
t,
|
||||||
|
handleSolutionModalHide
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby='solution-viewer-modal-title'
|
||||||
|
bsSize='large'
|
||||||
|
onHide={handleSolutionModalHide}
|
||||||
|
show={isOpen}
|
||||||
|
>
|
||||||
|
<Modal.Header className='this-one?' closeButton={true}>
|
||||||
|
<Modal.Title id='solution-viewer-modal-title'>
|
||||||
|
{t('settings.labels.solution-for', {
|
||||||
|
projectTitle: projectTitle
|
||||||
|
})}
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<SolutionViewer files={files} solution={solution} />
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button onClick={handleSolutionModalHide}>{t('buttons.close')}</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectModal.propTypes = propTypes;
|
||||||
|
ProjectModal.displayName = 'ProjectModal';
|
||||||
|
|
||||||
|
export default ProjectModal;
|
16
client/src/components/layouts/project-links.css
Normal file
16
client/src/components/layouts/project-links.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.project-link-button-override {
|
||||||
|
all: unset;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link-button-override:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--tertiary-color);
|
||||||
|
background-color: var(--tertiary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
@ -14,7 +14,7 @@ import { withTranslation } from 'react-i18next';
|
|||||||
import './timeline.css';
|
import './timeline.css';
|
||||||
import TimelinePagination from './TimelinePagination';
|
import TimelinePagination from './TimelinePagination';
|
||||||
import { FullWidthRow, Link } from '../../helpers';
|
import { FullWidthRow, Link } from '../../helpers';
|
||||||
import SolutionViewer from '../../settings/SolutionViewer';
|
import SolutionViewer from '../../SolutionViewer/SolutionViewer';
|
||||||
import {
|
import {
|
||||||
getCertIds,
|
getCertIds,
|
||||||
getPathFromID,
|
getPathFromID,
|
||||||
|
@ -7,8 +7,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Button,
|
Button,
|
||||||
DropdownButton,
|
DropdownButton,
|
||||||
MenuItem,
|
MenuItem
|
||||||
Modal
|
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import { Link, navigate } from 'gatsby';
|
import { Link, navigate } from 'gatsby';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
@ -20,7 +19,7 @@ import {
|
|||||||
} from '../../resources/certAndProjectMap';
|
} from '../../resources/certAndProjectMap';
|
||||||
|
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './SectionHeader';
|
||||||
import SolutionViewer from './SolutionViewer';
|
import ProjectModal from '../SolutionViewer/ProjectModal';
|
||||||
import { FullWidthRow, Spacer } from '../helpers';
|
import { FullWidthRow, Spacer } from '../helpers';
|
||||||
import { Form } from '../formHelpers';
|
import { Form } from '../formHelpers';
|
||||||
|
|
||||||
@ -599,28 +598,14 @@ export class CertificationSettings extends Component {
|
|||||||
{this.renderLegacyFullStack()}
|
{this.renderLegacyFullStack()}
|
||||||
{legacyCertifications.map(this.renderLegacyCertifications)}
|
{legacyCertifications.map(this.renderLegacyCertifications)}
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<Modal
|
<ProjectModal
|
||||||
aria-labelledby='solution-viewer-modal-title'
|
files={files}
|
||||||
bsSize='large'
|
handleSolutionModalHide={this.handleSolutionModalHide}
|
||||||
onHide={this.handleSolutionModalHide}
|
isOpen={isOpen}
|
||||||
show={isOpen}
|
projectTitle={projectTitle}
|
||||||
>
|
solution={solution}
|
||||||
<Modal.Header className='this-one?' closeButton={true}>
|
t={t}
|
||||||
<Modal.Title id='solution-viewer-modal-title'>
|
/>
|
||||||
{t('settings.labels.solution-for', {
|
|
||||||
projectTitle: projectTitle
|
|
||||||
})}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<SolutionViewer files={files} solution={solution} />
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={this.handleSolutionModalHide}>
|
|
||||||
{t('buttons.close')}
|
|
||||||
</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user