From 63a4729e9dcfad853c1d227b3496b92070a05b35 Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Tue, 11 Mar 2025 04:21:02 -0500 Subject: [PATCH] feat(client): add local instructions for rdb courses (#59184) Co-authored-by: Oliver Eyton-Williams --- client/i18n/locales/english/translations.json | 28 +++ .../components/Flash/redux/flash-messages.ts | 6 + .../growth-book/codeally-button.tsx | 17 +- .../codeally/rdb-gitpod-continue-alert.tsx | 39 ++++ .../codeally/rdb-gitpod-instructions.tsx | 42 ++++ .../codeally/rdb-gitpod-logout-alert.tsx | 23 ++ .../codeally/rdb-local-instructions.tsx | 212 ++++++++++++++++++ .../codeally/rdb-local-logout-alert.tsx | 23 ++ .../codeally/rdb-step-1-instructions.tsx | 32 +++ .../codeally/rdb-step-2-instructions.tsx | 33 +++ .../templates/Challenges/codeally/show.tsx | 209 ++++++++--------- client/src/utils/tone/index.ts | 6 + 12 files changed, 552 insertions(+), 118 deletions(-) create mode 100644 client/src/templates/Challenges/codeally/rdb-gitpod-continue-alert.tsx create mode 100644 client/src/templates/Challenges/codeally/rdb-gitpod-instructions.tsx create mode 100644 client/src/templates/Challenges/codeally/rdb-gitpod-logout-alert.tsx create mode 100644 client/src/templates/Challenges/codeally/rdb-local-instructions.tsx create mode 100644 client/src/templates/Challenges/codeally/rdb-local-logout-alert.tsx create mode 100644 client/src/templates/Challenges/codeally/rdb-step-1-instructions.tsx create mode 100644 client/src/templates/Challenges/codeally/rdb-step-2-instructions.tsx diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 63da34ce813..a7b5d3a1386 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -509,6 +509,28 @@ "learn-more": "Learn more about <0>Gitpod workspaces.", "logout-warning": "If you log out of freeCodeCamp before you complete the entire {{course}} course, your progress will not be saved to your freeCodeCamp account." }, + "local": { + "intro": "This course runs in a virtual Linux machine on your computer. To run the course, you first need to download each of the following if you don't already have them:", + "download-vscode": "<0>VS Code and the <1>Dev Containers extension", + "heading": "Then, follow these instructions to start the course:", + "step-1": "Open a terminal and clone the RDB Alpha repo if you don't already have it with <0>git clone https://github.com/freeCodeCamp/rdb-alpha", + "step-2": "Navigate to the <0>rdb-alpha directory in the terminal with <1>cd rdb-alpha, and open VS Code with <2>code .", + "sub-step-heading": "If you want to save your progress to your freeCodeCamp account, do the following:", + "sub-step-1": "Generate a user token if you don't already have one:", + "generate-token-btn": "Generate User Token", + "sub-step-2": "Copy your user token:", + "copy-token-btn": "Copy User Token", + "logout-warning": "If you log out of freeCodeCamp before you complete the entire {{course}} course, your user token will be deleted and your progress will not be saved to your freeCodeCamp account.", + "sub-step-3": "In the VS Code that opened, find and open the file named <0>Dockerfile. At the bottom of the file, paste your token in as the value for the <1>CODEROAD_WEBHOOK_TOKEN variable. It should look like this: <2>ENV CODEROAD_WEBHOOK_TOKEN=your-token-here", + "step-3": "Open the command palette in VS Code by expanding the \"View\" menu and clicking \"Command Palette...\" and enter <0>Dev Containers: Rebuild and Reopen in Container in the input.", + "step-4": "A new VS Code window will open and begin building the Docker image. It will take several minutes the first time.", + "step-5": "Once it is finished building, open the command palette again and enter <0>CodeRoad: Start to open CodeRoad.", + "step-6": "In the CodeRoad window, click \"Start New Tutorial\" and then the \"URL\" tab at the top.", + "step-7": "Copy the course URL below, paste it in the URL input, and click \"Load\".", + "copy-url": "Copy Course URL", + "step-8": "Click \"Start\" to begin.", + "step-9": "Follow the instructions in CodeRoad to complete the course. Note: You may need to restart the terminal once for terminal settings to take effect and the tests to pass." + }, "step-1": "Step 1: Complete the project", "step-2": "Step 2: Submit your code", "submit-public-url": "When you have completed the project, save all the required files into a public repository and submit the URL to it below.", @@ -918,6 +940,12 @@ "generate-exam-error": "An error occurred trying to generate your exam.", "cert-not-found": "The certification {{certSlug}} does not exist.", "reset-editor-layout": "Your editor layout has been reset.", + "user-token-generated": "A user token was created for you.", + "user-token-generate-error": "Something went wrong trying to generate a user token for you.", + "user-token-copied": "User token copied to clipboard.", + "user-token-copy-error": "Something went wrong trying to copy your token.", + "course-url-copied": "Course URL copied to clipboard.", + "course-url-copy-error": "Something went wrong trying to copy the course URL.", "ms": { "transcript": { "link-err-1": "Please include a Microsoft transcript URL in the request.", diff --git a/client/src/components/Flash/redux/flash-messages.ts b/client/src/components/Flash/redux/flash-messages.ts index bd50688b800..387dc296a99 100644 --- a/client/src/components/Flash/redux/flash-messages.ts +++ b/client/src/components/Flash/redux/flash-messages.ts @@ -11,6 +11,8 @@ export enum FlashMessages { CodeSaveError = 'flash.code-save-error', CodeSaveLess = 'flash.code-save-less', CompleteProjectFirst = 'flash.complete-project-first', + CourseUrlCopied = 'flash.course-url-copied', + CourseUrlCopyError = 'flash.course-url-copy-error', DeleteTokenErr = 'flash.delete-token-err', EmailValid = 'flash.email-valid', GenerateExamError = 'flash.generate-exam-error', @@ -66,6 +68,10 @@ export enum FlashMessages { UsernameUpdated = 'flash.username-updated', UsernameUsed = 'flash.username-used', UserNotCertified = 'flash.user-not-certified', + UserTokenCopied = 'flash.user-token-copied', + UserTokenCopyError = 'flash.user-token-copy-error', + UserTokenGenerated = 'flash.user-token-generated', + UserTokenGenerateError = 'flash.user-token-generate-error', WrongName = 'flash.wrong-name', WrongUpdating = 'flash.wrong-updating', WentWrong = 'flash.went-wrong' diff --git a/client/src/components/growth-book/codeally-button.tsx b/client/src/components/growth-book/codeally-button.tsx index 3f06bb94ff6..97f42438347 100644 --- a/client/src/components/growth-book/codeally-button.tsx +++ b/client/src/components/growth-book/codeally-button.tsx @@ -4,25 +4,34 @@ import React from 'react'; import { useFeature } from '@growthbook/growthbook-react'; import { useTranslation } from 'react-i18next'; import { Button } from '@freecodecamp/ui'; +import { challengeTypes } from '../../../../shared/config/challenge-types'; interface CodeAllyButtonProps { - text: string; + challengeType: number; onClick: () => void; } -export function CodeAllyButton(props: CodeAllyButtonProps): JSX.Element | null { +export function CodeAllyButton({ + challengeType, + onClick +}: CodeAllyButtonProps): JSX.Element | null { const codeAllyDisabledFeature = useFeature('codeally_disabled'); const { t } = useTranslation(); + const text = + challengeType === challengeTypes.codeAllyCert + ? t('buttons.click-start-project') + : t('buttons.click-start-course'); + return ( ); diff --git a/client/src/templates/Challenges/codeally/rdb-gitpod-continue-alert.tsx b/client/src/templates/Challenges/codeally/rdb-gitpod-continue-alert.tsx new file mode 100644 index 00000000000..e5c98e7d5fd --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-gitpod-continue-alert.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Trans } from 'react-i18next'; +import { Alert, Spacer } from '@freecodecamp/ui'; + +interface RdbGitpodContinueAlertProps { + course: string; +} + +function RdbGitpodContinueAlert({ + course +}: RdbGitpodContinueAlertProps): JSX.Element { + return ( + + + + placeholder + + + + + + placeholder + + + + ); +} + +RdbGitpodContinueAlert.displayName = 'RdbGitpodContinueAlert'; + +export default RdbGitpodContinueAlert; diff --git a/client/src/templates/Challenges/codeally/rdb-gitpod-instructions.tsx b/client/src/templates/Challenges/codeally/rdb-gitpod-instructions.tsx new file mode 100644 index 00000000000..26d3849761a --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-gitpod-instructions.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +function RdbGitpodInstructions(): JSX.Element { + const { t } = useTranslation(); + + return ( +
+

{t('learn.gitpod.intro')}

+
    +
  1. + + + placeholder + + +
  2. +
  3. {t('learn.gitpod.step-2')}
  4. +
  5. {t('learn.gitpod.step-3')}
  6. +
  7. + {t('learn.gitpod.step-4')} +
      +
    • {t('learn.gitpod.step-5')}
    • +
    • {t('learn.gitpod.step-6')}
    • +
    • {t('learn.gitpod.step-7')}
    • +
    • {t('learn.gitpod.step-8')}
    • +
    +
  8. +
  9. {t('learn.gitpod.step-9')}
  10. +
+
+ ); +} + +RdbGitpodInstructions.displayName = 'RdbGitpodInstructions'; + +export default RdbGitpodInstructions; diff --git a/client/src/templates/Challenges/codeally/rdb-gitpod-logout-alert.tsx b/client/src/templates/Challenges/codeally/rdb-gitpod-logout-alert.tsx new file mode 100644 index 00000000000..11abcfe3919 --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-gitpod-logout-alert.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert } from '@freecodecamp/ui'; + +interface RdbGitpodLogoutAlertProps { + course: string; +} + +function RdbGitpodLogoutAlert({ + course +}: RdbGitpodLogoutAlertProps): JSX.Element { + const { t } = useTranslation(); + + return ( + + {t('learn.gitpod.logout-warning', { course })} + + ); +} + +RdbGitpodLogoutAlert.displayName = 'RdbGitpodLogoutAlert'; + +export default RdbGitpodLogoutAlert; diff --git a/client/src/templates/Challenges/codeally/rdb-local-instructions.tsx b/client/src/templates/Challenges/codeally/rdb-local-instructions.tsx new file mode 100644 index 00000000000..a1394f8d2d0 --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-local-instructions.tsx @@ -0,0 +1,212 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Trans, useTranslation } from 'react-i18next'; +import { Spacer, Button } from '@freecodecamp/ui'; +import { postUserToken } from '../../../utils/ajax'; +import { createFlashMessage } from '../../../components/Flash/redux'; +import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; + +import { + isSignedInSelector, + userTokenSelector +} from '../../../redux/selectors'; +import { updateUserToken } from '../../../redux/actions'; +import { Link } from '../../../components/helpers'; + +import RdbLocalLogoutAlert from './rdb-local-logout-alert'; + +const mapStateToProps = (state: unknown) => ({ + isSignedIn: isSignedInSelector(state), + userToken: userTokenSelector(state) as string | null +}); + +const mapDispatchToProps = { + createFlashMessage, + updateUserToken +}; + +interface RdbLocalInstructionsProps { + course: string; + createFlashMessage: typeof createFlashMessage; + isSignedIn: boolean; + updateUserToken: (arg0: string) => void; + url: string; + userToken: string | null; +} + +function RdbLocalInstructions({ + course, + createFlashMessage, + isSignedIn, + updateUserToken, + url, + userToken +}: RdbLocalInstructionsProps): JSX.Element { + const { t } = useTranslation(); + + const coderoadTutorial = `https://raw.githubusercontent.com/${url}/main/tutorial.json`; + + const generateUserToken = async () => { + const createUserTokenResponse = await postUserToken(); + const { data = { userToken: null } } = createUserTokenResponse; + + if (data?.userToken) { + updateUserToken(data.userToken); + createFlashMessage({ + type: 'success', + message: FlashMessages.UserTokenGenerated + }); + } else { + createFlashMessage({ + type: 'danger', + message: FlashMessages.UserTokenGenerateError + }); + } + }; + + const copyUserToken = () => { + navigator.clipboard.writeText(userToken ?? '').then( + () => { + createFlashMessage({ + type: 'success', + message: FlashMessages.UserTokenCopied + }); + }, + () => { + createFlashMessage({ + type: 'danger', + message: FlashMessages.UserTokenCopyError + }); + } + ); + }; + + const copyUrl = () => { + navigator.clipboard.writeText(coderoadTutorial ?? '').then( + () => { + createFlashMessage({ + type: 'success', + message: FlashMessages.CourseUrlCopied + }); + }, + () => { + createFlashMessage({ + type: 'danger', + message: FlashMessages.CourseUrlCopyError + }); + } + ); + }; + + return ( +
+

{t('learn.local.intro')}

+ + +

{t('learn.local.heading')}

+
    +
  1. + + placeholder + +
  2. +
  3. + + placeholder + placeholder + placeholder + +
  4. + {isSignedIn && ( + <> + +

    {t('learn.local.sub-step-heading')}

    +
      +
    1. {t('learn.local.sub-step-1')}
    2. + + + +
    3. {t('learn.local.sub-step-2')}
    4. + + + +
    5. + + placeholder + placeholder + placeholder + +
    6. + + +
    + + + )} +
  5. + + placeholder + +
  6. +
  7. {t('learn.local.step-4')}
  8. +
  9. + + placeholder + +
  10. +
  11. {t('learn.local.step-6')}
  12. +
  13. {t('learn.local.step-7')}
  14. + + + +
  15. {t('learn.local.step-8')}
  16. +
  17. {t('learn.local.step-9')}
  18. +
+
+ ); +} + +RdbLocalInstructions.displayName = 'RdbLocalInstructions'; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(RdbLocalInstructions); diff --git a/client/src/templates/Challenges/codeally/rdb-local-logout-alert.tsx b/client/src/templates/Challenges/codeally/rdb-local-logout-alert.tsx new file mode 100644 index 00000000000..60edc7f4992 --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-local-logout-alert.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert } from '@freecodecamp/ui'; + +interface RdbLocalLogoutAlertProps { + course: string; +} + +function RdbLocalLogoutAlert({ + course +}: RdbLocalLogoutAlertProps): JSX.Element { + const { t } = useTranslation(); + + return ( + + {t('learn.local.logout-warning', { course })} + + ); +} + +RdbLocalLogoutAlert.displayName = 'RdbLocalLogoutAlert'; + +export default RdbLocalLogoutAlert; diff --git a/client/src/templates/Challenges/codeally/rdb-step-1-instructions.tsx b/client/src/templates/Challenges/codeally/rdb-step-1-instructions.tsx new file mode 100644 index 00000000000..a69f42eaddf --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-step-1-instructions.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Spacer } from '@freecodecamp/ui'; + +import ChallengeHeading from '../components/challenge-heading'; +import PrismFormatted from '../components/prism-formatted'; + +interface RdbStep1InstructionsProps { + instructions: string; + isCompleted: boolean; +} + +function RdbStep1Instructions({ + instructions, + isCompleted +}: RdbStep1InstructionsProps): JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + +
{t('learn.runs-in-vm')}
+ + + + ); +} + +RdbStep1Instructions.displayName = 'RdbStep1Instructions'; + +export default RdbStep1Instructions; diff --git a/client/src/templates/Challenges/codeally/rdb-step-2-instructions.tsx b/client/src/templates/Challenges/codeally/rdb-step-2-instructions.tsx new file mode 100644 index 00000000000..77ade0efd06 --- /dev/null +++ b/client/src/templates/Challenges/codeally/rdb-step-2-instructions.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { useTranslation } from 'react-i18next'; +import { Spacer } from '@freecodecamp/ui'; + +import ChallengeHeading from '../components/challenge-heading'; +import PrismFormatted from '../components/prism-formatted'; + +interface RdbStep2InstructionsProps { + notes: string; + isCompleted: boolean; +} + +function RdbStep2Instructions({ + isCompleted, + notes +}: RdbStep2InstructionsProps): JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + +
{t('learn.submit-public-url')}
+ + + + ); +} + +RdbStep2Instructions.displayName = 'RdbStep2Instructions'; + +export default RdbStep2Instructions; diff --git a/client/src/templates/Challenges/codeally/show.tsx b/client/src/templates/Challenges/codeally/show.tsx index 04a3549e4f4..b7be3b0f5bd 100644 --- a/client/src/templates/Challenges/codeally/show.tsx +++ b/client/src/templates/Challenges/codeally/show.tsx @@ -3,17 +3,17 @@ import { graphql } from 'gatsby'; import React, { useEffect, useRef } from 'react'; import Helmet from 'react-helmet'; import type { TFunction } from 'i18next'; -import { Trans, withTranslation } from 'react-i18next'; +import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import type { Dispatch } from 'redux'; import { createSelector } from 'reselect'; -import { Container, Col, Row, Alert, Spacer } from '@freecodecamp/ui'; +import { Container, Col, Row, Spacer } from '@freecodecamp/ui'; +import { useFeature } from '@growthbook/growthbook-react'; // Local Utilities import LearnLayout from '../../../components/layouts/learn'; import ChallengeTitle from '../components/challenge-title'; -import ChallengeHeading from '../components/challenge-heading'; import PrismFormatted from '../components/prism-formatted'; import { challengeTypes } from '../../../../../shared/config/challenge-types'; import CompletionModal from '../components/completion-modal'; @@ -48,9 +48,16 @@ import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; import { SuperBlocks } from '../../../../../shared/config/curriculum'; import { CodeAllyDown } from '../../../components/growth-book/codeally-down'; import { postUserToken } from '../../../utils/ajax'; +import { CodeAllyButton } from '../../../components/growth-book/codeally-button'; + +import RdbGitpodContinueAlert from './rdb-gitpod-continue-alert'; +import RdbGitpodInstructions from './rdb-gitpod-instructions'; +import RdbGitpodLogoutAlert from './rdb-gitpod-logout-alert'; +import RdbLocalInstructions from './rdb-local-instructions'; +import RdbStep1Instructions from './rdb-step-1-instructions'; +import RdbStep2Instructions from './rdb-step-2-instructions'; import './codeally.css'; -import { CodeAllyButton } from '../../../components/growth-book/codeally-button'; // Redux const mapStateToProps = createSelector( @@ -125,7 +132,8 @@ function ShowCodeAlly(props: ShowCodeAllyProps) { notes, superBlock, title, - translationPending + translationPending, + url } } }, @@ -261,6 +269,8 @@ function ShowCodeAlly(props: ShowCodeAllyProps) { } }; + const gitpodDeprecated = useFeature('gitpod-deprecated').on; + return ( @@ -280,121 +290,92 @@ function ShowCodeAlly(props: ShowCodeAllyProps) { -
-

