fix(client): forum help and hints (#36625)
* fix(client): add warning on exceeding forum post length limit Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com> * fix: corrected guide url for backend challenges and projects * fix: moved getGuideUrl to utils * fix: added forumTopicId to ChallengeNode PropTypes * fix: conolidated helpers and add base url to env.json * fix: made use template literals * tests: created unit tests for getGuideUrl * fix: add characters which will be encoded Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix: use encoded version of title Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix: encode title Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -20,6 +20,7 @@ export const MarkdownRemark = PropTypes.shape({
|
|||||||
|
|
||||||
export const ChallengeNode = PropTypes.shape({
|
export const ChallengeNode = PropTypes.shape({
|
||||||
block: PropTypes.string,
|
block: PropTypes.string,
|
||||||
|
challengeOrder: PropTypes.number,
|
||||||
challengeType: PropTypes.number,
|
challengeType: PropTypes.number,
|
||||||
dashedName: PropTypes.string,
|
dashedName: PropTypes.string,
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
@ -31,9 +32,9 @@ export const ChallengeNode = PropTypes.shape({
|
|||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
blockName: PropTypes.string
|
blockName: PropTypes.string
|
||||||
}),
|
}),
|
||||||
|
forumTopicId: PropTypes.number,
|
||||||
guideUrl: PropTypes.string,
|
guideUrl: PropTypes.string,
|
||||||
head: PropTypes.arrayOf(PropTypes.string),
|
head: PropTypes.arrayOf(PropTypes.string),
|
||||||
challengeOrder: PropTypes.number,
|
|
||||||
instructions: PropTypes.string,
|
instructions: PropTypes.string,
|
||||||
isBeta: PropTypes.bool,
|
isBeta: PropTypes.bool,
|
||||||
isComingSoon: PropTypes.bool,
|
isComingSoon: PropTypes.bool,
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
updateProjectFormValues,
|
updateProjectFormValues,
|
||||||
backendNS
|
backendNS
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { createGuideUrl } from '../utils';
|
import { getGuideUrl } from '../utils';
|
||||||
|
|
||||||
import LearnLayout from '../../../components/layouts/Learn';
|
import LearnLayout from '../../../components/layouts/Learn';
|
||||||
import ChallengeTitle from '../components/Challenge-Title';
|
import ChallengeTitle from '../components/Challenge-Title';
|
||||||
@ -44,6 +44,7 @@ const propTypes = {
|
|||||||
}),
|
}),
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
executeChallenge: PropTypes.func.isRequired,
|
executeChallenge: PropTypes.func.isRequired,
|
||||||
|
forumTopicId: PropTypes.number,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
initConsole: PropTypes.func.isRequired,
|
initConsole: PropTypes.func.isRequired,
|
||||||
initTests: PropTypes.func.isRequired,
|
initTests: PropTypes.func.isRequired,
|
||||||
@ -155,8 +156,9 @@ export class BackEnd extends Component {
|
|||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
fields: { blockName, slug },
|
fields: { blockName },
|
||||||
challengeType,
|
challengeType,
|
||||||
|
forumTopicId,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
instructions
|
instructions
|
||||||
@ -210,7 +212,9 @@ export class BackEnd extends Component {
|
|||||||
updateProjectForm={updateProjectFormValues}
|
updateProjectForm={updateProjectFormValues}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ProjectToolPanel guideUrl={createGuideUrl(slug)} />
|
<ProjectToolPanel
|
||||||
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<Output
|
<Output
|
||||||
defaultOutput={`/**
|
defaultOutput={`/**
|
||||||
@ -247,6 +251,7 @@ export default connect(
|
|||||||
export const query = graphql`
|
export const query = graphql`
|
||||||
query BackendChallenge($slug: String!) {
|
query BackendChallenge($slug: String!) {
|
||||||
challengeNode(fields: { slug: { eq: $slug } }) {
|
challengeNode(fields: { slug: { eq: $slug } }) {
|
||||||
|
forumTopicId
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
instructions
|
instructions
|
||||||
|
@ -20,7 +20,7 @@ import ResetModal from '../components/ResetModal';
|
|||||||
import MobileLayout from './MobileLayout';
|
import MobileLayout from './MobileLayout';
|
||||||
import DesktopLayout from './DesktopLayout';
|
import DesktopLayout from './DesktopLayout';
|
||||||
|
|
||||||
import { createGuideUrl } from '../utils';
|
import { getGuideUrl } from '../utils';
|
||||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||||
import { ChallengeNode } from '../../../redux/propTypes';
|
import { ChallengeNode } from '../../../redux/propTypes';
|
||||||
import { dasherize } from '../../../../utils';
|
import { dasherize } from '../../../../utils';
|
||||||
@ -166,13 +166,6 @@ class ShowClassic extends Component {
|
|||||||
return `${blockName}: ${title}`;
|
return `${blockName}: ${title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuideUrl() {
|
|
||||||
const { forumTopicId, title } = this.getChallenge();
|
|
||||||
return forumTopicId
|
|
||||||
? 'https://www.freecodecamp.org/forum/t/' + forumTopicId
|
|
||||||
: createGuideUrl(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideoUrl = () => this.getChallenge().videoUrl;
|
getVideoUrl = () => this.getChallenge().videoUrl;
|
||||||
|
|
||||||
getChallengeFile() {
|
getChallengeFile() {
|
||||||
@ -200,11 +193,13 @@ class ShowClassic extends Component {
|
|||||||
nextChallengePath,
|
nextChallengePath,
|
||||||
prevChallengePath
|
prevChallengePath
|
||||||
} = this.props.pageContext.challengeMeta;
|
} = this.props.pageContext.challengeMeta;
|
||||||
|
|
||||||
|
const { forumTopicId, title } = this.getChallenge();
|
||||||
return (
|
return (
|
||||||
<SidePanel
|
<SidePanel
|
||||||
className='full-height'
|
className='full-height'
|
||||||
description={description}
|
description={description}
|
||||||
guideUrl={this.getGuideUrl()}
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||||
instructions={instructions}
|
instructions={instructions}
|
||||||
introPath={introPath}
|
introPath={introPath}
|
||||||
nextChallengePath={nextChallengePath}
|
nextChallengePath={nextChallengePath}
|
||||||
@ -247,6 +242,7 @@ class ShowClassic extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { forumTopicId, title } = this.getChallenge();
|
||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<LearnLayout>
|
||||||
<Helmet
|
<Helmet
|
||||||
@ -255,7 +251,7 @@ class ShowClassic extends Component {
|
|||||||
<Media maxWidth={MAX_MOBILE_WIDTH}>
|
<Media maxWidth={MAX_MOBILE_WIDTH}>
|
||||||
<MobileLayout
|
<MobileLayout
|
||||||
editor={this.renderEditor()}
|
editor={this.renderEditor()}
|
||||||
guideUrl={this.getGuideUrl()}
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||||
hasPreview={this.hasPreview()}
|
hasPreview={this.hasPreview()}
|
||||||
instructions={this.renderInstructionsPanel({
|
instructions={this.renderInstructionsPanel({
|
||||||
showToolPanel: false
|
showToolPanel: false
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
updateProjectFormValues
|
updateProjectFormValues
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { frontEndProject } from '../../../../utils/challengeTypes';
|
import { frontEndProject } from '../../../../utils/challengeTypes';
|
||||||
import { createGuideUrl } from '../utils';
|
import { getGuideUrl } from '../utils';
|
||||||
|
|
||||||
import LearnLayout from '../../../components/layouts/Learn';
|
import LearnLayout from '../../../components/layouts/Learn';
|
||||||
import Spacer from '../../../components/helpers/Spacer';
|
import Spacer from '../../../components/helpers/Spacer';
|
||||||
@ -93,7 +93,8 @@ export class Project extends Component {
|
|||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
challengeType,
|
challengeType,
|
||||||
fields: { blockName, slug },
|
fields: { blockName },
|
||||||
|
forumTopicId,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
guideUrl
|
guideUrl
|
||||||
@ -127,7 +128,7 @@ export class Project extends Component {
|
|||||||
onSubmit={openCompletionModal}
|
onSubmit={openCompletionModal}
|
||||||
updateProjectForm={updateProjectFormValues}
|
updateProjectForm={updateProjectFormValues}
|
||||||
/>
|
/>
|
||||||
<ToolPanel guideUrl={createGuideUrl(slug)} />
|
<ToolPanel guideUrl={getGuideUrl({ forumTopicId, title })} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</div>
|
</div>
|
||||||
<CompletionModal />
|
<CompletionModal />
|
||||||
@ -148,6 +149,7 @@ export default connect(
|
|||||||
export const query = graphql`
|
export const query = graphql`
|
||||||
query ProjectChallenge($slug: String!) {
|
query ProjectChallenge($slug: String!) {
|
||||||
challengeNode(fields: { slug: { eq: $slug } }) {
|
challengeNode(fields: { slug: { eq: $slug } }) {
|
||||||
|
forumTopicId
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
challengeType
|
challengeType
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import dedent from 'dedent';
|
||||||
import { ofType } from 'redux-observable';
|
import { ofType } from 'redux-observable';
|
||||||
import {
|
import {
|
||||||
types,
|
types,
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { tap, mapTo } from 'rxjs/operators';
|
import { tap, mapTo } from 'rxjs/operators';
|
||||||
import { helpCategory } from '../../../../utils/challengeTypes';
|
import { helpCategory } from '../../../../utils/challengeTypes';
|
||||||
|
import { forumLocation } from '../../../../../config/env.json';
|
||||||
|
|
||||||
function filesToMarkdown(files = {}) {
|
function filesToMarkdown(files = {}) {
|
||||||
const moreThenOneFile = Object.keys(files).length > 1;
|
const moreThenOneFile = Object.keys(files).length > 1;
|
||||||
@ -17,17 +19,7 @@ function filesToMarkdown(files = {}) {
|
|||||||
}
|
}
|
||||||
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
|
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
|
||||||
const fileType = file.ext;
|
const fileType = file.ext;
|
||||||
return (
|
return `${fileString}\`\`\`${fileType}\n${fileName}\n${file.contents}\n\`\`\`\n\n`;
|
||||||
fileString +
|
|
||||||
'```' +
|
|
||||||
fileType +
|
|
||||||
'\n' +
|
|
||||||
fileName +
|
|
||||||
'\n' +
|
|
||||||
file.contents +
|
|
||||||
'\n' +
|
|
||||||
'```\n\n'
|
|
||||||
);
|
|
||||||
}, '\n');
|
}, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,28 +36,59 @@ function createQuestionEpic(action$, state$, { window }) {
|
|||||||
navigator: { userAgent },
|
navigator: { userAgent },
|
||||||
location: { href }
|
location: { href }
|
||||||
} = window;
|
} = window;
|
||||||
const textMessage = [
|
|
||||||
"**Tell us what's happening:**\n\n\n\n",
|
const endingText = dedent(
|
||||||
'**Your code so far**\n',
|
`**Your browser information:**
|
||||||
filesToMarkdown(files),
|
|
||||||
'**Your browser information:**\n\n',
|
User Agent is: <code>${userAgent}</code>.
|
||||||
'User Agent is: `',
|
|
||||||
userAgent,
|
**Challenge:** ${challengeTitle}
|
||||||
'`.\n\n',
|
|
||||||
'**Challenge:**\n',
|
**Link to the challenge:**
|
||||||
challengeTitle,
|
${href}`
|
||||||
'\n**Link to the challenge:**\n',
|
|
||||||
href
|
|
||||||
].join('');
|
|
||||||
window.open(
|
|
||||||
'https://forum.freecodecamp.org/new-topic' +
|
|
||||||
'?category=' +
|
|
||||||
window.encodeURIComponent(helpCategory[challengeType] || 'Help') +
|
|
||||||
'&title=' +
|
|
||||||
'&body=' +
|
|
||||||
window.encodeURIComponent(textMessage),
|
|
||||||
'_blank'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let textMessage = dedent(
|
||||||
|
`**Tell us what's happening:**\n\n\n\n**Your code so far**
|
||||||
|
${filesToMarkdown(files)}\n${endingText}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const altTextMessage = dedent(
|
||||||
|
`**Tell us what's happening:**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Your code so far**
|
||||||
|
|
||||||
|
WARNING
|
||||||
|
|
||||||
|
The challenge seed code and/or your solution exceeded the maximum length we can port over from the challenge.
|
||||||
|
|
||||||
|
You will need to take an additional step here so the code you wrote presents in an easy to read format.
|
||||||
|
|
||||||
|
Please copy/paste all the editor code showing in the challenge from where you just linked.
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Replace these two sentences with your copied code.
|
||||||
|
Please leave the \`\`\` line above and the \`\`\` line below,
|
||||||
|
because they allow your code to properly format in the post.
|
||||||
|
|
||||||
|
\`\`\`\n${endingText}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const category = window.encodeURIComponent(
|
||||||
|
helpCategory[challengeType] || 'Help'
|
||||||
|
);
|
||||||
|
|
||||||
|
const studentCode = window.encodeURIComponent(textMessage);
|
||||||
|
const altStudentCode = window.encodeURIComponent(altTextMessage);
|
||||||
|
|
||||||
|
const baseURI = `${forumLocation}/new-topic?category=${category}&title=&body=`;
|
||||||
|
const defaultURI = `${baseURI}${studentCode}`;
|
||||||
|
const altURI = `${baseURI}${altStudentCode}`;
|
||||||
|
|
||||||
|
window.open(defaultURI.length < 8000 ? defaultURI : altURI, '_blank');
|
||||||
}),
|
}),
|
||||||
mapTo(closeModal('help'))
|
mapTo(closeModal('help'))
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
const guideBase = 'https://www.freecodecamp.org/forum/search?q=';
|
import { forumLocation } from '../../../../../config/env.json';
|
||||||
|
|
||||||
export function createGuideUrl(title = '') {
|
export function getGuideUrl({ forumTopicId, title = '' }) {
|
||||||
return guideBase + title + '%20in%3Atitle%20order%3Aviews';
|
title = encodeURIComponent(title);
|
||||||
|
return forumTopicId
|
||||||
|
? `${forumLocation}/t/${forumTopicId}`
|
||||||
|
: `${forumLocation}/search?q=${title}%20in%3Atitle%20order%3Aviews`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGoodXHRStatus(status) {
|
export function isGoodXHRStatus(status) {
|
||||||
|
26
client/src/templates/Challenges/utils/index.test.js
Normal file
26
client/src/templates/Challenges/utils/index.test.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/* global expect */
|
||||||
|
|
||||||
|
import { forumLocation } from '../../../../../config/env.json';
|
||||||
|
|
||||||
|
const { getGuideUrl } = require('./');
|
||||||
|
|
||||||
|
describe('index', () => {
|
||||||
|
describe('getGuideUrl', () => {
|
||||||
|
it('should use forum topic url when forumTopicId is supplied', () => {
|
||||||
|
const value = getGuideUrl({
|
||||||
|
forumTopicId: 12345,
|
||||||
|
title: 'a sample title'
|
||||||
|
});
|
||||||
|
expect(value).toEqual(`${forumLocation}/t/12345`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use search endpoint when no forumTopicId is supplied', () => {
|
||||||
|
const value = getGuideUrl({
|
||||||
|
title: '& a sample title?'
|
||||||
|
});
|
||||||
|
expect(value).toEqual(
|
||||||
|
`${forumLocation}/search?q=%26%20a%20sample%20title%3F%20in%3Atitle%20order%3Aviews`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user