diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp
index 059a080739..6ed7912eaf 100644
--- a/include/nlohmann/detail/input/lexer.hpp
+++ b/include/nlohmann/detail/input/lexer.hpp
@@ -1049,6 +1049,7 @@ class lexer : public lexer_base<BasicJsonType>
                decimal_point_position =
@@ -1085,6 +1086,7 @@ class lexer : public lexer_base<BasicJsonType>
                decimal_point_position =
@@ -1322,6 +1324,7 @@ class lexer : public lexer_base<BasicJsonType>
        decimal_point_position =
@@ -1430,6 +1433,11 @@ class lexer : public lexer_base<BasicJsonType>
        // translate decimal points from locale back to '.' (#4084)
        if (decimal_point_char != '.' && decimal_point_position != std::string::npos)
        {
@@ -1627,6 +1635,8 @@ class lexer : public lexer_base<BasicJsonType>
    /// the position of the decimal point in the input
    std::size_t decimal_point_position =

diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index ccc07626aa..31ba37060e 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -104,23 +104,29 @@ jobs:

   ci_test_coverage:
     runs-on: ubuntu-latest
-    container: ghcr.io/nlohmann/json-ci:v2.4.0
     permissions:
       contents: read
       checks: write
     steps:
       - uses: actions/checkout@v4
+      - name: Install dependencies and de_DE locale
+        run: |
+          sudo apt-get clean
+          sudo apt-get update
+          sudo apt-get install -y build-essential cmake lcov ninja-build make locales gcc-multilib g++-multilib
+          sudo locale-gen de_DE
+          sudo update-locale
       - name: Run CMake
         run: cmake -S . -B build -DJSON_CI=On
       - name: Build
         run: cmake --build build --target ci_test_coverage
       - name: Archive coverage report
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: code-coverage-report
           path: ${{ github.workspace }}/build/html
       - name: Publish report to Coveralls
-        uses: coverallsapp/github-action@master
+        uses: coverallsapp/github-action@v2.3.4
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
           path-to-lcov: ${{ github.workspace }}/build/json.info.filtered.noexcept
diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp
index 059a080739..6ed7912eaf 100644
--- a/include/nlohmann/detail/input/lexer.hpp
+++ b/include/nlohmann/detail/input/lexer.hpp
@@ -1049,6 +1049,7 @@ class lexer : public lexer_base<BasicJsonType>
             case '.':
             {
                 add(decimal_point_char);
+                decimal_point_position = token_buffer.size() - 1;
                 goto scan_number_decimal1;
             }

@@ -1085,6 +1086,7 @@ class lexer : public lexer_base<BasicJsonType>
             case '.':
             {
                 add(decimal_point_char);
+                decimal_point_position = token_buffer.size() - 1;
                 goto scan_number_decimal1;
             }

@@ -1322,6 +1324,7 @@ class lexer : public lexer_base<BasicJsonType>
     {
         token_buffer.clear();
         token_string.clear();
+        decimal_point_position = std::string::npos;
         token_string.push_back(char_traits<char_type>::to_char_type(current));
     }

@@ -1430,6 +1433,11 @@ class lexer : public lexer_base<BasicJsonType>
     /// return current string value (implicitly resets the token; useful only once)
     string_t& get_string()
     {
+        // translate decimal points from locale back to '.' (#4084)
+        if (decimal_point_char != '.' && decimal_point_position != std::string::npos)
+        {
+            token_buffer[decimal_point_position] = '.';
+        }
         return token_buffer;
     }

@@ -1627,6 +1635,8 @@ class lexer : public lexer_base<BasicJsonType>

     /// the decimal point
     const char_int_type decimal_point_char = '.';
+    /// the position of the decimal point in the input
+    std::size_t decimal_point_position = std::string::npos;
 };

 }  // namespace detail
diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp
index e7e8fe743d..18adac2210 100644
--- a/single_include/nlohmann/json.hpp
+++ b/single_include/nlohmann/json.hpp
@@ -8518,6 +8518,7 @@ class lexer : public lexer_base<BasicJsonType>
             case '.':
             {
                 add(decimal_point_char);
+                decimal_point_position = token_buffer.size() - 1;
                 goto scan_number_decimal1;
             }

@@ -8554,6 +8555,7 @@ class lexer : public lexer_base<BasicJsonType>
             case '.':
             {
                 add(decimal_point_char);
+                decimal_point_position = token_buffer.size() - 1;
                 goto scan_number_decimal1;
             }

@@ -8791,6 +8793,7 @@ class lexer : public lexer_base<BasicJsonType>
     {
         token_buffer.clear();
         token_string.clear();
+        decimal_point_position = std::string::npos;
         token_string.push_back(char_traits<char_type>::to_char_type(current));
     }

@@ -8899,6 +8902,11 @@ class lexer : public lexer_base<BasicJsonType>
     /// return current string value (implicitly resets the token; useful only once)
     string_t& get_string()
     {
+        // translate decimal points from locale back to '.' (#4084)
+        if (decimal_point_char != '.' && decimal_point_position != std::string::npos)
+        {
+            token_buffer[decimal_point_position] = '.';
+        }
         return token_buffer;
     }

@@ -9096,6 +9104,8 @@ class lexer : public lexer_base<BasicJsonType>

     /// the decimal point
     const char_int_type decimal_point_char = '.';
+    /// the position of the decimal point in the input
+    std::size_t decimal_point_position = std::string::npos;
 };

 }  // namespace detail

Track the position of the decimal point in numbers and restore it to '.' before returning the string representation.

Prompt:
The following is the text of a github issue and related comments for this repository:

SAX interface unexpectedly gets locale-altered float representation.
### Description

I didn't expect (neither from experience, nor from the documentation) that the float string representation would be altered according to the locale when feeded into the SAX parser.
I understand, why it has to be altered internally, but I'd expect the original string in the callback.

### Reproduction steps

`LC_NUMERIC=de_DE.UTF-8`

`{ "aFloat": 42.0 }`

```c++
bool ParserImpl::number_float( number_float_t val, const string_t &s )
{
    std::cout << s << '\n';
}
```

### Expected vs. actual results

Expected: 42.0
Actual: 42,0

### Minimal code example

```c++
#include <nlohmann/json.hpp>
#include <iostream>
#include <string_view>
#include <clocale>

using Json = nlohmann::json;

struct ParserImpl : public nlohmann::json_sax<Json>
{
    bool null() override { return true; };
    bool boolean( bool val ) override { return true; };
    bool number_integer( number_integer_t val ) override { return true; };
    bool number_unsigned( number_unsigned_t val ) override { return true; };
    bool number_float( number_float_t val, const string_t &s ) override {
        std::cout << s << '\n';
        return true;
    };
    bool string( string_t &val ) override { return true; };
    bool binary( binary_t &val ) override { return true; };
    bool start_object( std::size_t elements ) override { return true; };
    bool key( string_t &val ) override { return true; };
    bool end_object() override { return true; };
    bool start_array( std::size_t elements ) override { return true; };
    bool end_array() override { return true; };
    bool parse_error( std::size_t position, const std::string &last_token,
                      const nlohmann::detail::exception &ex ) override { return true; };
};

int main() {
    if(!setlocale(LC_NUMERIC, "de_DE.utf8"))
        return -1;
    std::string_view data { R"json({ "aFloat": 42.0 })json" };
    ParserImpl sax {};
    Json::sax_parse( data, &sax );
    return 0;
}

```
`g++-12 -o jsontest -I /path/to/nlohmann_json-src/single_include/ --std=c++20 jsontest.cpp`

### Error messages

_No response_

### Compiler and operating system

Linux/gcc-12, Windows/MSVC19.35

### Library version

3.11.2

Note that you need the "de_DE.utf8" locale in your system. Alternatively use any other locale with something else than a "." as decimal separator.

The error is that we replace . in the input by the decimal separator from the locale to be able to use std::strtod under the hood to make sure we properly parse floating-point numbers when a different locale is set. We forgot, however, that the string we pass to the SAX number_float function still contains the replaced separator.

To fix this, we need a way to efficiently replace this to . once we're done with std::strtod.

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/tests/src/unit-locale-cpp.cpp b/tests/src/unit-locale-cpp.cpp
new file mode 100644
index 0000000000..846b450c6a
--- /dev/null
+++ b/tests/src/unit-locale-cpp.cpp
@@ -0,0 +1,161 @@
+//     __ _____ _____ _____
+//  __|  |   __|     |   | |  JSON for Modern C++ (supporting code)
+// |  |  |__   |  |  | | | |  version 3.11.3
+// |_____|_____|_____|_|___|  https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann <https://nlohmann.me>
+// SPDX-License-Identifier: MIT
+
+#include "doctest_compatibility.h"
+
+#define JSON_TESTS_PRIVATE
+#include <nlohmann/json.hpp>
+using nlohmann::json;
+
+#include <clocale>
+
+struct ParserImpl final: public nlohmann::json_sax<json>
+{
+    bool null() override
+    {
+        return true;
+    }
+    bool boolean(bool /*val*/) override
+    {
+        return true;
+    }
+    bool number_integer(json::number_integer_t /*val*/) override
+    {
+        return true;
+    }
+    bool number_unsigned(json::number_unsigned_t /*val*/) override
+    {
+        return true;
+    }
+    bool number_float(json::number_float_t /*val*/, const json::string_t& s) override
+    {
+        float_string_copy = s;
+        return true;
+    }
+    bool string(json::string_t& /*val*/) override
+    {
+        return true;
+    }
+    bool binary(json::binary_t& /*val*/) override
+    {
+        return true;
+    }
+    bool start_object(std::size_t /*val*/) override
+    {
+        return true;
+    }
+    bool key(json::string_t& /*val*/) override
+    {
+        return true;
+    }
+    bool end_object() override
+    {
+        return true;
+    }
+    bool start_array(std::size_t /*val*/) override
+    {
+        return true;
+    }
+    bool end_array() override
+    {
+        return true;
+    }
+    bool parse_error(std::size_t /*val*/, const std::string& /*val*/, const nlohmann::detail::exception& /*val*/) override
+    {
+        return false;
+    }
+
+    ~ParserImpl() override;
+
+    ParserImpl()
+        : float_string_copy("not set")
+    {}
+
+    ParserImpl(const ParserImpl& other)
+        : float_string_copy(other.float_string_copy)
+    {}
+
+    ParserImpl(ParserImpl&& other) noexcept
+        : float_string_copy(std::move(other.float_string_copy))
+    {}
+
+    ParserImpl& operator=(const ParserImpl& other)
+    {
+        if (this != &other)
+        {
+            float_string_copy = other.float_string_copy;
+        }
+        return *this;
+    }
+
+    ParserImpl& operator=(ParserImpl&& other) noexcept
+    {
+        if (this != &other)
+        {
+            float_string_copy = std::move(other.float_string_copy);
+        }
+        return *this;
+    }
+
+    json::string_t float_string_copy;
+};
+
+ParserImpl::~ParserImpl() = default;
+
+TEST_CASE("locale-dependent test (LC_NUMERIC=C)")
+{
+    WARN_MESSAGE(std::setlocale(LC_NUMERIC, "C") != nullptr, "could not set locale");
+
+    SECTION("check if locale is properly set")
+    {
+        std::array<char, 6> buffer = {};
+        CHECK(std::snprintf(buffer.data(), buffer.size(), "%.2f", 12.34) == 5); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
+        CHECK(std::string(buffer.data()) == "12.34");
+    }
+
+    SECTION("parsing")
+    {
+        CHECK(json::parse("12.34").dump() == "12.34");
+    }
+
+    SECTION("SAX parsing")
+    {
+        ParserImpl sax {};
+        json::sax_parse( "12.34", &sax );
+        CHECK(sax.float_string_copy == "12.34");
+    }
+}
+
+TEST_CASE("locale-dependent test (LC_NUMERIC=de_DE)")
+{
+    if (std::setlocale(LC_NUMERIC, "de_DE") != nullptr)
+    {
+        SECTION("check if locale is properly set")
+        {
+            std::array<char, 6> buffer = {};
+            CHECK(std::snprintf(buffer.data(), buffer.size(), "%.2f", 12.34) == 5); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
+            CHECK(std::string(buffer.data()) == "12,34");
+        }
+
+        SECTION("parsing")
+        {
+            CHECK(json::parse("12.34").dump() == "12.34");
+        }
+
+        SECTION("SAX parsing")
+        {
+            ParserImpl sax{};
+            json::sax_parse("12.34", &sax);
+            CHECK(sax.float_string_copy == "12.34");
+        }
+    }
+    else
+    {
+        MESSAGE("locale de_DE is not usable");
+    }
+}


diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx
index 280662b6710c2c..aaaafc9d4bfbbb 100644
--- a/client/src/client-only-routes/show-certification.tsx
+++ b/client/src/client-only-routes/show-certification.tsx
@@ -35,8 +35,10 @@ import {
 import { PaymentContext } from '../../../shared/config/donation-settings';
 import ribbon from '../assets/images/ribbon.svg';
 import {
+  CertSlug,
   certTypes,
-  certTypeTitleMap
+  certTypeTitleMap,
+  linkedInCredentialIds
 } from '../../../shared/config/certification-settings';
 import MultiTierDonationForm from '../components/Donation/multi-tier-donation-form';
 import callGA from '../analytics/call-ga';
@@ -48,7 +50,7 @@ const localeCode = getLangCode(clientLocale);
 type Cert = {
   username: string;
   name: string;
-  certName: string;
+  certSlug: CertSlug;
   certTitle: string;
   completionTime: number;
   date: number;
@@ -218,6 +220,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
     name: userFullName = null,
     username,
     certTitle,
+    certSlug,
     completionTime
   } = cert;

@@ -286,6 +289,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
   );

   const urlFriendlyCertTitle = encodeURIComponent(certTitle);
+  const linkedInCredentialId = `${username}-${linkedInCredentialIds[certSlug]}`;

   const shareCertBtns = (
     <Row className='text-center'>
@@ -296,7 +300,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
           variant='primary'
           href={`https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=${urlFriendlyCertTitle}&organizationId=4831032&issueYear=${certYear}&issueMonth=${
             certMonth + 1
-          }&certUrl=${certURL}`}
+          }&certUrl=${certURL}&certId=${linkedInCredentialId}`}
           target='_blank'
           data-playwright-test-label='linkedin-share-btn'
         >
diff --git a/shared/config/certification-settings.ts b/shared/config/certification-settings.ts
index de01cc92b81670..f84a57ca033438 100644
--- a/shared/config/certification-settings.ts
+++ b/shared/config/certification-settings.ts
@@ -255,4 +255,30 @@ export const certTypeTitleMap = {
     'JavaScript Algorithms and Data Structures (Beta)'
 };

+export type CertSlug = (typeof Certification)[keyof typeof Certification];
+
+export const linkedInCredentialIds = {
+  [Certification.LegacyFrontEnd]: 'lfe',
+  [Certification.LegacyBackEnd]: 'lbe',
+  [Certification.LegacyDataVis]: 'ldv',
+  [Certification.LegacyInfoSecQa]: 'lisaqa',
+  [Certification.LegacyFullStack]: 'lfs',
+  [Certification.RespWebDesign]: 'rwd',
+  [Certification.FrontEndDevLibs]: 'fedl',
+  [Certification.JsAlgoDataStruct]: 'ljaads',
+  [Certification.DataVis]: 'dv',
+  [Certification.BackEndDevApis]: 'bedaa',
+  [Certification.QualityAssurance]: 'qa',
+  [Certification.InfoSec]: 'is',
+  [Certification.SciCompPy]: 'scwp',
+  [Certification.DataAnalysisPy]: 'dawp',
+  [Certification.MachineLearningPy]: 'mlwp',
+  [Certification.RelationalDb]: 'rd',
+  [Certification.CollegeAlgebraPy]: 'cawp',
+  [Certification.FoundationalCSharp]: 'fcswm',
+  [Certification.UpcomingPython]: 'up',
+  [Certification.JsAlgoDataStructNew]: 'jaads',
+  [Certification.A2English]: 'a2efd'
+};
+
 export const oldDataVizId = '561add10cb82ac38a17513b3';

Create a credential ID by combining the username and certification abbreviation from `linkedInCredentialIds`, and add it to the LinkedIn share URL parameter list.

Prompt:
The following is the text of a github issue and related comments for this repository:

Add LinkedIn Credential ID parameter to LinkedIn button
LinkedIn recently added a "Credential ID" field to their form. We want to automatically generate this and populate it, as it will add an additional line of information to the Linkedin certification and make it look more professional:

I figured out if we update the "Add this certification to my LinkedIn Profile" button's URL this way with a {parameter}, it will automatically populate the correct field:

```
https://www.linkedin.com/in/quincylarson/edit/forms/certification/new/?certUrl=https%3A%2F%2Ffreecodecamp.org%2Fcertification%2Fhappycamper101%2Ffront-end-development-libraries&isFromA2p=true&issueMonth=10&issueYear=2020&name=Front%20End%20Development%20Libraries&organizationId=4831032&certId={parameter}
```

Here I put "test" as the parameter. This will auto-populate this into the form when the camper arrives on LinkedIn:

For the certification parameter, rather than coming up with a new unique ID, we can just use the person's freeCodeCamp username + an abbreviation of the certification in question. For example, for the Front End Development Libraries we can use the ID "happycamper101-fedl"

This way the cert ID should always be unique, since each camper will only have one instance of each certification.

Use the following slugs for the certification ID.  Store them in a mapping called linkedInCredentialIds in certification-settings.ts.

LegacyFrontEnd: 'lfe'
LegacyBackEnd: 'lbe'
LegacyDataVis: 'ldv'
LegacyInfoSecQa: 'lisaqa'
LegacyFullStack: 'lfs'
RespWebDesign: 'rwd'
FrontEndDevLibs: 'fedl'
JsAlgoDataStruct: 'ljaads'
DataVis: 'dv'
BackEndDevApis: 'bedaa'
QualityAssurance: 'qa'
InfoSec: 'is'
SciCompPy: 'scwp'
DataAnalysisPy: 'dawp'
MachineLearningPy: 'mlwp'
RelationalDb: 'rd'
CollegeAlgebraPy: 'cawp'
FoundationalCSharp: 'fcswm'
UpcomingPython: 'up'
JsAlgoDataStructNew: 'jaads'
A2English: 'a2efd'

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/e2e/certification.spec.ts b/e2e/certification.spec.ts
index f82b516190c9fe..cb8a3bb1de77f4 100644
--- a/e2e/certification.spec.ts
+++ b/e2e/certification.spec.ts
@@ -55,7 +55,7 @@ test.describe('Certification page - Non Microsoft', () => {
     await expect(linkedinLink).toBeVisible();
     await expect(linkedinLink).toHaveAttribute(
       'href',
-      `https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=Responsive%20Web%20Design&organizationId=4831032&issueYear=2018&issueMonth=8&certUrl=https://freecodecamp.org/certification/certifieduser/responsive-web-design`
+      `https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=Responsive%20Web%20Design&organizationId=4831032&issueYear=2018&issueMonth=8&certUrl=https://freecodecamp.org/certification/certifieduser/responsive-web-design&certId=certifieduser-rwd`
     );

     const twitterLink = certLink.getByTestId('twitter-share-btn');