{t('learn.gitpod.intro')}

-
    -
  1. - - - placeholder - - -
  2. - -
  3. {t('learn.gitpod.step-2')}
  4. -
  5. {t('learn.gitpod.step-3')}
  6. -
  7. - {t('learn.gitpod.step-4')} -
      -
    • {t('learn.gitpod.step-5')}
    • -
    • {t('learn.gitpod.step-6')}
    • -
    • {t('learn.gitpod.step-7')}
    • -
    • {t('learn.gitpod.step-8')}
    • -
    -
  8. - -
  9. {t('learn.gitpod.step-9')}
  10. -
-
- - - {isSignedIn && challengeType === challengeTypes.codeAllyCert && ( + {gitpodDeprecated ? ( <> -
- {t('learn.complete-both-steps')} -
-
+ - - -
{t('learn.runs-in-vm')}
- - + {isSignedIn && + challengeType === challengeTypes.codeAllyCert && ( + <> +
+ {t('learn.complete-both-steps')} +
+
+ + +
+ + + + + + )} + + ) : ( + <> + + {isSignedIn && + challengeType === challengeTypes.codeAllyCert ? ( + <> +
+ {t('learn.complete-both-steps')} +
+
+ + + + + {isSignedIn && } + +
+ + + + + + ) : ( + <> + + {isSignedIn && } + + + )} + )} - - - - placeholder - - - - - - placeholder - - - - {isSignedIn && ( - - {t('learn.gitpod.logout-warning', { course: title })} - - )} - - {isSignedIn && challengeType === challengeTypes.codeAllyCert && ( - <> -
- - - -
- {t('learn.submit-public-url')} -
- - - - - - )} - +
diff --git a/client/src/utils/tone/index.ts b/client/src/utils/tone/index.ts index 97dcbd48d20..0707d59ac96 100644 --- a/client/src/utils/tone/index.ts +++ b/client/src/utils/tone/index.ts @@ -25,6 +25,8 @@ const toneUrls = { [FlashMessages.CodeSaveError]: TRY_AGAIN, [FlashMessages.CodeSaveLess]: TRY_AGAIN, [FlashMessages.CompleteProjectFirst]: TRY_AGAIN, + [FlashMessages.CourseUrlCopied]: CHAL_COMP, + [FlashMessages.CourseUrlCopyError]: TRY_AGAIN, [FlashMessages.DeleteTokenErr]: TRY_AGAIN, [FlashMessages.EmailValid]: CHAL_COMP, [FlashMessages.GenerateExamError]: TRY_AGAIN, @@ -80,6 +82,10 @@ const toneUrls = { [FlashMessages.UsernameUpdated]: CHAL_COMP, [FlashMessages.UsernameUsed]: TRY_AGAIN, [FlashMessages.UserNotCertified]: TRY_AGAIN, + [FlashMessages.UserTokenCopied]: CHAL_COMP, + [FlashMessages.UserTokenCopyError]: TRY_AGAIN, + [FlashMessages.UserTokenGenerated]: CHAL_COMP, + [FlashMessages.UserTokenGenerateError]: TRY_AGAIN, [FlashMessages.WrongName]: TRY_AGAIN, [FlashMessages.WrongUpdating]: TRY_AGAIN, [FlashMessages.WentWrong]: TRY_AGAIN