mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(client): use Gatsby Link for internal links (#55350)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
} from '../../../../shared/config/superblocks';
|
||||
import { SuperBlockIcon } from '../../assets/icons/superblock-icon';
|
||||
import LinkButton from '../../assets/icons/link-button';
|
||||
import { Link, Spacer } from '../helpers';
|
||||
import { Spacer, ButtonLink } from '../helpers';
|
||||
import { getSuperBlockTitleForMap } from '../../utils/superblock-map-titles';
|
||||
import { showUpcomingChanges } from '../../../config/env.json';
|
||||
|
||||
@@ -102,13 +102,18 @@ function MapLi({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link className='btn link-btn btn-lg' to={`/learn/${superBlock}/`}>
|
||||
<ButtonLink
|
||||
block
|
||||
size='large'
|
||||
className='map-superblock-link'
|
||||
href={`/learn/${superBlock}/`}
|
||||
>
|
||||
<div style={linkSpacingStyle}>
|
||||
<SuperBlockIcon className='map-icon' superBlock={superBlock} />
|
||||
{getSuperBlockTitleForMap(superBlock)}
|
||||
</div>
|
||||
{landing && <LinkButton />}
|
||||
</Link>
|
||||
</ButtonLink>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Link as GatsbyLink } from 'gatsby';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
|
||||
export type ButtonSize = 'small' | 'medium' | 'large';
|
||||
|
||||
interface Props extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
className?: string;
|
||||
size?: ButtonSize;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
block?: boolean;
|
||||
download?: string;
|
||||
target?: React.HTMLAttributeAnchorTarget;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ButtonLink is a link that looks like a button.
|
||||
* The component renders:
|
||||
* - Internal link with Gatsby's Link component
|
||||
* - External link with freecodecamp/ui's Button component,
|
||||
* which renders a plain `<a>` if it receives an `href`.
|
||||
* NOTE: We probably want to move this component to @freecodecamp/ui.
|
||||
*/
|
||||
export const ButtonLink = ({
|
||||
className,
|
||||
children,
|
||||
href,
|
||||
target,
|
||||
download,
|
||||
onClick,
|
||||
size = 'medium',
|
||||
block,
|
||||
disabled
|
||||
}: Props) => {
|
||||
// We only need to compute the styles of `size` and `block` for Gatsby Link.
|
||||
// freecodecamp/ui's Button already has the logic implemented.
|
||||
const cls = ['btn'];
|
||||
if (block) cls.push('btn-block');
|
||||
// The 'btn' class contains the base button styles as well as the `medium` variant styles.
|
||||
// So we only need to handle `large` and `small`.
|
||||
if (size === 'large') cls.push('btn-lg');
|
||||
if (size === 'small') cls.push('btn-sm');
|
||||
if (className) cls.push(className);
|
||||
const gatsbyLinkCls = cls.join(' ');
|
||||
|
||||
// Links cannot be disabled. So if `disabled` is true,
|
||||
// we pass the prop to `Button` in order to render a `<button>` instead of an `<a>`.
|
||||
if (disabled || target === '_blank' || download) {
|
||||
return (
|
||||
<Button
|
||||
variant='primary'
|
||||
className={className}
|
||||
href={href}
|
||||
target={target}
|
||||
onClick={onClick}
|
||||
download={download}
|
||||
size={size}
|
||||
block={block}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GatsbyLink
|
||||
className={gatsbyLinkCls}
|
||||
to={href}
|
||||
target={target}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</GatsbyLink>
|
||||
);
|
||||
};
|
||||
@@ -5,3 +5,4 @@ export { default as Spacer } from './spacer';
|
||||
export { default as Link } from './link';
|
||||
export { default as LazyImage } from './lazy-image';
|
||||
export { default as AvatarRenderer } from './avatar-renderer';
|
||||
export { ButtonLink } from './button-link';
|
||||
|
||||
@@ -291,6 +291,7 @@ input[type='submit'],
|
||||
border-radius: 0;
|
||||
text-decoration: none;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
@@ -370,13 +371,6 @@ fieldset[disabled] .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Equivalent to the `large` size of `@freecodecamp/ui` Button component */
|
||||
.btn-lg {
|
||||
padding: 10px 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.3333333;
|
||||
}
|
||||
|
||||
/* Equivalent to the `medium` size of `@freecodecamp/ui` Button component */
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
@@ -384,7 +378,21 @@ fieldset[disabled] .btn {
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
/* Equivalent to the `large` size of `@freecodecamp/ui` Button component */
|
||||
.btn-lg {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 24px;
|
||||
line-height: 1.3333333;
|
||||
}
|
||||
|
||||
/* Equivalent to the `small` size of `@freecodecamp/ui` Button component */
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.map-superblock-link {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -394,42 +402,42 @@ fieldset[disabled] .btn {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link-btn.btn-lg svg {
|
||||
.map-superblock-link.btn-lg svg {
|
||||
height: 100%;
|
||||
min-height: 20px;
|
||||
min-width: 16px;
|
||||
margin-inline-start: 5px;
|
||||
}
|
||||
|
||||
.link-btn:active {
|
||||
.map-superblock-link:active {
|
||||
background-color: var(--quaternary-background);
|
||||
}
|
||||
|
||||
.link-btn:focus,
|
||||
.link-btn.btn-lg:focus svg {
|
||||
.map-superblock-link:focus,
|
||||
.map-superblock-link.btn-lg:focus svg {
|
||||
fill: var(--quaternary-background);
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--gray-45);
|
||||
color: var(--secondary-background);
|
||||
}
|
||||
|
||||
.link-btn:focus:not(:focus-visible),
|
||||
.link-btn.btn-lg:focus:not(:focus-visible) svg {
|
||||
.map-superblock-link:focus:not(:focus-visible),
|
||||
.map-superblock-link.btn-lg:focus:not(:focus-visible) svg {
|
||||
background-color: var(--quaternary-background);
|
||||
border-color: var(--secondary-color);
|
||||
color: var(--secondary-color);
|
||||
fill: var(--primary-color);
|
||||
}
|
||||
|
||||
.link-btn:hover,
|
||||
.link-btn.btn-lg:hover svg {
|
||||
.map-superblock-link:hover,
|
||||
.map-superblock-link.btn-lg:hover svg {
|
||||
fill: var(--quaternary-background) !important;
|
||||
background-color: var(--secondary-color) !important;
|
||||
border-color: var(--secondary-color) !important;
|
||||
color: var(--secondary-background) !important;
|
||||
}
|
||||
|
||||
.link-btn.btn-lg svg,
|
||||
.map-superblock-link.btn-lg svg,
|
||||
.map-icon,
|
||||
.cert-header-icon {
|
||||
flex-shrink: 0;
|
||||
@@ -440,7 +448,7 @@ fieldset[disabled] .btn {
|
||||
fill: var(--quaternary-background);
|
||||
}
|
||||
|
||||
.link-btn.btn-lg:hover .map-icon .inverted-color,
|
||||
.map-superblock-link.btn-lg:hover .map-icon .inverted-color,
|
||||
.map-arrow-icon {
|
||||
fill: var(--secondary-color);
|
||||
}
|
||||
@@ -463,7 +471,7 @@ fieldset[disabled] .btn {
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.link-btn {
|
||||
.map-superblock-link {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.map-icon {
|
||||
|
||||
@@ -23,7 +23,7 @@ Intro project buttons and headings
|
||||
intro to courses section
|
||||
*/
|
||||
|
||||
[dir='rtl'] .link-btn.btn-lg > svg {
|
||||
[dir='rtl'] .map-superblock-link.btn-lg > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
|
||||
import { certificatesByNameSelector } from '../../../redux/selectors';
|
||||
import type { CurrentCert } from '../../../redux/prop-types';
|
||||
import { FullWidthRow, Spacer } from '../../helpers';
|
||||
import { FullWidthRow, Spacer, ButtonLink } from '../../helpers';
|
||||
|
||||
const mapStateToProps = (
|
||||
state: Record<string, unknown>,
|
||||
@@ -47,16 +46,15 @@ function CertButton({ username, cert }: CertButtonProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
<ButtonLink
|
||||
block
|
||||
size='large'
|
||||
href={`/certification/${username}/${cert.certSlug}`}
|
||||
data-playwright-test-label='claimed-certification'
|
||||
>
|
||||
{t('buttons.view-cert-title', {
|
||||
certTitle: t(`certification.title.${cert.certSlug}`)
|
||||
})}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
<Spacer size='small' />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,12 @@ import { Link } from 'gatsby';
|
||||
import React from 'react';
|
||||
import { withTranslation, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
|
||||
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { ChallengeWithCompletedNode } from '../../../redux/prop-types';
|
||||
import { SuperBlocks } from '../../../../../shared/config/superblocks';
|
||||
import { challengeTypes } from '../../../../../shared/config/challenge-types';
|
||||
import { ButtonLink } from '../../../components/helpers/button-link';
|
||||
|
||||
const getStepNumber = (dashedName: string) => {
|
||||
// dashedName should be in the format 'step-1' or 'task-1'
|
||||
@@ -68,12 +67,12 @@ function Challenges({
|
||||
<>
|
||||
{firstIncompleteChallenge && (
|
||||
<div className='challenge-jump-link'>
|
||||
<Button size='small' href={firstIncompleteChallenge.fields.slug}>
|
||||
<ButtonLink size='small' href={firstIncompleteChallenge.fields.slug}>
|
||||
{!isChallengeStarted
|
||||
? t('buttons.start-project')
|
||||
: t('buttons.resume-project')}{' '}
|
||||
{blockTitle && <span className='sr-only'>{blockTitle}</span>}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
</div>
|
||||
)}
|
||||
<nav
|
||||
|
||||
@@ -3,8 +3,8 @@ import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Container, Button } from '@freecodecamp/ui';
|
||||
import Spacer from '../../components/helpers/spacer';
|
||||
import { Container } from '@freecodecamp/ui';
|
||||
import { Spacer, ButtonLink } from '../../components/helpers';
|
||||
import FullWidthRow from '../../components/helpers/full-width-row';
|
||||
import LearnLayout from '../../components/layouts/learn';
|
||||
import type { MarkdownRemark, AllChallengeNode } from '../../redux/prop-types';
|
||||
@@ -44,13 +44,13 @@ function IntroductionPage({
|
||||
/>
|
||||
</FullWidthRow>
|
||||
<FullWidthRow>
|
||||
<Button block size='large' href={firstLessonPath}>
|
||||
<ButtonLink block size='large' href={firstLessonPath}>
|
||||
{t('buttons.first-lesson')}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
<Spacer size='small' />
|
||||
<Button block size='large' href='/learn'>
|
||||
<ButtonLink block size='large' href='/learn'>
|
||||
{t('buttons.view-curriculum')}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
<Spacer size='small' />
|
||||
<hr />
|
||||
</FullWidthRow>
|
||||
|
||||
@@ -15,7 +15,9 @@ test.describe('Public profile certifications', () => {
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('claimed-certification')).toHaveCount(19);
|
||||
await expect(
|
||||
page.getByRole('link', { name: /View.+Certification/ })
|
||||
).toHaveCount(19);
|
||||
});
|
||||
|
||||
test('Should show claimed certifications if the username includes uppercase characters', async ({
|
||||
@@ -37,7 +39,9 @@ test.describe('Public profile certifications', () => {
|
||||
}
|
||||
|
||||
await page.waitForURL('/certifiedboozer');
|
||||
await expect(page.getByTestId('claimed-certification')).toHaveCount(19);
|
||||
await expect(
|
||||
page.getByRole('link', { name: /View.+Certification/ })
|
||||
).toHaveCount(19);
|
||||
});
|
||||
|
||||
test.afterAll(() => {
|
||||
|
||||
Reference in New Issue
Block a user