@@ -191,7 +191,7 @@ test.describe('Certification page - Microsoft', () => {
     await expect(linkedinLink).toBeVisible();
     await expect(linkedinLink).toHaveAttribute(
       'href',
-      'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=Foundational%20C%23%20with%20Microsoft&organizationId=4831032&issueYear=2023&issueMonth=9&certUrl=https://freecodecamp.org/certification/certifieduser/foundational-c-sharp-with-microsoft'
+      'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=Foundational%20C%23%20with%20Microsoft&organizationId=4831032&issueYear=2023&issueMonth=9&certUrl=https://freecodecamp.org/certification/certifieduser/foundational-c-sharp-with-microsoft&certId=certifieduser-fcswm'
     );

     const twitterLink = certLink.getByTestId('twitter-share-btn');
diff --git a/shared/config/certification-settings.test.ts b/shared/config/certification-settings.test.ts
new file mode 100644
index 00000000000000..312eb6b24f529d
--- /dev/null
+++ b/shared/config/certification-settings.test.ts
@@ -0,0 +1,10 @@
+import { Certification, linkedInCredentialIds } from './certification-settings';
+
+describe('linkedInCredentialIds', () => {
+  it('should contain a value for all certifications', () => {
+    const allCertifications = Object.values(Certification).sort();
+    const linkedInCredentialIdsKeys = Object.keys(linkedInCredentialIds).sort();
+
+    expect(linkedInCredentialIdsKeys).toEqual(allCertifications);
+  });
+});


diff --git a/client/src/pages/update-email.css b/client/src/client-only-routes/show-update-email.css
similarity index 100%
rename from client/src/pages/update-email.css
rename to client/src/client-only-routes/show-update-email.css
diff --git a/client/src/client-only-routes/show-update-email.tsx b/client/src/client-only-routes/show-update-email.tsx
new file mode 100644
index 00000000000000..e79f5f300b7fb7
--- /dev/null
+++ b/client/src/client-only-routes/show-update-email.tsx
@@ -0,0 +1,140 @@
+import { Link } from 'gatsby';
+import React, {
+  useState,
+  useEffect,
+  type FormEvent,
+  type ChangeEvent
+} from 'react';
+import Helmet from 'react-helmet';
+import { useTranslation } from 'react-i18next';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import isEmail from 'validator/lib/isEmail';
+import {
+  Container,
+  FormGroup,
+  FormControl,
+  ControlLabel,
+  Col,
+  Row,
+  Button
+} from '@freecodecamp/ui';
+
+import envData from '../../config/env.json';
+import { Spacer, Loader } from '../components/helpers';
+import './show-update-email.css';
+import { isSignedInSelector, userSelector } from '../redux/selectors';
+import { hardGoTo as navigate } from '../redux/actions';
+import { updateMyEmail } from '../redux/settings/actions';
+import { maybeEmailRE } from '../utils';
+
+const { apiLocation } = envData;
+
+interface ShowUpdateEmailProps {
+  isNewEmail: boolean;
+  updateMyEmail: (e: string) => void;
+  path?: string;
+  isSignedIn: boolean;
+  navigate: (location: string) => void;
+}
+
+const mapStateToProps = createSelector(
+  userSelector,
+  isSignedInSelector,
+  (
+    { email, emailVerified }: { email: string; emailVerified: boolean },
+    isSignedIn
+  ) => ({
+    isNewEmail: !email || emailVerified,
+    isSignedIn
+  })
+);
+
+const mapDispatchToProps = { updateMyEmail, navigate };
+
+function ShowUpdateEmail({
+  isNewEmail,
+  updateMyEmail,
+  isSignedIn,
+  navigate
+}: ShowUpdateEmailProps) {
+  const { t } = useTranslation();
+  const [emailValue, setEmailValue] = useState('');
+
+  useEffect(() => {
+    if (!isSignedIn) navigate(`${apiLocation}/signin`);
+  }, [isSignedIn, navigate]);
+  if (!isSignedIn) return <Loader fullScreen={true} />;
+
+  function handleSubmit(event: FormEvent) {
+    event.preventDefault();
+    updateMyEmail(emailValue);
+  }
+
+  function onChange(event: ChangeEvent<HTMLInputElement>) {
+    setEmailValue(event.target.value);
+    return null;
+  }
+
+  const emailValidationState = maybeEmailRE.test(emailValue)
+    ? isEmail(emailValue)
+      ? 'success'
+      : 'error'
+    : null;
+
+  return (
+    <>
+      <Helmet>
+        <title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
+      </Helmet>
+      <Container>
+        <Spacer size='medium' />
+
{t('misc.update-email-2')}

+        <Row>
+          <Col sm={6} smOffset={3}>
+            <Row>
+              <form
+                onSubmit={handleSubmit}
+                data-playwright-test-label='update-email-form'
+              >
+                <FormGroup
+                  className='update-email-field'
+                  validationState={emailValidationState}
+                >
+                  <ControlLabel htmlFor='emailInput'>
+                    {t('misc.email')}
+                  </ControlLabel>
+                  <FormControl
+                    id='emailInput'
+                    onChange={onChange}
+                    placeholder='camperbot@example.com'
+                    required={true}
+                    type='email'
+                  />
+                </FormGroup>
+                <Button
+                  block={true}
+                  size='large'
+                  variant='primary'
+                  disabled={emailValidationState !== 'success'}
+                  type='submit'
+                >
+                  {isNewEmail
+                    ? t('buttons.update-email')
+                    : t('buttons.verify-email')}
+                </Button>
+              </form>
+

+                <Link to='/signout'>{t('buttons.sign-out')}</Link>
+

+            </Row>
+          </Col>
+        </Row>
+      </Container>
+    </>
+  );
+}
+
+ShowUpdateEmail.displayName = 'ShowUpdateEmail';
+
+export default connect(mapStateToProps, mapDispatchToProps)(ShowUpdateEmail);
diff --git a/client/src/pages/update-email.tsx b/client/src/pages/update-email.tsx
index 1d9745d4f5cc0f..5ed95699ba1820 100644
--- a/client/src/pages/update-email.tsx
+++ b/client/src/pages/update-email.tsx
@@ -1,131 +1,20 @@
-import { Link } from 'gatsby';
-import { isString } from 'lodash-es';
-import React, { useState, type FormEvent, type ChangeEvent } from 'react';
-import Helmet from 'react-helmet';
-import type { TFunction } from '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 isEmail from 'validator/lib/isEmail';
-import {
-  Container,
-  FormGroup,
-  FormControl,
-  ControlLabel,
-  Col,
-  Row,
-  Button
-} from '@freecodecamp/ui';
+import { Router } from '@reach/router';
+import { withPrefix } from 'gatsby';
+import React from 'react';

-import { Spacer } from '../components/helpers';
-import './update-email.css';
-import { userSelector } from '../redux/selectors';
-import { updateMyEmail } from '../redux/settings/actions';
-import { maybeEmailRE } from '../utils';
-
-interface UpdateEmailProps {
-  isNewEmail: boolean;
-  t: TFunction;
-  updateMyEmail: (e: string) => void;
-}
-
-const mapStateToProps = createSelector(
-  userSelector,
-  ({ email, emailVerified }: { email: string; emailVerified: boolean }) => ({
-    isNewEmail: !email || emailVerified
-  })
-);
-
-const mapDispatchToProps = (dispatch: Dispatch) =>
-  bindActionCreators({ updateMyEmail }, dispatch);
-
-function UpdateEmail({ isNewEmail, t, updateMyEmail }: UpdateEmailProps) {
-  const [emailValue, setEmailValue] = useState('');
-
-  function handleSubmit(event: FormEvent) {
-    event.preventDefault();
-    updateMyEmail(emailValue);
-  }
-
-  function onChange(event: ChangeEvent<HTMLInputElement>) {
-    const newEmailValue = event.target.value;
-    if (!isString(newEmailValue)) {
-      return null;
-    }
-    setEmailValue(newEmailValue);
-    return null;
-  }
-
-  function getEmailValidationState() {
-    if (maybeEmailRE.test(emailValue)) {
-      return isEmail(emailValue) ? 'success' : 'error';
-    }
-    return null;
-  }
+import ShowUpdateEmail from '../client-only-routes/show-update-email';
+import RedirectHome from '../components/redirect-home';

+function UpdateEmail(): JSX.Element {
   return (
-    <>
-      <Helmet>
-        <title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
-      </Helmet>
-      <Container>
-        <Spacer size='medium' />
-

-          {t('misc.update-email-2')}
-

-        <Row>
-          <Col sm={6} smOffset={3}>
-            <Row>
-              <form
-                onSubmit={handleSubmit}
-                data-playwright-test-label='update-email-form'
-              >
-                <FormGroup
-                  className='update-email-field'
-                  validationState={getEmailValidationState()}
-                >
-                  <ControlLabel htmlFor='emailInput'>
-                    {t('misc.email')}
-                  </ControlLabel>
-                  <FormControl
-                    id='emailInput'
-                    onChange={onChange}
-                    placeholder='camperbot@example.com'
-                    required={true}
-                    type='email'
-                  />
-                </FormGroup>
-                <Button
-                  block={true}
-                  size='large'
-                  variant='primary'
-                  disabled={getEmailValidationState() !== 'success'}
-                  type='submit'
-                >
-                  {isNewEmail
-                    ? t('buttons.update-email')
-                    : t('buttons.verify-email')}
-                </Button>
-              </form>
-

-                <Link to='/signout'>{t('buttons.sign-out')}</Link>
-

-            </Row>
-          </Col>
-        </Row>
-      </Container>
-    </>
+    <Router>
+      <ShowUpdateEmail path={withPrefix('/update-email')} />
+
+      <RedirectHome default />
+    </Router>
   );
 }

-UpdateEmail.displayName = 'Update-Email';
+UpdateEmail.displayName = 'UpdateEmail';

-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(withTranslation()(UpdateEmail));
+export default UpdateEmail;
diff --git a/client/src/pages/update-stripe-card.tsx b/client/src/pages/update-stripe-card.tsx
index 5b6f0ed93f8def..8113791507483c 100644
--- a/client/src/pages/update-stripe-card.tsx
+++ b/client/src/pages/update-stripe-card.tsx
@@ -9,7 +9,6 @@ import { Container, Row, Col, Button } from '@freecodecamp/ui';
 import BigCallToAction from '../components/landing/components/big-call-to-action';

 import { Spacer } from '../components/helpers';
-import './update-email.css';
 import {
   isSignedInSelector,
   isDonatingSelector,

Remove the existing UpdateEmail implementation and instead implement the component using a Router to ShowUpdateEmail or RedirectHome. In addition, update display name to "UpdateEmail".

Prompt:
The following is the text of a github issue and related comments for this repository:

Update email page should be gated behind authentication
### Describe the Issue

The update email page is currently accessible even if the user is signed out.

### Affected Page

https://www.freecodecamp.org/update-email

### Steps to Reproduce

1. Sign out of fCC
2. Go to https://www.freecodecamp.org/update-email/

### Expected behavior

- The page should be gated behind authentication. The logic would look similar to this:
  https://github.com/freeCodeCamp/freeCodeCamp/blob/fc9d2687d54d898c6966d9fca274e6f5f05e7fd2/client/src/client-only-routes/show-settings.tsx#L153-L156

I tried following this approach:

freeCodeCamp/client/src/client-only-routes/show-settings.tsx

Lines 153 to 156 in fc9d268

 if (!isSignedInRef.current) {
   navigate(`${apiLocation}/signin`);
   return <Loader fullScreen={true} />;
 }
But it didn't work. The page would navigate the user to http://localhost:8000/update-email/http://localhost:3000/signin instead.

The difference between UpdateEmail and ShowSettings is that ShowSettings is placed under a Router while UpdateEmail isn't:

freeCodeCamp/client/src/pages/settings.tsx

Lines 8 to 16 in fc9d268

 function Settings(): JSX.Element {
   return (
     <Router>
       <ShowSettings path={withPrefix('/settings')} />

       <RedirectHome default />
     </Router>
   );
 }
I'm not sure if that's what affects the redirect behavior, and I'm also not sure if using createRedirect here is okay / recommended.

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/client/src/pages/update-email.tsx b/client/src/pages/update-email.tsx
index 1d9745d4f5cc0f..5ed95699ba1820 100644
--- a/client/src/pages/update-email.tsx
+++ b/client/src/pages/update-email.tsx
@@ -1,131 +1,20 @@
-import { Link } from 'gatsby';
-import { isString } from 'lodash-es';
-import React, { useState, type FormEvent, type ChangeEvent } from 'react';
-import Helmet from 'react-helmet';
-import type { TFunction } from '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 isEmail from 'validator/lib/isEmail';
-import {
-  Container,
-  FormGroup,
-  FormControl,
-  ControlLabel,
-  Col,
-  Row,
-  Button
-} from '@freecodecamp/ui';
+import { Router } from '@reach/router';
+import { withPrefix } from 'gatsby';
+import React from 'react';

-import { Spacer } from '../components/helpers';
-import './update-email.css';
-import { userSelector } from '../redux/selectors';
-import { updateMyEmail } from '../redux/settings/actions';
-import { maybeEmailRE } from '../utils';
-
-interface UpdateEmailProps {
-  isNewEmail: boolean;
-  t: TFunction;
-  updateMyEmail: (e: string) => void;
-}
-
-const mapStateToProps = createSelector(
-  userSelector,
-  ({ email, emailVerified }: { email: string; emailVerified: boolean }) => ({
-    isNewEmail: !email || emailVerified
-  })
-);
-
-const mapDispatchToProps = (dispatch: Dispatch) =>
-  bindActionCreators({ updateMyEmail }, dispatch);
-
-function UpdateEmail({ isNewEmail, t, updateMyEmail }: UpdateEmailProps) {
-  const [emailValue, setEmailValue] = useState('');
-
-  function handleSubmit(event: FormEvent) {
-    event.preventDefault();
-    updateMyEmail(emailValue);
-  }
-
-  function onChange(event: ChangeEvent<HTMLInputElement>) {
-    const newEmailValue = event.target.value;
-    if (!isString(newEmailValue)) {
-      return null;
-    }
-    setEmailValue(newEmailValue);
-    return null;
-  }
-
-  function getEmailValidationState() {
-    if (maybeEmailRE.test(emailValue)) {
-      return isEmail(emailValue) ? 'success' : 'error';
-    }
-    return null;
-  }
+import ShowUpdateEmail from '../client-only-routes/show-update-email';
+import RedirectHome from '../components/redirect-home';

+function UpdateEmail(): JSX.Element {
   return (
-    <>
-      <Helmet>
-        <title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
-      </Helmet>
-      <Container>
-        <Spacer size='medium' />
-        <h2
-          className='text-center'
-          data-playwright-test-label='update-email-heading'
-        >
-          {t('misc.update-email-2')}
-        </h2>
-        <Row>
-          <Col sm={6} smOffset={3}>
-            <Row>
-              <form
-                onSubmit={handleSubmit}
-                data-playwright-test-label='update-email-form'
-              >
-                <FormGroup
-                  className='update-email-field'
-                  validationState={getEmailValidationState()}
-                >
-                  <ControlLabel htmlFor='emailInput'>
-                    {t('misc.email')}
-                  </ControlLabel>
-                  <FormControl
-                    id='emailInput'
-                    onChange={onChange}
-                    placeholder='camperbot@example.com'
-                    required={true}
-                    type='email'
-                  />
-                </FormGroup>
-                <Button
-                  block={true}
-                  size='large'
-                  variant='primary'
-                  disabled={getEmailValidationState() !== 'success'}
-                  type='submit'
-                >
-                  {isNewEmail
-                    ? t('buttons.update-email')
-                    : t('buttons.verify-email')}
-                </Button>
-              </form>
-              <p className='text-center'>
-                <Link to='/signout'>{t('buttons.sign-out')}</Link>
-              </p>
-            </Row>
-          </Col>
-        </Row>
-      </Container>
-    </>
+    <Router>
+      <ShowUpdateEmail path={withPrefix('/update-email')} />
+
+      <RedirectHome default />
+    </Router>
   );
 }

-UpdateEmail.displayName = 'Update-Email';
+UpdateEmail.displayName = 'UpdateEmail';

-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(withTranslation()(UpdateEmail));
+export default UpdateEmail;

diff --git a/e2e/update-email.spec.ts b/e2e/update-email.spec.ts
index 9bf96a61de516d..0ed02e8f9d64b8 100644
--- a/e2e/update-email.spec.ts
+++ b/e2e/update-email.spec.ts
@@ -1,33 +1,24 @@
-import { test, expect, type Page } from '@playwright/test';
+import { test, expect } from '@playwright/test';
 import translations from '../client/i18n/locales/english/translations.json';

-let page: Page;
+test.describe('The update-email page when the user is signed in', () => {
+  test.use({ storageState: 'playwright/.auth/certified-user.json' });

-test.beforeAll(async ({ browser }) => {
-  page = await browser.newPage();
-  await page.goto('/update-email');
-});
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/update-email');
+  });

-test.describe('The update-email page', () => {
-  test('The page renders with correct title', async () => {
+  test('should display the content correctly', async ({ page }) => {
     await expect(page).toHaveTitle(
       'Update your email address | freeCodeCamp.org'
     );
-  });
+    await expect(
+      page.getByRole('heading', { name: 'Update your email address here' })
+    ).toBeVisible();

-  test('The page has correct header', async () => {
-    const header = page.getByTestId('update-email-heading');
-
-    await expect(header).toBeVisible();
-    await expect(header).toContainText(translations.misc['update-email-2']);
-  });
-
-  test('The page has update email form', async () => {
     const form = page.getByTestId('update-email-form');
-    const emailInput = page.getByLabel(translations.misc.email);
-    const submitButton = page.getByRole('button', {
-      name: translations.buttons['update-email']
-    });
+    const emailInput = page.getByLabel('Email');
+    const submitButton = page.getByRole('button', { name: 'Update my Email' });

     await expect(form).toBeVisible();
     await expect(emailInput).toBeVisible();
@@ -38,22 +29,19 @@ test.describe('The update-email page', () => {
     );
     await expect(submitButton).toBeVisible();
     await expect(submitButton).toHaveAttribute('type', 'submit');
-  });

-  test('The page has sign out button', async () => {
-    const signOutButton = page.getByRole('link', {
-      name: translations.buttons['sign-out']
-    });
+    const signOutButton = page.getByRole('link', { name: 'Sign out' });

     await expect(signOutButton).toBeVisible();
     await expect(signOutButton).toHaveAttribute('href', '/signout');
   });

-  test('should enable the submit button if the email input is valid', async () => {
+  test('should enable the submit button if the email input is valid', async ({
+    page
+  }) => {
     const emailInput = page.getByLabel(translations.misc.email);
-    const submitButton = page.getByRole('button', {
-      name: translations.buttons['update-email']
-    });
+    const submitButton = page.getByRole('button', { name: 'Update my Email' });
+
     await expect(submitButton).toBeDisabled();
     await emailInput.fill('123');
     await expect(submitButton).toBeDisabled();
@@ -61,3 +49,27 @@ test.describe('The update-email page', () => {
     await expect(submitButton).toBeEnabled();
   });
 });
+
+test.describe('The update-email page when the user is not signed in', () => {
+  test.use({ storageState: { cookies: [], origins: [] } });
+
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/update-email');
+  });
+
+  test('should sign the user in and redirect them to /learn', async ({
+    page,
+    browserName
+  }) => {
+    // The signin step involves multiple navigations, which results a network error in Firefox.
+    // The error is harmless but Playwright doesn't suppress it, causing the test to fail.
+    // Ref: https://github.com/microsoft/playwright/issues/20749
+    test.skip(browserName === 'firefox');
+
+    await page.waitForURL(/\/learn\/?/);
+
+    await expect(
+      page.getByRole('heading', { name: 'Welcome back, Full Stack User' })
+    ).toBeVisible();
+  });
+});


diff --git a/api/src/routes/donate.ts b/api/src/routes/donate.ts
index e7ba329754be10..82b82cde2d4b0d 100644
--- a/api/src/routes/donate.ts
+++ b/api/src/routes/donate.ts
@@ -102,6 +102,17 @@ export const donateRoutes: FastifyPluginCallbackTypebox = (
         });

         const { email, name } = user;
+        const threeChallengesCompleted = user.completedChallenges.length >= 3;
+
+        if (!threeChallengesCompleted) {
+          void reply.code(400);
+          return {
+            error: {
+              type: 'MethodRestrictionError',
+              message: `Donate using another method`
+            }
+          } as const;
+        }

         if (user.isDonating) {
           void reply.code(400);
diff --git a/api/src/schemas/donate/charge-stripe-card.ts b/api/src/schemas/donate/charge-stripe-card.ts
index 50b21148bba316..ec820456eba85c 100644
--- a/api/src/schemas/donate/charge-stripe-card.ts
+++ b/api/src/schemas/donate/charge-stripe-card.ts
@@ -14,7 +14,10 @@ export const chargeStripeCard = {
     400: Type.Object({
       error: Type.Object({
         message: Type.String(),
-        type: Type.Literal('AlreadyDonatingError')
+        type: Type.Union([
+          Type.Literal('AlreadyDonatingError'),
+          Type.Literal('MethodRestrictionError')
+        ])
       })
     }),
     402: Type.Object({

If the user has not completed at least 3 challenges, return a 400 status code with a MethodRestrictionError and the message "Donate using another method".

Prompt:
The following is the text of a github issue and related comments for this repository:

POST /donate/charge-stripe-card

Update charge stripe card endpoint with the recent changes in api-server/src/server/utils/donation.js, which blocks donations if the user has not completed three challenges.

The error should be:
type: 'MethodRestrictionError',
message: `Donate using another method`

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/api/src/routes/donate.test.ts b/api/src/routes/donate.test.ts
index ff1a35f15d4c4b..78b87824873f35 100644
--- a/api/src/routes/donate.test.ts
+++ b/api/src/routes/donate.test.ts
@@ -1,9 +1,12 @@
+import type { Prisma } from '@prisma/client';
 import {
   createSuperRequest,
   devLogin,
   setupServer,
-  superRequest
+  superRequest,
+  defaultUserEmail
 } from '../../jest.utils';
+import { createUserInput } from '../utils/create-user';

 const chargeStripeCardReqBody = {
   paymentMethodId: 'UID',
@@ -44,6 +47,39 @@ jest.mock('stripe', () => {
   });
 });

+const userWithoutProgress: Prisma.userCreateInput =
+  createUserInput(defaultUserEmail);
+
+const userWithProgress: Prisma.userCreateInput = {
+  ...createUserInput(defaultUserEmail),
+  completedChallenges: [
+    {
+      id: 'a6b0bb188d873cb2c8729495',
+      completedDate: 1520002973119,
+      solution: null,
+      challengeType: 5
+    },
+    {
+      id: '33b0bb188d873cb2c8729433',
+      completedDate: 4420002973122,
+      solution: null,
+      challengeType: 5
+    },
+    {
+      id: 'a5229172f011153519423690',
+      completedDate: 1520440323273,
+      solution: null,
+      challengeType: 5
+    },
+    {
+      id: 'a5229172f011153519423692',
+      completedDate: 1520440323274,
+      githubLink: '',
+      challengeType: 5
+    }
+  ]
+};
+
 describe('Donate', () => {
   setupServer();

@@ -53,6 +89,10 @@ describe('Donate', () => {
     beforeEach(async () => {
       const setCookies = await devLogin();
       superPost = createSuperRequest({ method: 'POST', setCookies });
+      await fastifyTestInstance.prisma.user.updateMany({
+        where: { email: userWithProgress.email },
+        data: userWithProgress
+      });
     });

     describe('POST /donate/charge-stripe-card', () => {
@@ -133,6 +173,23 @@ describe('Donate', () => {
           error: 'Donation failed due to a server error.'
         });
       });
+
+      it('should return 400 if user has not completed challenges', async () => {
+        await fastifyTestInstance.prisma.user.updateMany({
+          where: { email: userWithProgress.email },
+          data: userWithoutProgress
+        });
+        const failResponse = await superPost('/donate/charge-stripe-card').send(
+          chargeStripeCardReqBody
+        );
+        expect(failResponse.body).toEqual({
+          error: {
+            type: 'MethodRestrictionError',
+            message: `Donate using another method`
+          }
+        });
+        expect(failResponse.status).toBe(400);
+      });
     });

     describe('POST /donate/add-donation', () => {


diff --git a/client/src/templates/Challenges/components/completion-modal.tsx b/client/src/templates/Challenges/components/completion-modal.tsx
index ecdb17835e3925..f3c2f76ba159ba 100644
--- a/client/src/templates/Challenges/components/completion-modal.tsx
+++ b/client/src/templates/Challenges/components/completion-modal.tsx
@@ -164,6 +164,8 @@ class CompletionModal extends Component<
    const isMacOS =

@@ -199,7 +201,9 @@ class CompletionModal extends Component<
            <span className='hidden-xs'>

diff --git a/client/src/templates/Challenges/components/completion-modal.tsx b/client/src/templates/Challenges/components/completion-modal.tsx
index ecdb17835e3925..f3c2f76ba159ba 100644
--- a/client/src/templates/Challenges/components/completion-modal.tsx
+++ b/client/src/templates/Challenges/components/completion-modal.tsx
@@ -164,6 +164,8 @@ class CompletionModal extends Component<
       submitChallenge
     } = this.props;

+    const isMacOS = navigator.userAgent.includes('Mac OS');
+
     return (
       <Modal
         onClose={close}
@@ -199,7 +201,9 @@ class CompletionModal extends Component<
             onClick={() => submitChallenge()}
           >
             {isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')}
-             (Ctrl + Enter)
+
+              {isMacOS ? ' (Command + Enter)' : ' (Ctrl + Enter)'}
+
           </Button>
           <Spacer size='xxSmall' />
           {this.state.downloadURL ? (

Show " (Command + Enter)" instead of " (Ctrl + Enter)" in the button text when the user is on MacOS.

Prompt:
The following is the text of a github issue and related comments for this repository:

For MacOS, the submit button of the completion modal should display Command + Enter
## Description

The submit button on the completion modal always displays Ctrl + Enter, but it should be Command + Enter if the user is on MacOS.

https://github.com/freeCodeCamp/freeCodeCamp/blob/c7d3b1303e6badfbdabc16de8c95e9f334b2a4b5/client/src/templates/Challenges/components/completion-modal.tsx#L202

The modal can be found on one of these pages:
- https://www.freecodecamp.org/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/task-3
- https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/basic-javascript/declare-javascript-variables

## Requirements

- Update the `completion-modal.tsx` file to have the key name displayed based on user device
- Update tests in `completion-modal-spec.ts` file to cover the button text display

https://github.com/freeCodeCamp/freeCodeCamp/pull/54276 can be used as reference.

## Additional context

We recently made a similar change to the submit button in the lower jaw: https://github.com/freeCodeCamp/freeCodeCamp/issues/54270.

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/e2e/completion-modal.spec.ts b/e2e/completion-modal.spec.ts
index c85db1b4520cb8..05b242faef48cf 100644
--- a/e2e/completion-modal.spec.ts
+++ b/e2e/completion-modal.spec.ts
@@ -33,6 +33,33 @@ test.describe('Challenge Completion Modal Tests (Signed Out)', () => {
     await expect(page.getByTestId('completion-success-icon')).not.toBeVisible();
   });

+  test('should display the text of go to next challenge button accordingly based on device type', async ({
+    page,
+    isMobile,
+    browserName
+  }) => {
+    if (isMobile) {
+      await expect(
+        page.getByRole('button', {
+          name: 'Go to next challenge',
+          exact: true
+        })
+      ).toBeVisible();
+    } else if (browserName === 'webkit') {
+      await expect(
+        page.getByRole('button', {
+          name: 'Go to next challenge (Command + Enter)'
+        })
+      ).toBeVisible();
+    } else {
+      await expect(
+        page.getByRole('button', {
+          name: 'Go to next challenge (Ctrl + Enter)'
+        })
+      ).toBeVisible();
+    }
+  });
+
   test('should redirect to /learn after sign in', async ({ page }) => {
     await page
       .getByRole('link', { name: translations.learn['sign-in-save'] })
@@ -69,6 +96,33 @@ test.describe('Challenge Completion Modal Tests (Signed In)', () => {
     await expect(page.getByTestId('completion-success-icon')).not.toBeVisible();
   });

+  test('should display the text of go to next challenge button accordingly based on device type', async ({
+    page,
+    isMobile,
+    browserName
+  }) => {
+    if (isMobile) {
+      await expect(
+        page.getByRole('button', {
+          name: 'Submit and go to next challenge',
+          exact: true
+        })
+      ).toBeVisible();
+    } else if (browserName === 'webkit') {
+      await expect(
+        page.getByRole('button', {
+          name: 'Submit and go to next challenge (Command + Enter)'
+        })
+      ).toBeVisible();
+    } else {
+      await expect(
+        page.getByRole('button', {
+          name: 'Submit and go to next challenge (Ctrl + Enter)'
+        })
+      ).toBeVisible();
+    }
+  });
+
   test('should submit and go to the next challenge when the user clicks the submit button', async ({
     page
   }) => {


diff --git a/client/src/utils/solution-display-type.ts b/client/src/utils/solution-display-type.ts
index c15c123e56c753..e84839b0bbdf42 100644
--- a/client/src/utils/solution-display-type.ts
+++ b/client/src/utils/solution-display-type.ts
@@ -19,8 +19,7 @@ export const getSolutionDisplayType = ({
    return

diff --git a/client/src/utils/solution-display-type.ts b/client/src/utils/solution-display-type.ts
index c15c123e56c753..e84839b0bbdf42 100644
--- a/client/src/utils/solution-display-type.ts
+++ b/client/src/utils/solution-display-type.ts
@@ -19,8 +19,7 @@ export const getSolutionDisplayType = ({
 }: CompletedChallenge): DisplayType => {
   if (examResults) return 'showExamResults';
   if (challengeFiles?.length)
-    return challengeType === challengeTypes.multifileCertProject ||
-      challengeType === challengeTypes.multifilePythonCertProject
+    return challengeType === challengeTypes.multifileCertProject
       ? 'showMultifileProjectSolution'
       : 'showUserCode';
   if (!solution) return 'none';

Do not show multifile project solutions for Python certification projects.

Prompt:
The following is the text of a github issue and related comments for this repository:

Viewing multifile Python cert project solutions show blank page
### Describe the Issue

After submitting one of the new multifile Python cert projects, when you try to view the **project** on your settings page - it shows a blank page

### Affected Page

https://www.freecodecamp.org/settings

### Steps to Reproduce

(links are to .dev)
1. Go to a [new python cert project](https://www.freecodecamp.dev/learn/scientific-computing-with-python/scientific-computing-with-python-projects/arithmetic-formatter)
2. Submit a solution
3. Go to [your settings page](https://www.freecodecamp.dev/settings)
4. Scroll down to the project you just submitted
5. Click `View` -> `View Project`
6. See blank page

### Expected behavior

Remove the `View Project` button for those projects.

### Screenshots

![Feb-29-2024 10-13-04](https://github.com/freeCodeCamp/freeCodeCamp/assets/20648924/4308f714-91f8-492c-95cf-0a0b5a252fde)

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/client/src/utils/solution-display-type.test.ts b/client/src/utils/solution-display-type.test.ts
index aeb541fb1676c7..9eed5971a17223 100644
--- a/client/src/utils/solution-display-type.test.ts
+++ b/client/src/utils/solution-display-type.test.ts
@@ -23,7 +23,7 @@ describe('getSolutionDisplayType', () => {
       'showMultifileProjectSolution'
     );
     expect(getSolutionDisplayType(multifilePythonSolution)).toBe(
-      'showMultifileProjectSolution'
+      'showUserCode'
     );
   });
   it('should handle solutions with a single valid url', () => {


diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 760b5862ae73cb..86670b1af5fcd8 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -732,6 +732,7 @@
     "last-page": "Go to last page",
     "primary-nav": "primary",
     "breadcrumb-nav": "breadcrumb",
+    "timeline-pagination-nav": "Timeline Pagination",
     "submit": "Use Ctrl + Enter to submit.",
     "running-tests": "Running tests",
     "hide-preview": "Hide the preview",
diff --git a/client/src/components/profile/components/timeline-pagination.tsx b/client/src/components/profile/components/timeline-pagination.tsx
index 41a94bde84e4b7..737483d34f5603 100644
--- a/client/src/components/profile/components/timeline-pagination.tsx
+++ b/client/src/components/profile/components/timeline-pagination.tsx
@@ -16,7 +16,7 @@ const TimelinePagination = (props: TimelinePaginationProps): JSX.Element => {
   const { t } = useTranslation();

   return (
-    <nav aria-label='Timeline Pagination' role='navigation'>
+    <nav aria-label={t('aria.timeline-pagination-nav')}>
       <ul aria-hidden='true' className='timeline-pagination_list'>
         {totalPages > 10 && (
           <li

Replace the hard-coded aria-label for the nav element with the translation key and remove the redundant role attribute.

Prompt:
The following is the text of a github issue and related comments for this repository:

Use translation string in timeline pagination
## Description

The `aria-label` is being hard-coded with an English string, we should change this to use a translation string instead. Additionally, the `role='navigation'` attribute is redundant as `nav` already has `navigation` role implicitly.

https://github.com/freeCodeCamp/freeCodeCamp/blob/4dcef371953ef31a2127c4745ca33403cad06a0d/client/src/components/profile/components/timeline-pagination.tsx#L19

## Changes needed

- Replace the `'Timeline Pagination'` string with a translation string, something like `t('aria.timeline-pagination-nav')` (the string also needs to be added to `client/i18n/locales/english/translations.json`)
- Remove `role='navigation'`

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/client/src/components/profile/components/__snapshots__/timeline-buttons.test.js.snap b/client/src/components/profile/components/__snapshots__/timeline-buttons.test.js.snap
index 9ed4b0075264e0..fc10d652fcc61e 100644
--- a/client/src/components/profile/components/__snapshots__/timeline-buttons.test.js.snap
+++ b/client/src/components/profile/components/__snapshots__/timeline-buttons.test.js.snap
@@ -370,8 +370,7 @@ exports[`should check certification page consistency 1`] = `
       </tbody>
     </table>
     <nav
-      aria-label="Timeline Pagination"
-      role="navigation"
+      aria-label="aria.timeline-pagination-nav"
     >
       <ul
         aria-hidden="true"


diff --git a/client/src/redux/settings/actions.js b/client/src/redux/settings/actions.js
index bb451ca2d96fbd..466e74dfa22ae5 100644
--- a/client/src/redux/settings/actions.js
+++ b/client/src/redux/settings/actions.js
@@ -105,5 +105,13 @@ export const verifyCertError = createAction(types.verifyCertError);
export const resetMyEditorLayout =
export const resetMyEditorLayoutComplete =
export const resetMyEditorLayoutError =

diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index f086b5ea6508e3..e3872e81b6b6b0 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -238,6 +238,8 @@
     "sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.",
     "sound-volume": "Campfire Volume:",
     "scrollbar-width": "Editor Scrollbar Width",
+    "reset-editor-layout-tooltip": "Reset the editor layout to its default state",
+    "reset-editor-layout": "Reset Editor Layout",
     "shortcuts-explained": "Within a challenge, press ESC followed by the question mark to show a list of available shortcuts.",
     "username": {
       "contains invalid characters": "Username \"{{username}}\" contains invalid characters",
@@ -891,6 +893,7 @@
     "invalid-update-flag": "You are attempting to access forbidden resources. Please request assistance on https://forum.freecodecamp.org if this is a valid request.",
     "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.",
     "ms": {
       "transcript": {
         "link-err-1": "Please include a Microsoft transcript URL in the request.",
diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx
index 5c7177c346e16c..506e7d934dd6f7 100644
--- a/client/src/client-only-routes/show-settings.tsx
+++ b/client/src/client-only-routes/show-settings.tsx
@@ -35,7 +35,8 @@ import {
   updateMySound,
   updateMyTheme,
   updateMyKeyboardShortcuts,
-  verifyCert
+  verifyCert,
+  resetMyEditorLayout
 } from '../redux/settings/actions';

 const { apiLocation } = envData;
@@ -47,6 +48,7 @@ type ShowSettingsProps = Pick<ThemeProps, 'toggleNightMode'> & {
   navigate: (location: string) => void;
   showLoading: boolean;
   toggleSoundMode: (sound: boolean) => void;
+  resetEditorLayout: () => void;
   toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
   updateIsHonest: () => void;
   updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
@@ -80,6 +82,7 @@ const mapDispatchToProps = {
   updateIsHonest: updateMyHonesty,
   updateQuincyEmail: (sendQuincyEmail: boolean) =>
     updateMyQuincyEmail({ sendQuincyEmail }),
+  resetEditorLayout: () => resetMyEditorLayout(),
   verifyCert
 };

@@ -91,6 +94,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
     toggleNightMode,
     toggleSoundMode,
     toggleKeyboardShortcuts,
+    resetEditorLayout,
     user: {
       completedChallenges,
       email,
@@ -140,6 +144,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
     return <Loader fullScreen={true} />;
   }
   const sound = (store.get('fcc-sound') as boolean) ?? false;
+  const editorLayout = (store.get('challenge-layout') as boolean) ?? false;
   return (
     <>
       <Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} />
@@ -161,6 +166,8 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
             currentTheme={theme}
             keyboardShortcuts={keyboardShortcuts}
             sound={sound}
+            editorLayout={editorLayout}
+            resetEditorLayout={resetEditorLayout}
             toggleKeyboardShortcuts={toggleKeyboardShortcuts}
             toggleNightMode={toggleNightMode}
             toggleSoundMode={toggleSoundMode}
diff --git a/client/src/components/settings/misc-settings.tsx b/client/src/components/settings/misc-settings.tsx
index e5c4eab557c1bc..2518cf35a4dc85 100644
--- a/client/src/components/settings/misc-settings.tsx
+++ b/client/src/components/settings/misc-settings.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { useTranslation } from 'react-i18next';
-import { Spacer } from '@freecodecamp/ui';
+import { Button, Spacer } from '@freecodecamp/ui';
 import { FullWidthRow } from '../helpers';
 import ThemeSettings, { ThemeProps } from '../../components/settings/theme';
 import SoundSettings from '../../components/settings/sound';
@@ -11,15 +11,19 @@ type MiscSettingsProps = ThemeProps & {
   currentTheme: string;
   keyboardShortcuts: boolean;
   sound: boolean;
+  editorLayout: boolean | null;
   toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
   toggleNightMode: () => void;
   toggleSoundMode: (sound: boolean) => void;
+  resetEditorLayout: () => void;
 };

 const MiscSettings = ({
   currentTheme,
   keyboardShortcuts,
   sound,
+  editorLayout,
+  resetEditorLayout,
   toggleKeyboardShortcuts,
   toggleNightMode,
   toggleSoundMode
@@ -41,6 +45,19 @@ const MiscSettings = ({
           explain={t('settings.shortcuts-explained')?.toString()}
         />
         <ScrollbarWidthSettings />
+        <label htmlFor='reset-layout-btn'>
+          {t('settings.reset-editor-layout-tooltip')}
+        </label>
+        <Spacer size='xs' />
+        <Button
+          onClick={resetEditorLayout}
+          id='reset-layout-btn'
+          data-playwright-test-label='reset-layout-btn'
+          disabled={!editorLayout}
+          aria-disabled={!editorLayout}
+        >
+          {t('settings.reset-editor-layout')}
+        </Button>
       </FullWidthRow>
     </>
   );
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index 931c8b6044ec2d..a4b76b21ab9323 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -491,6 +491,8 @@ export const reducer = handleActions(
       payload ? spreadThePayloadOnUser(state, payload) : state,
     [settingsTypes.updateMyPortfolioComplete]: (state, { payload }) =>
       payload ? spreadThePayloadOnUser(state, payload) : state,
+    [settingsTypes.resetMyEditorLayoutComplete]: (state, { payload }) =>
+      payload ? spreadThePayloadOnUser(state, payload) : state,
     [settingsTypes.verifyCertComplete]: (state, { payload }) =>
       payload ? spreadThePayloadOnUser(state, payload) : state,
     [settingsTypes.submitProfileUIComplete]: (state, { payload }) =>
diff --git a/client/src/redux/settings/action-types.js b/client/src/redux/settings/action-types.js
index 853690022b1e32..68931381c01d77 100644
--- a/client/src/redux/settings/action-types.js
+++ b/client/src/redux/settings/action-types.js
@@ -18,6 +18,7 @@ export const actionTypes = createTypes(
     ...createAsyncTypes('submitProfileUI'),
     ...createAsyncTypes('verifyCert'),
     ...createAsyncTypes('resetProgress'),
+    ...createAsyncTypes('resetMyEditorLayout'),
     ...createAsyncTypes('deleteAccount')
   ],
   ns
diff --git a/client/src/redux/settings/actions.js b/client/src/redux/settings/actions.js
index bb451ca2d96fbd..466e74dfa22ae5 100644
--- a/client/src/redux/settings/actions.js
+++ b/client/src/redux/settings/actions.js
@@ -105,5 +105,13 @@ export const verifyCertError = createAction(types.verifyCertError);
 export const resetProgress = createAction(types.resetProgress);
 export const resetProgressError = createAction(types.resetProgressError);

+export const resetMyEditorLayout = createAction(types.resetMyEditorLayout);
+export const resetMyEditorLayoutComplete = createAction(
+  types.resetMyEditorLayoutComplete
+);
+export const resetMyEditorLayoutError = createAction(
+  types.resetMyEditorLayoutError
+);
+
 export const deleteAccount = createAction(types.deleteAccount);
 export const deleteAccountError = createAction(types.deleteAccountError);
diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js
index 99c8692d62a9be..35c47eef3fbd60 100644
--- a/client/src/redux/settings/settings-sagas.js
+++ b/client/src/redux/settings/settings-sagas.js
@@ -30,6 +30,8 @@ import {
 } from '../../utils/ajax';
 import { completedChallengesSelector } from '../selectors';
 import {
+  resetMyEditorLayoutComplete,
+  resetMyEditorLayoutError,
   submitNewAboutComplete,
   submitNewAboutError,
   submitNewUsernameComplete,
@@ -110,6 +112,26 @@ function* updateMySoundSaga({ payload: update }) {
   }
 }

+function* resetMyEditorLayoutSaga() {
+  const layout = store.get('challenge-layout');
+
+  if (layout) {
+    try {
+      const data = {
+        message: 'flash.reset-editor-layout',
+        type: 'success'
+      };
+
+      store.remove('challenge-layout');
+
+      yield put(createFlashMessage({ ...data }));
+      yield put(resetMyEditorLayoutComplete({ ...data }));
+    } catch (e) {
+      yield put(resetMyEditorLayoutError);
+    }
+  }
+}
+
 function* updateMyThemeSaga({ payload: update }) {
   try {
     const { data } = yield call(putUpdateMyTheme, update);
@@ -225,6 +247,7 @@ export function createSettingsSagas(types) {
     takeEvery(types.updateMySocials, updateMySocialsSaga),
     takeEvery(types.updateMyHonesty, updateMyHonestySaga),
     takeEvery(types.updateMySound, updateMySoundSaga),
+    takeEvery(types.resetMyEditorLayout, resetMyEditorLayoutSaga),
     takeEvery(types.updateMyTheme, updateMyThemeSaga),
     takeEvery(types.updateMyKeyboardShortcuts, updateMyKeyboardShortcutsSaga),
     takeEvery(types.updateMyQuincyEmail, updateMyQuincyEmailSaga),

Add a button labeled "Reset Editor Layout" (with id "reset-layout-btn") that is disabled when there is no custom editor layout, and show a tooltip explaining that it resets the editor layout to its default state. Make sure to use the correct text from the translation file.

Prompt:
The following is the text of a github issue and related comments for this repository:

Reset editor layout option
### Is your feature request related to a problem? Please describe.

We fairly frequently get support threads with campers that have gotten the editor into some unusable state by dragging the layout around.

### Describe the solution you'd like

Reset layout button in the editor.

### Describe alternatives you've considered

It could also be an option on the settings page to reset the editor layout.

### Additional context

_No response_

The button should have id (and playwright test id) "reset-layout-btn". It should contain the text "Reset Editor Layout". It should reset the sizing of editor panes to their original dimensions.
When clicked, there should be a flash message saying "Your editor layout has been reset".

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/e2e/reset-editor-layout.spec.ts b/e2e/reset-editor-layout.spec.ts
new file mode 100644
index 00000000000000..11af13d2b95b49
--- /dev/null
+++ b/e2e/reset-editor-layout.spec.ts
@@ -0,0 +1,48 @@
+import { test, expect } from '@playwright/test';
+test.use({ storageState: 'playwright/.auth/certified-user.json' });
+test.describe('Reset Editor Layout', () => {
+  test('drag layout and reset', async ({ page, isMobile }) => {
+    test.skip(
+      isMobile,
+      'The mobile layout does not have resizable panes, so this test is not applicable.'
+    );
+
+    await page.goto(
+      '/learn/2022/responsive-web-design/learn-basic-css-by-building-a-cafe-menu/step-15'
+    );
+
+    const desktopLayout = page.getByTestId('desktop-layout');
+    const splitter = page.getByTestId('preview-left-splitter');
+    const editorPane = desktopLayout.getByTestId('editor-pane');
+    const initialStyle = await editorPane.getAttribute('style');
+
+    expect(initialStyle).toContain('flex: 1');
+
+    // Drag the splitter to resize the editor pane
+    await splitter.hover();
+    await page.mouse.down();
+    await page.mouse.move(100, 100);
+    await page.mouse.up();
+
+    const newStyle = await editorPane.getAttribute('style');
+    expect(newStyle).not.toContain('flex: 1');
+
+    await page.goto('/settings#privacy-settings');
+
+    const resetButton = page.getByTestId('reset-layout-btn');
+    await resetButton.click();
+
+    await expect(page.getByTestId('flash-message')).toContainText(
+      'Your editor layout has been reset'
+    );
+
+    await expect(resetButton).toBeDisabled();
+
+    await page.goto(
+      '/learn/2022/responsive-web-design/learn-basic-css-by-building-a-cafe-menu/step-15'
+    );
+
+    const afterReset = await editorPane.getAttribute('style');
+    expect(afterReset).toContain('flex: 1');
+  });
+});


diff --git a/client/src/components/profile/components/portfolio.tsx b/client/src/components/profile/components/portfolio.tsx
index 987277134c2e30..7d45bae306b12a 100644
--- a/client/src/components/profile/components/portfolio.tsx
+++ b/client/src/components/profile/components/portfolio.tsx
@@ -33,6 +33,7 @@ type PortfolioProps = {
  isImageValid:
@@ -76,7 +82,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
      this.
@@ -86,6 +92,14 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
        if (key === 'image' && userInput) {
        else

@@ -199,10 +229,9 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
    const { state: imageState, message: imageMessage } =

@@ -263,6 +292,13 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {

    const combineImageStatus =
    const combineImageMessage =

@@ -316,7 +352,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
            validationState=

diff --git a/client/src/components/profile/components/portfolio.tsx b/client/src/components/profile/components/portfolio.tsx
index 987277134c2e30..7d45bae306b12a 100644
--- a/client/src/components/profile/components/portfolio.tsx
+++ b/client/src/components/profile/components/portfolio.tsx
@@ -33,6 +33,7 @@ type PortfolioProps = {
 type PortfolioState = {
   portfolio: PortfolioProjectData[];
   unsavedItemId: string | null;
+  isImageValid: ProfileValidation;
 };

 interface ProfileValidation {
@@ -55,15 +56,20 @@ function createFindById(id: string) {
 }

 class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
+  validationImage: HTMLImageElement;
   static displayName: string;
   constructor(props: PortfolioProps) {
     super(props);
-
+    this.validationImage = new Image();
     const { portfolio = [] } = props;

     this.state = {
       portfolio: [...portfolio],
-      unsavedItemId: null
+      unsavedItemId: null,
+      isImageValid: {
+        state: 'success',
+        message: ''
+      }
     };
   }

@@ -76,7 +82,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
     (e: React.ChangeEvent<HTMLInputElement>) => {
       e.preventDefault();
       const userInput = e.target.value.slice();
-      return this.setState(state => {
+      this.setState(state => {
         const { portfolio: currentPortfolio } = state;
         const mutablePortfolio = currentPortfolio.slice(0);
         const index = findIndex(currentPortfolio, p => p.id === id);
@@ -86,6 +92,14 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
           [key]: userInput
         };

+        if (key === 'image' && userInput) {
+          void this.validateImageLoad(userInput).then(imageValidation => {
+            this.setState({ isImageValid: imageValidation });
+          });
+        } else if (key === 'image' && !userInput) {
+          this.setState({ isImageValid: { state: 'success', message: '' } });
+        }
+
         return { portfolio: mutablePortfolio };
       });
     };
@@ -168,29 +182,45 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
     return { state: 'success', message: '' };
   }

-  getUrlValidation(maybeUrl: string, isImage?: boolean) {
-    const { t } = this.props;
-    const len = maybeUrl.length;
-    if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
-      return {
-        state: 'error',
-        message: t('validation.invalid-protocol')
+  async validateImageLoad(image: string): Promise<ProfileValidation> {
+    return new Promise(resolve => {
+      this.validationImage.src = encodeURI(image);
+
+      this.validationImage.onload = () => {
+        resolve({
+          state: 'success',
+          message: ''
+        });
       };
+
+      this.validationImage.onerror = () => {
+        resolve({
+          state: 'error',
+          message: this.props.t('validation.url-not-image')
+        });
+      };
+    });
+  }
+
+  getUrlValidation(url: string) {
+    const { t } = this.props;
+    const len = url.length;
+
+    if (!url) {
+      return { state: 'success', message: '' };
     }
-    if (isImage && !maybeUrl) {
-      return { state: null, message: '' };
-    }
-    if (isImage && !/\.(png|jpg|jpeg|gif)$/.test(maybeUrl)) {
+
+    if (len >= 4 && !hasProtocolRE.test(url)) {
       return {
         state: 'error',
-        message: t('validation.url-not-image')
+        message: t('validation.invalid-protocol')
       };
     }
-    return isURL(maybeUrl)
+
+    return isURL(url)
       ? { state: 'success', message: '' }
       : { state: 'warning', message: t('validation.use-valid-url') };
   }
-
   formCorrect(portfolio: PortfolioProjectData) {
     const { id, title, description, url, image } = portfolio;

@@ -199,10 +229,9 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
     const { state: urlState, message: urlMessage } = this.getUrlValidation(url);
     const { state: descriptionState, message: descriptionMessage } =
       this.getDescriptionValidation(description);
-    const { state: imageState, message: imageMessage } = this.getUrlValidation(
-      image,
-      true
-    );
+    const { state: imageState, message: imageMessage } =
+      this.getUrlValidation(image);
+
     const pristine = this.isFormPristine(id);

     const urlIsValid = !isURL(url, {
@@ -263,6 +292,13 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
       this.toggleEditing();
       return this.updateItem(id);
     };
+
+    const combineImageStatus =
+      imageState === 'success' && this.state.isImageValid.state === 'success'
+        ? null
+        : 'error';
+    const combineImageMessage = imageMessage || this.state.isImageValid.message;
+
     return (
       <FullWidthRow key={id}>
         <form
@@ -316,7 +352,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
           </FormGroup>
           <FormGroup
             controlId={`${id}-image`}
-            validationState={pristine ? null : imageState}
+            validationState={pristine ? null : combineImageStatus}
           >
             <ControlLabel htmlFor={`${id}-image-input`}>
               {t('settings.labels.image')}
@@ -328,9 +364,9 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
               name='portfolio-image'
               id={`${id}-image-input`}
             />
-            {imageMessage ? (
+            {combineImageMessage ? (
               <HelpBlock data-playwright-test-label='image-validation'>
-                {imageMessage}
+                {combineImageMessage}
               </HelpBlock>
             ) : null}
           </FormGroup>
@@ -387,6 +423,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
   render() {
     const { t } = this.props;
     const { portfolio = [], unsavedItemId } = this.state;
+
     return (
       <section id='portfolio-settings'>
         <SectionHeader>{t('settings.headings.portfolio')}</SectionHeader>

Track whether images are valid by actually loading them and checking for errors, displaying "URL must link directly to an image file" if loading fails.

Prompt:
The following is the text of a github issue and related comments for this repository:

Weird validation for portfolio inputs in the setting page
### Describe the Issue

Validations are weirdly strict in imageUrl, but can still accept broken result, but lenient on URL section, although it doesn't accept broken URL.

### Affected Page

https://www.freecodecamp.org/settings

### Steps to Reproduce

1. Go to https://www.freecodecamp.org/settings
2. Type "https.com" in URL section
3. Type "https://github.com/freeCodeCamp/cdn/blob/main/build/chapter/puppy-small.jpg" in the image section
4. Go to your portfolio
5. See broken Media component

Note: I know that the correct URL is https://cdn.freecodecamp.org/chapter/orange-graphics-small.jpg, the issue is that I can spend time creating an item in portfolio, and it results in broken image later without knowing why.
![image](https://user-images.githubusercontent.com/88248797/214584980-f7a782cd-97ee-43f6-940b-c5dc8ee5614d.png)

### Expected behavior

- stricter validation on URL
- lenient validation on imageURL, maybe shows how image looks instead, so it becomes harder to break it in the portfolio page
![image](https://user-images.githubusercontent.com/88248797/214584806-309c0ac0-1902-4173-b2ad-7e9c6e90834c.png)

### Screenshots

![image](https://user-images.githubusercontent.com/88248797/214585811-d7bb8a0a-3d17-48b1-9e24-f3c915b81ee5.png)

### System

- Device: [e.g. iPhone 6, Laptop]
- OS: [e.g. iOS 14, Windows 10, Ubuntu 20.04]
- Browser: [e.g. Chrome, Safari]
- Version: [e.g. 22]

### Additional context

_No response_

To fix this, we should follow similar logic as in the "Personal Information" section to validate that the URL points to a valid image that can be loaded.
If the URL simply isn't valid, we should display "Please use a valid URL" in the image-validation HelpBlock.
If the URL is valid but does not point to an image that can be loaded, we should display "URL must link directly to an image file" in the HelpBlock.

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/client/src/components/profile/components/portfolio.tsx b/client/src/components/profile/components/portfolio.tsx
index 987277134c2e30..7d45bae306b12a 100644
--- a/client/src/components/profile/components/portfolio.tsx
+++ b/client/src/components/profile/components/portfolio.tsx
@@ -33,6 +33,7 @@ type PortfolioProps = {
 type PortfolioState = {
   portfolio: PortfolioProjectData[];
   unsavedItemId: string | null;
+  isImageValid: ProfileValidation;
 };

 interface ProfileValidation {
@@ -76,7 +82,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
     (e: React.ChangeEvent<HTMLInputElement>) => {
       e.preventDefault();
       const userInput = e.target.value.slice();
-      return this.setState(state => {
+      this.setState(state => {
         const { portfolio: currentPortfolio } = state;
         const mutablePortfolio = currentPortfolio.slice(0);
         const index = findIndex(currentPortfolio, p => p.id === id);
@@ -86,6 +92,14 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
           [key]: userInput
         };

+        if (key === 'image' && userInput) {
+          void this.validateImageLoad(userInput).then(imageValidation => {
+            this.setState({ isImageValid: imageValidation });
+          });
+        } else if (key === 'image' && !userInput) {
+          this.setState({ isImageValid: { state: 'success', message: '' } });
+        }
+
         return { portfolio: mutablePortfolio };
       });
     };
@@ -199,10 +229,9 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
     const { state: urlState, message: urlMessage } = this.getUrlValidation(url);
     const { state: descriptionState, message: descriptionMessage } =
       this.getDescriptionValidation(description);
-    const { state: imageState, message: imageMessage } = this.getUrlValidation(
-      image,
-      true
-    );
+    const { state: imageState, message: imageMessage } =
+      this.getUrlValidation(image);
+
     const pristine = this.isFormPristine(id);

     const urlIsValid = !isURL(url, {
@@ -263,6 +292,13 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
       this.toggleEditing();
       return this.updateItem(id);
     };
+
+    const combineImageStatus =
+      imageState === 'success' && this.state.isImageValid.state === 'success'
+        ? null
+        : 'error';
+    const combineImageMessage = imageMessage || this.state.isImageValid.message;
+
     return (
       <FullWidthRow key={id}>
         <form
@@ -316,7 +352,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
           </FormGroup>
           <FormGroup
             controlId={`${id}-image`}
-            validationState={pristine ? null : imageState}
+            validationState={pristine ? null : combineImageStatus}
           >
             <ControlLabel htmlFor={`${id}-image-input`}>
               {t('settings.labels.image')}

diff --git a/e2e/portfolio.spec.ts b/e2e/portfolio.spec.ts
index 5bd3fb0ebfdaa1..631daba3f7734a 100644
--- a/e2e/portfolio.spec.ts
+++ b/e2e/portfolio.spec.ts
@@ -63,12 +63,21 @@ test.describe('Add Portfolio Item', () => {
   test('The image has validation', async ({ page }) => {
     await page.getByLabel(translations.settings.labels.image).fill('T');
     await expect(page.getByTestId('image-validation')).toContainText(
-      'URL must link directly to an image file'
+      'Please use a valid URL'
     );
     await page
       .getByLabel(translations.settings.labels.image)
-      .fill('http://helloworld.com/image.png');
+      .fill(
+        'https://cdn.freecodecamp.org/universal/favicons/favicon-32x32.png'
+      );
     await expect(page.getByTestId('image-validation')).toBeHidden();
+
+    await page
+      .getByLabel(translations.settings.labels.image)
+      .fill('https://cdn.freecodecamp.org/universal/favicons/favicon-32x32.pn');
+    await expect(page.getByTestId('image-validation')).toContainText(
+      'URL must link directly to an image file'
+    );
   });

   test('The description has validation', async ({ page }) => {


diff --git a/api/src/schemas/certificate/cert-slug.ts b/api/src/schemas/certificate/cert-slug.ts
index 402fb2fac408f0..9880b6f5232527 100644
--- a/api/src/schemas/certificate/cert-slug.ts
+++ b/api/src/schemas/certificate/cert-slug.ts
@@ -85,14 +85,7 @@ export const certSlug = {
        name:

diff --git a/api/src/schemas/certificate/cert-slug.ts b/api/src/schemas/certificate/cert-slug.ts
index 402fb2fac408f0..9880b6f5232527 100644
--- a/api/src/schemas/certificate/cert-slug.ts
+++ b/api/src/schemas/certificate/cert-slug.ts
@@ -85,14 +85,7 @@ export const certSlug = {
         certSlug: Type.Enum(Certification),
         certTitle: Type.String(),
         username: Type.String(),
-        date: Type.Number(),
-        completionTime: Type.Number()
-      }),
-      Type.Object({
-        certSlug: Type.Enum(Certification),
-        certTitle: Type.String(),
-        username: Type.String(),
-        name: Type.String(),
+        name: Type.Optional(Type.String()),
         date: Type.Number(),
         completionTime: Type.Number()
       }),

Combine the two object schemas into one, making the `name` field optional.

Prompt:
The following is the text of a github issue and related comments for this repository:

[Staging] Username is displayed instead of name on certification
### Describe the Issue

On https://www.freecodecamp.dev site, certifications are showing the username instead of real name, even if I set the name to public.

I'm not sure if this is related to the new API, but it doesn't happen in production.

### Affected Page

https://www.freecodecamp.dev/certification/fccf064ea61-53d7-44b7-9051-7f4bc75e2702/responsive-web-design

### Steps to Reproduce

1. Go to https://www.freecodecamp.dev/certification/fccf064ea61-53d7-44b7-9051-7f4bc75e2702/responsive-web-design

### Expected behavior

When the name is set to public, certifications should show the name instead of username

### Screenshots

![Image](https://github.com/user-attachments/assets/5e45506a-f91e-43db-a3f1-0c3e9fa0c74b)

![Image](https://github.com/user-attachments/assets/23d4f2e9-8a66-44a9-a0b2-a454ed690454)

![Image](https://github.com/user-attachments/assets/95067b65-fee4-4ae0-82f9-b409435165a1)

### System

- Device: Laptop
- OS: Windows 10
- Browser: Chrome
- Version: 131.0.6778.140 (Official Build) (64-bit)

### Additional context

_No response_

If showName is true, the endpoint should return both user's name and username. Otherwise, it should only return username.

freeCodeCamp/api/src/routes/public/certificate.ts

Lines 213 to 232 in 2563c61

 if (!user.profileUI.showName) {
   void reply.code(200);
   return reply.send({
     certSlug,
     certTitle,
     username,
     date: completedDate,
     completionTime
   });
 }

 void reply.code(200);
 return reply.send({
   certSlug,
   certTitle,
   username,
   name,
   date: completedDate,
   completionTime
 });
I went to https://www.freecodecamp.dev/certification/fccf064ea61-53d7-44b7-9051-7f4bc75e2702/responsive-web-design (the page that Yoko was testing with) and checked the API responses.

/get-public-profile returns:

{
  profileUI: {
    showName: true
  }
}
But still /certificate/showCert doesn't return the name property.

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/api/src/routes/public/certificate.test.ts b/api/src/routes/public/certificate.test.ts
index 568c6155e96fa4..e4f2163d0751d2 100644
--- a/api/src/routes/public/certificate.test.ts
+++ b/api/src/routes/public/certificate.test.ts
@@ -7,11 +7,24 @@ import {
 } from '../../../jest.utils';
 import { getFallbackFullStackDate } from '../helpers/certificate-utils';

+const DATE_NOW = Date.now();
+
 describe('certificate routes', () => {
   setupServer();

   describe('Unauthenticated user', () => {
-    beforeAll(async () => resetDefaultUser());
+    beforeAll(async () => {
+      await resetDefaultUser();
+
+      jest.useFakeTimers({
+        doNotFake: ['nextTick']
+      });
+      jest.setSystemTime(DATE_NOW);
+    });
+
+    afterAll(() => {
+      jest.useRealTimers();
+    });

     describe('GET /certificate/showCert/:username/:certSlug', () => {
       beforeEach(async () => {
@@ -204,6 +217,70 @@ describe('certificate routes', () => {
         expect(response.status).toBe(200);
       });

+      test('should not return user full name if `showName` is `false`', async () => {
+        await fastifyTestInstance.prisma.user.update({
+          where: { id: defaultUserId },
+          data: {
+            profileUI: {
+              showTimeLine: true,
+              showCerts: true,
+              isLocked: false,
+              showName: false
+            },
+            isJsAlgoDataStructCert: true,
+            completedChallenges: [
+              {
+                id: '561abd10cb81ac38a17513bc', // Cert ID
+                completedDate: DATE_NOW
+              }
+            ]
+          }
+        });
+
+        const response = await superRequest(
+          '/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
+          {
+            method: 'GET'
+          }
+        );
+
+        // TODO: delete this assertion once there's only one status 200 response
+        expect(response.body).toHaveProperty('username', 'foobar');
+        expect(response.body).not.toHaveProperty('name');
+        expect(response.status).toBe(200);
+      });
+
+      test('should return user full name if `showName` is `true`', async () => {
+        await fastifyTestInstance.prisma.user.update({
+          where: { id: defaultUserId },
+          data: {
+            profileUI: {
+              showTimeLine: true,
+              showCerts: true,
+              isLocked: false,
+              showName: true
+            },
+            isJsAlgoDataStructCert: true,
+            completedChallenges: [
+              {
+                id: '561abd10cb81ac38a17513bc', // Cert ID
+                completedDate: DATE_NOW
+              }
+            ]
+          }
+        });
+
+        const response = await superRequest(
+          '/certificate/showCert/foobar/javascript-algorithms-and-data-structures',
+          {
+            method: 'GET'
+          }
+        );
+
+        expect(response.body).toHaveProperty('name', 'foobar');
+        expect(response.status).toBe(200);
+      });
+
       test('should return cert-not-found if there is no cert with that slug', async () => {
         const response = await superRequest(
           '/certificate/showCert/foobar/not-a-valid-cert-slug',


diff --git a/client/src/templates/Challenges/classic/lower-jaw.tsx b/client/src/templates/Challenges/classic/lower-jaw.tsx
index 58ce7923c9ea20..2c3064b5a599f2 100644
--- a/client/src/templates/Challenges/classic/lower-jaw.tsx
+++ b/client/src/templates/Challenges/classic/lower-jaw.tsx
@@ -285,10 +285,13 @@ const LowerJaw = ({
  const isDesktop =
  const isMacOS =
  const checkButtonText =

diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 1f364d3fc7d88f..a275ef92732948 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -57,6 +57,7 @@
     "run-test": "Run the Tests (Ctrl + Enter)",
     "check-code": "Check Your Code (Ctrl + Enter)",
     "check-code-2": "Check Your Code",
+    "check-code-3": "Check Your Code (Command + Enter)",
     "reset": "Reset",
     "reset-step": "Reset This Step",
     "help": "Help",
diff --git a/client/src/templates/Challenges/classic/lower-jaw.tsx b/client/src/templates/Challenges/classic/lower-jaw.tsx
index 58ce7923c9ea20..2c3064b5a599f2 100644
--- a/client/src/templates/Challenges/classic/lower-jaw.tsx
+++ b/client/src/templates/Challenges/classic/lower-jaw.tsx
@@ -285,10 +285,13 @@ const LowerJaw = ({
     testsLength &&
     (currentAttempts >= testsLength || currentAttempts >= 3);

-  const showDesktopButton = window.innerWidth > MAX_MOBILE_WIDTH;
+  const isDesktop = window.innerWidth > MAX_MOBILE_WIDTH;
+  const isMacOS = navigator.userAgent.includes('Mac OS');

-  const checkButtonText = showDesktopButton
-    ? t('buttons.check-code')
+  const checkButtonText = isDesktop
+    ? isMacOS
+      ? t('buttons.check-code-3')
+      : t('buttons.check-code')
     : t('buttons.check-code-2');

   const showSignInButton = !isSignedIn && challengeIsCompleted;

Show "Check Your Code (Command + Enter)" for desktop MacOS users, "Check Your Code (Ctrl + Enter)" for other desktop users, and just "Check Your Code" for mobile users. Use the correct text from the translation file depending on the user's operating system.

Prompt:
The following is the text of a github issue and related comments for this repository:

Enhancement Request: For MacOS, add text that says “Command + Enter to submit lesson"
### Is your feature request related to a problem? Please describe.

SUMMARY: On MacOS platform, it’s not made clear that Command+Enter can be used to submit lessons, despite the fact that this keyboard shortcut does work and is preferable. I suggest making this clear to Macbook users.

DISCUSSION:
Currently, the lessons say: Check Your Code (Ctrl + Enter). On Mac, the Ctrl key is in a slightly awkward position compared to the Command Key. The Command key is very easy and quick to hit.

This might seem like a small request, but I think that the change would be greatly appreciated and helpful to those who are (a) on Macs and (b) appreciate keyboard shortcuts.  My personal story is I "suffered" through the lessons for awhile, trying to get accustomed to Ctrl + Enter, but it was always a little bother. Once I figured out the "trick" of getting to the next page via ⌘+ Enter, I thought, "I wish I had known about that earlier!"

(Staff requested I open this as an issue: https://forum.freecodecamp.org/t/enhancement-request-text-that-says-enter-to-submit-lesson-on-mac/683217.)

### Describe the solution you'd like

* Change the text on MacOS to “Check Your Code (Command + Enter)”.

### Describe alternatives you've considered

None.

### Additional context

_No response_

Resolve the issue described above. Do not modify tests or user code. The issue can be resolved by modifying the actual library/application code. Make the minimal changes to resolve the issue while adhering to the specification and coding style of the codebase.

diff --git a/e2e/lower-jaw.spec.ts b/e2e/lower-jaw.spec.ts
index 0936e93b380801..232490a14d51d2 100644
--- a/e2e/lower-jaw.spec.ts
+++ b/e2e/lower-jaw.spec.ts
@@ -19,7 +19,7 @@ test('Check the initial states of submit button and "check your code" button', a
 });

 test('Click on the "check your code" button', async ({ page }) => {
-  const checkButton = page.getByTestId('lowerJaw-check-button');
+  const checkButton = page.getByRole('button', { name: 'Check Your Code' });

   await checkButton.click();

@@ -43,20 +43,26 @@ test('Should render UI correctly', async ({ page }) => {
     name: 'Check Your Code'
   });
   const lowerJawTips = page.getByTestId('failing-test-feedback');
-  await expect(codeCheckButton).toHaveText('Check Your Code (Ctrl + Enter)');
+  await expect(codeCheckButton).toBeVisible();
   await expect(lowerJawTips).toHaveCount(0);
 });

-test('Should display full button text on desktop but hide (Ctrl + Enter on mobile)', async ({
+test('Should display the text of the check code button accordingly based on device type and screen size', async ({
   page,
-  isMobile
+  isMobile,
+  browserName
 }) => {
-  const codeCheckButton = page.getByRole('button', {
-    name: 'Check Your Code'
-  });
-  await expect(codeCheckButton).toHaveText('Check Your Code (Ctrl + Enter)');
-
   if (isMobile) {
-    await expect(codeCheckButton).toHaveText('Check Your Code');
+    await expect(
+      page.getByRole('button', { name: 'Check Your Code', exact: true })
+    ).toBeVisible();
+  } else if (browserName === 'webkit') {
+    await expect(
+      page.getByRole('button', { name: 'Check Your Code (Command + Enter)' })
+    ).toBeVisible();
+  } else {
+    await expect(
+      page.getByRole('button', { name: 'Check Your Code (Ctrl + Enter)' })
+    ).toBeVisible();
   }
 });
diff --git a/e2e/progress-bar.spec.ts b/e2e/progress-bar.spec.ts
index 28ce001bf82fe8..4fea790c405f65 100644
--- a/e2e/progress-bar.spec.ts
+++ b/e2e/progress-bar.spec.ts
@@ -1,5 +1,4 @@
 import { expect, test, type Page } from '@playwright/test';
-import translations from '../client/i18n/locales/english/translations.json';

 let page: Page;

@@ -36,15 +35,7 @@ test.describe('Progress bar component', () => {
       '
CatPhotoApp

Cat Photos
'
     );

-    if (isMobile) {
-      await page
-        .getByRole('button', { name: translations.buttons['check-code-2'] })
-        .click();
-    } else {
-      await page
-        .getByRole('button', { name: translations.buttons['check-code'] })
-        .click();
-    }
+    await page.getByRole('button', { name: 'Check Your Code' }).click();

     const progressBarContainer = page.getByTestId('progress-bar-container');
     await expect(progressBarContainer).toContainText(