Merge branch 'main' into feat/swap-media-with-the-card

This commit is contained in:
Muhammed Mustafa 2023-02-16 12:40:36 +02:00 committed by GitHub
commit f0df85b385
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
296 changed files with 1725 additions and 2612 deletions

1
.github/labeler.yml vendored
View File

@ -23,4 +23,3 @@
- client/i18n/**/*
- config/crowdin/**/*
- config/i18n/**/*
- tools/crowdin/**/*

View File

@ -1,13 +1,15 @@
name: CI - Run CodeQL Analysis
on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
branches:
- 'main'
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
branches:
- 'main'
permissions:
contents: read
@ -32,8 +34,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Setup CodeQL
uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
with:
languages: ${{ matrix.language }}
- name: Perform Analysis
uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2

View File

@ -4,11 +4,14 @@ on:
# push:
# paths-ignore:
# - 'docs/**'
# branches-ignore:
# - 'renovate/**'
# branches:
# - 'main'
# pull_request:
# paths-ignore:
# - 'docs/**'
# branches:
# - 'main'
# - 'next-**'
jobs:
mobile-test:

View File

@ -5,7 +5,7 @@ name: CI - E2E - 3rd party donation tests
on:
push:
branches:
- 'prod-*'
- 'prod-**'
paths-ignore:
- 'docs/**'

View File

@ -3,14 +3,14 @@ on:
push:
paths-ignore:
- 'docs/**'
branches-ignore:
- 'renovate/**'
- 'next-api'
branches:
- 'main'
pull_request:
paths-ignore:
- 'docs/**'
branches-ignore:
- 'next-api'
branches:
- 'main'
- 'next-**'
jobs:
build-client:

View File

@ -2,14 +2,20 @@ name: CI - Node.js Test Upcoming
env:
NODE_OPTIONS: '--max_old_space_size=6144'
on:
# Run on push events, but only for the below branches
push:
branches:
# Treat the below branches as special case for working on workflows
- actions-**
- upcoming-**
- 'main'
- 'prod-**'
# Run on pull requests, but only for the below targets
pull_request:
branches:
- 'main'
- 'next-**'
schedule:
# run this Action every 14 days
- cron: '0 * */14 * *'
# Run on demand
workflow_dispatch:
permissions:

View File

@ -1,14 +1,21 @@
name: CI - Node.js Test Current
env:
NODE_OPTIONS: '--max_old_space_size=6144'
on:
# Run on push events, but only for the below branches
push:
branches-ignore:
- 'renovate/**'
- 'next-api'
branches:
- 'main'
- 'prod-**'
# Run on pull requests, but only for the below targets
pull_request:
branches-ignore:
- 'next-api'
branches:
- 'main'
- 'next-**'
# Run on Merge Queue
merge_group:
types: [checks_requested]
permissions:
contents: read

8
.gitpod.Dockerfile vendored Normal file
View File

@ -0,0 +1,8 @@
FROM gitpod/workspace-mongodb:latest
# from https://www.gitpod.io/docs/introduction/languages/javascript#node-versions
RUN bash -c 'VERSION="lts/*" \
&& source $HOME/.nvm/nvm.sh && nvm install $VERSION \
&& nvm use $VERSION && nvm alias default $VERSION'
RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix

View File

@ -1,4 +1,5 @@
image: gitpod/workspace-mongodb
image:
file: .gitpod.Dockerfile
ports:
- port: 27017 # mongodb
onOpen: ignore

View File

@ -45,6 +45,7 @@
"dedent": "0.7.0",
"dotenv": "6.2.0",
"express-flash": "0.0.2",
"express-rate-limit": "^6.7.0",
"express-session": "1.17.3",
"express-validator": "6.14.1",
"helmet": "3.23.3",
@ -60,12 +61,14 @@
"mongodb": "3.6.9",
"morgan": "1.10.0",
"nanoid": "3.3.4",
"node-fetch": "^2.6.7",
"nodemailer-ses-transport": "1.5.1",
"passport": "0.4.1",
"passport-auth0": "1.4.2",
"passport-local": "1.0.0",
"passport-mock-strategy": "2.0.0",
"query-string": "6.14.0",
"rate-limit-mongo": "^2.3.2",
"rx": "4.1.0",
"stripe": "8.205.0",
"uuid": "3.4.0",

View File

@ -162,6 +162,8 @@ export default function initializeUser(User) {
User.definition.properties.rand.default = getRandomNumber;
// increase user accessToken ttl to 900 days
User.settings.ttl = 900 * 24 * 60 * 60 * 1000;
// Sets ttl to 900 days for mobile login created access tokens
User.settings.maxTTL = 900 * 24 * 60 * 60 * 1000;
// username should not be in blocklist
User.validatesExclusionOf('username', {
@ -341,6 +343,21 @@ export default function initializeUser(User) {
);
};
User.prototype.mobileLoginByRequest = function mobileLoginByRequest(
req,
res
) {
return new Promise((resolve, reject) =>
this.createAccessToken({}, (err, accessToken) => {
if (err) {
return reject(err);
}
setAccessTokenToResponse({ accessToken }, req, res);
return resolve(accessToken);
})
);
};
User.afterRemote('logout', function ({ req, res }, result, next) {
removeCookies(req, res);
next();

View File

@ -2,10 +2,9 @@ import dedent from 'dedent';
import { check } from 'express-validator';
import jwt from 'jsonwebtoken';
import passport from 'passport';
import fetch from 'node-fetch';
import { isEmail } from 'validator';
import { jwtSecret } from '../../../../config/secrets';
import { decodeEmail } from '../../common/utils';
import {
createPassportCallbackAuthenticator,
@ -14,7 +13,11 @@ import {
} from '../component-passport';
import { wrapHandledError } from '../utils/create-handled-error.js';
import { removeCookies } from '../utils/getSetAccessToken';
import { ifUserRedirectTo, ifNoUserRedirectHome } from '../utils/middleware';
import {
ifUserRedirectTo,
ifNoUserRedirectHome,
ifNotMobileRedirect
} from '../utils/middleware';
import { getRedirectParams } from '../utils/redirection';
import { createDeleteUserToken } from '../middlewares/user-token';
@ -34,6 +37,7 @@ module.exports = function enableAuthentication(app) {
// enable loopback access control authentication. see:
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
app.enableAuth();
const ifNotMobile = ifNotMobileRedirect();
const ifUserRedirect = ifUserRedirectTo();
const ifNoUserRedirect = ifNoUserRedirectHome();
const devSaveAuthCookies = devSaveResponseAuthCookies();
@ -87,6 +91,8 @@ module.exports = function enableAuthentication(app) {
createGetPasswordlessAuth(app)
);
api.get('/mobile-login', ifNotMobile, ifUserRedirect, mobileLogin(app));
app.use(api);
};
@ -188,3 +194,53 @@ function createGetPasswordlessAuth(app) {
);
};
}
function mobileLogin(app) {
const {
models: { User }
} = app;
return async function getPasswordlessAuth(req, res, next) {
try {
const auth0Res = await fetch(
`https://${process.env.AUTH0_DOMAIN}/userinfo`,
{
headers: { Authorization: req.headers.authorization }
}
);
if (!auth0Res.ok) {
return next(
wrapHandledError(new Error('Invalid Auth0 token'), {
type: 'danger',
message: 'We could not log you in, please try again in a moment.',
status: auth0Res.status
})
);
}
const { email } = await auth0Res.json();
if (!isEmail(email)) {
return next(
wrapHandledError(new TypeError('decoded email is invalid'), {
type: 'danger',
message: 'The email is incorrectly formatted',
status: 400
})
);
}
User.findOne$({ where: { email } })
.do(async user => {
if (!user) {
user = await User.create({ email });
}
await user.mobileLoginByRequest(req, res);
res.end();
})
.subscribe(() => {}, next);
} catch (err) {
next(err);
}
};
}

View File

@ -39,7 +39,10 @@
"./middlewares/constant-headers": {},
"./middlewares/csp": {},
"./middlewares/flash-cheaters": {},
"./middlewares/passport-login": {}
"./middlewares/passport-login": {},
"./middlewares/rate-limit": {
"paths": ["/mobile-login"]
}
},
"files": {},
"final:after": {

View File

@ -0,0 +1,23 @@
import rateLimit from 'express-rate-limit';
import MongoStore from 'rate-limit-mongo';
const url = process.env.MONGODB || process.env.MONGOHQ_URL;
// Rate limit for mobile login
// 10 requests per 15 minute windows
export default function rateLimitMiddleware() {
return rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: req => {
return req.headers['x-forwarded-for'] || 'localhost';
},
store: new MongoStore({
collectionName: 'UserRateLimit',
uri: url,
expireTimeMs: 15 * 60 * 1000
})
});
}

View File

@ -26,6 +26,7 @@ const updateHooksRE = /^\/hooks\/update-paypal$/;
// note: this would be replaced by webhooks later
const donateRE = /^\/donate\/charge-stripe$/;
const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/;
const mobileLoginRE = /^\/mobile-login\/?$/;
const _pathsAllowedREs = [
authRE,
@ -41,7 +42,8 @@ const _pathsAllowedREs = [
unsubscribeRE,
updateHooksRE,
donateRE,
submitCoderoadChallengeRE
submitCoderoadChallengeRE,
mobileLoginRE
];
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {

View File

@ -77,6 +77,20 @@ export function ifUserRedirectTo(status) {
};
}
export function ifNotMobileRedirect() {
return (req, res, next) => {
//
// Todo: Use the below check once we have done more research on usage
//
// const isMobile = /(iPhone|iPad|Android)/.test(req.headers['user-agent']);
// if (!isMobile) {
// res.json({ error: 'not from mobile' });
// } else {
// next();
// }
next();
};
}
// for use with express-validator error formatter
export const createValidatorErrorHandler =
(...args) =>

View File

@ -6,7 +6,7 @@ import { Provider } from 'react-redux';
import i18n from './i18n/config';
import AppMountNotifier from './src/components/app-mount-notifier';
import { createStore } from './src/redux/createStore';
import { createStore } from './src/redux/create-store';
import layoutSelector from './utils/gatsby/layout-selector';
import GrowthBookProvider from './src/components/growth-book/growth-book-wrapper';

View File

@ -4,7 +4,7 @@ import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import i18n from './i18n/config';
import { createStore } from './src/redux/createStore';
import { createStore } from './src/redux/create-store';
import layoutSelector from './utils/gatsby/layout-selector';
import { getheadTagComponents, getPostBodyComponents } from './utils/tags';
import GrowthBookProvider from './src/components/growth-book/growth-book-wrapper';

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>أسناد: Rosetta Code</a>"
]
},
"the-odin-project": {
"title": "مشروع أودين",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "مشروع Euler",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "مشروع أودين",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "شهادة {{cert}}",
"browse-other": "تصفح الشهادات المجانية الأخرى\n(نوصي بالقيام بها بالترتيب)",

View File

@ -15,8 +15,8 @@
"show-cert": "عرض الشهادة",
"claim-cert": "المطالبة بالشهادة",
"save-progress": "حفظ التقدم",
"accepted-honesty": "لقد قبلت سياسة الصدق الأكاديمي الخاصة بنا.",
"agree": "موافق",
"accepted-honesty": "لقد وافقت على سياستنا للصدق الأكاديمي.",
"agree-honesty": "أوافق على سياسة freeCodeCamp للصدق الأكاديمي.",
"save-portfolio": "حفظ عنصر الحافظة هذا",
"remove-portfolio": "إزالة عنصر الحافظة هذا",
"add-portfolio": "إضافة عنصر حافظة جديد",
@ -302,7 +302,6 @@
"certs": "شهادة {{title}}"
},
"editor-tabs": {
"info": "معلومات",
"code": "الكود",
"tests": "الاختبارات",
"restart": "أعد التشغيل",
@ -518,7 +517,7 @@
"opens-new-window": "فتح في نافذة جديدة"
},
"flash": {
"honest-first": "للمطالبة بشهادة ، يجب عليك أولاً قبول سياسة الصدق الأكاديمي الخاصة بنا",
"honest-first": "للمطالبة بشهادة، يجب عليك أولاً الموافقة على سياسة للصدق الأكاديمي",
"really-weird": "حدث شيء غريب حقاً، إذا حدث مرة أخرى، يرجى النظر في الإبلاغ عنها على https://github.com/freeCodeCamp/freeCodeCamp/issues/new",
"not-right": "يبدو ان هناك خطأ ما. لقد تم إنشاء تقرير وتم إخطار فريق freeCodeCamp.org",
"went-wrong": "حدث خطأ ما، الرجاء التحقق والمحاولة مرة أخرى",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>屬性Rosetta 代碼</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "歐拉計劃",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "{{cert}} 認證",
"browse-other": "瀏覽我們的其他免費認證\n我們建議你按順序學習",

View File

@ -15,8 +15,8 @@
"show-cert": "顯示認證",
"claim-cert": "申請認證",
"save-progress": "保存進度",
"accepted-honesty": "你已接受我們的《學術誠信條例》",
"agree": "同意",
"accepted-honesty": "You have agreed to our Academic Honesty Policy.",
"agree-honesty": "I agree to freeCodeCamp's Academic Honesty Policy.",
"save-portfolio": "保存這個作品集項目",
"remove-portfolio": "移除這個作品集項目",
"add-portfolio": "增加一個新的作品集項目",
@ -302,7 +302,6 @@
"certs": "{{title}} 認證"
},
"editor-tabs": {
"info": "信息",
"code": "編程",
"tests": "測試",
"restart": "重啓",
@ -518,7 +517,7 @@
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "申請認證之前,你必須先接受我們的《學術誠信條例》",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "出現了一些奇怪的情況。如果再出現這種情況,請考慮在 https://github.com/freeCodeCamp/freeCodeCamp/issues/new 提交 issue。",
"not-right": "有些不對勁。已生成報告,通知 freeCodeCamp.org 團隊。",
"went-wrong": "出了點問題,請檢查並重試。",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>属性Rosetta 代码</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "欧拉计划",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "{{cert}} 认证",
"browse-other": "浏览我们的其他免费认证\n我们建议你按顺序学习",

View File

@ -15,8 +15,8 @@
"show-cert": "显示认证",
"claim-cert": "申请认证",
"save-progress": "保存进度",
"accepted-honesty": "你已接受我们的《学术诚信条例》",
"agree": "同意",
"accepted-honesty": "You have agreed to our Academic Honesty Policy.",
"agree-honesty": "I agree to freeCodeCamp's Academic Honesty Policy.",
"save-portfolio": "保存这个作品集项目",
"remove-portfolio": "移除这个作品集项目",
"add-portfolio": "增加一个新的作品集项目",
@ -302,7 +302,6 @@
"certs": "{{title}} 认证"
},
"editor-tabs": {
"info": "信息",
"code": "编程",
"tests": "测试",
"restart": "重启",
@ -518,7 +517,7 @@
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "申请认证之前,你必须先接受我们的《学术诚信条例》",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "出现了一些奇怪的情况。如果再出现这种情况,请考虑在 https://github.com/freeCodeCamp/freeCodeCamp/issues/new 提交 issue。",
"not-right": "有些不对劲。已生成报告,通知 freeCodeCamp.org 团队。",
"went-wrong": "出了点问题,请检查并重试。",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>Attribute: Rosetta Code</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "Project Euler",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "{{cert}} Certification",
"browse-other": "Browse our other free certifications\n(we recommend doing these in order)",

View File

@ -15,8 +15,8 @@
"show-cert": "Show Certification",
"claim-cert": "Claim Certification",
"save-progress": "Save Progress",
"accepted-honesty": "You have accepted our Academic Honesty Policy.",
"agree": "Agree",
"accepted-honesty": "You have agreed to our Academic Honesty Policy.",
"agree-honesty": "I agree to freeCodeCamp's Academic Honesty Policy.",
"save-portfolio": "Save this portfolio item",
"remove-portfolio": "Remove this portfolio item",
"add-portfolio": "Add a new portfolio Item",
@ -302,7 +302,6 @@
"certs": "{{title}} Certification"
},
"editor-tabs": {
"info": "Info",
"code": "Code",
"tests": "Tests",
"restart": "Restart",
@ -518,7 +517,7 @@
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "To claim a certification, you must first accept our academic honesty policy",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "Something really weird happened, if it happens again, please consider raising an issue on https://github.com/freeCodeCamp/freeCodeCamp/issues/new",
"not-right": "Something is not quite right. A report has been generated and the freeCodeCamp.org team have been notified",
"went-wrong": "Something went wrong, please check and try again",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>Atributo: Código Rosetta</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "Project Euler",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "Certificación de {{cert}}",
"browse-other": "Navega por nuestras otras certificaciones gratuitas\n(recomendamos hacerlo en orden)",

View File

@ -11,12 +11,12 @@
"view": "Ver",
"view-code": "Mostrar Código",
"view-project": "Mostrar Proyecto",
"view-cert-title": "View {{certTitle}}",
"view-cert-title": "Ver {{certTitle}}",
"show-cert": "Mostrar certificación",
"claim-cert": "Solicitar certificación",
"save-progress": "Guardar progreso",
"accepted-honesty": "Has aceptado nuestra Política de Honestidad Académica.",
"agree": "Aceptar",
"agree-honesty": "Estoy de acuerdo con la Política de Honestidad Académica de freeCodeCamp.",
"save-portfolio": "Guardar este elemento de portafolio",
"remove-portfolio": "Eliminar este elemento de portafolio",
"add-portfolio": "Agregar un nuevo elemento de portafolio",
@ -53,7 +53,7 @@
"check-code": "Comprueba tu código (Ctrl + Enter)",
"check-code-2": "Comprueba tu código",
"reset": "Restablecer",
"reset-step": "Reset This Step",
"reset-step": "Restablecer este paso",
"help": "Ayuda",
"get-help": "Obtener ayuda",
"watch-video": "Ver un Video",
@ -159,7 +159,7 @@
"internet": "Tu presencia en Internet",
"portfolio": "Ajustes de portafolio",
"privacy": "Ajustes de privacidad",
"personal-info": "Personal Information"
"personal-info": "Información Personal"
},
"danger": {
"heading": "Zona de peligro",
@ -228,12 +228,12 @@
"page-number": "{{pageNumber}} de {{totalPages}}"
},
"footer": {
"tax-exempt-status": "freeCodeCamp is a donor-supported tax-exempt 501(c)(3) charitable organization (United States Federal Tax Identification Number: 82-0779546)",
"tax-exempt-status": "freeCodeCamp es una organización benéfica 501(c)(3) exenta de impuestos apoyada por donantes (Número de Identificación Fiscal Federal De Los Estados Unidos: 82-0779546)",
"mission-statement": "Nuestra misión: ayudar a las personas a aprender a programar de forma gratuita. Logramos esto mediante la creación de miles de videos, artículos y lecciones de programación interactivas, todos disponibles gratuitamente para el público. También tenemos miles de grupos de estudio de FreeCodeCamp en todo el mundo.",
"donation-initiatives": "Las donaciones a freeCodeCamp se destinan a nuestras iniciativas educativas y ayudan a pagar los servidores, los servicios y el personal.",
"donate-text": "Puedes <1>hacer una donación deducible de impuestos aquí</1>.",
"trending-guides": "Guías de tendencias",
"our-nonprofit": "Our Charity",
"our-nonprofit": "Nuestra Caridad",
"links": {
"about": "Acerca de",
"alumni": "Red de ex-Alumnos",
@ -274,9 +274,9 @@
"add-subtitles": "Ayudar a mejorar o agregar subtítulos",
"wrong-answer": "Lo siento, esa no es la respuesta correcta. ¡Vuelve a intentarlo!",
"check-answer": "Haz clic en el botón de abajo para verificar tu respuesta.",
"assignment-not-complete": "Please finish the assignments",
"assignments": "Assignments",
"question": "Question",
"assignment-not-complete": "Por favor, completa las tareas",
"assignments": "Asignaciones",
"question": "Pregunta",
"solution-link": "Enlace a la solución",
"github-link": "Enlace de GitHub",
"submit-and-go": "Enviar y pasar a mi siguiente desafío",
@ -302,7 +302,6 @@
"certs": "Certificación {{title}}"
},
"editor-tabs": {
"info": "Info",
"code": "Código",
"tests": "Pruebas",
"restart": "Reiniciar",
@ -336,15 +335,15 @@
"sorry-dont-giveup": "Lo sentimos, su código no pasa. No te rindas.",
"challenges-completed": "{{completedCount}} de {{totalChallenges}} desafíos completados",
"season-greetings-fcc": "Saludos de Temporada de la comunidad freeCodeCamp 🎉",
"if-getting-value": "If you're getting a lot out of freeCodeCamp, now is a great time to donate to support our charity's mission.",
"if-getting-value": "Si estás obteniendo mucho de freeCodeCamp, ahora es un buen momento para donar con el fin de apoyar nuestra misión sin fines de lucro.",
"building-a-university": "Estamos construyendo un programa gratuito de grado universitario en ciencias de la computación",
"if-help-university": "Ya hemos hecho un montón de progresos. Apoyamos nuestra caridad con el largo camino por delante."
},
"donate": {
"title": "Support our charity",
"title": "Apoya nuestra caridad",
"processing": "Estamos procesando tu donación.",
"redirecting": "Redirigiendo...",
"thanks": "Thanks for donating",
"thanks": "Gracias por donar",
"thank-you": "Gracias por tu apoyo.",
"additional": "Puede hacer una donación adicional única de cualquier monto utilizando este enlace: <0>{{url}}</0>",
"help-more": "Ayúdanos a hacer más",
@ -364,9 +363,9 @@
"your-donation-2": "Tu donación de ${{usd}} proporcionará {{hours}} horas de aprendizaje a personas de todo el mundo cada mes.",
"your-donation-3": "Tu donación de ${{usd}} proporcionará {{hours}} horas de aprendizaje a personas de todo el mundo cada año.",
"become-supporter": "Conviértete en un colaborador",
"duration": "Become a one-time supporter of our charity.",
"duration-2": "Become a monthly supporter of our charity.",
"duration-4": "Become a supporter of our charity",
"duration": "Se un benefactor de nuestra organización benéfica con una donación única.",
"duration-2": "Se un benefactor mensual de nuestra organización benéfica.",
"duration-4": "Se un benefactor de nuestra organización benéfica",
"nicely-done": "Bien hecho. Acabas de completar {{block}}.",
"credit-card": "Tarjeta de crédito",
"credit-card-2": "O dona con una tarjeta de crédito:",
@ -380,25 +379,25 @@
"email-receipt": "Correo electrónico (te enviaremos un recibo de donación deducible de impuestos):",
"need-help": "¿Necesitas ayuda con tus donaciones actuales o pasadas?",
"forward-receipt": "Envía una copia de tu recibo de donación a donors@freecodecamp.org y dinos cómo podemos ayudar.",
"efficiency": "freeCodeCamp is a highly efficient education charity.",
"efficiency": "freeCodeCamp es una organización benéfica educativa de gran eficacia.",
"why-donate-1": "Cuando donas a freeCodeCamp, ayudas a las personas a aprender nuevas habilidades y proveer para sus familias",
"why-donate-2": "También nos ayudas a crear nuevos recursos para que los utilices y amplíes tus propias habilidades tecnológicas.",
"bigger-donation": "¿Quieres hacer una donación más grande de una sola vez, envíanos un cheque o da de otras maneras?",
"other-ways": "Here are many <0>other ways you can support our charity's mission</0>.",
"other-ways": "Aquí hay muchas <0>otras formas de apoyar la misión de nuestra organización benéfica</0>.",
"failed-pay": "Oh no. Parece que tu transacción no se realizó. ¿Podrías intentarlo de nuevo?",
"try-again": "Por favor, intenta de nuevo.",
"card-number": "Tu número de tarjeta:",
"expiration": "Fecha de vencimiento:",
"secure-donation": "Donación segura",
"faq": "Preguntas frecuentes",
"only-you": "Only you can see this message. Congratulations on earning this certification. It's no easy task. Running freeCodeCamp isn't easy either. Nor is it cheap. Help us help you and many other people around the world. Make a tax-deductible supporting donation to our charity today.",
"only-you": "Sólo tu puedes ver este mensaje. Enhorabuena por haber obtenido esta certificación. No es tarea fácil. Administrar freeCodeCamp tampoco es fácil. Tampoco es barato. Ayúdanos a ayudarte a ti y a muchas otras personas de todo el mundo. Haz hoy una donación deducible de impuestos a nuestra organización benéfica.",
"get-help": "¿Cómo puedo obtener ayuda con mis donaciones?",
"how-transparent": "¿Qué tan transparente es freeCodeCamp.org?",
"very-transparent": "Muy Transparente. Tenemos una valoración de transparencia de platino en GuideStar.org.",
"download-irs": "Puedes <0>descargar nuestra Carta de Determinación de IRS aquí</0>.",
"download-990": "Puedes <0>descargar nuestro 990 más reciente (informe anual de impuestos) aquí</0>.",
"how-efficient": "¿Qué tan eficiente es freeCodeCamp?",
"fcc-budget": "freeCodeCamp's budget is much smaller than most comparable charity. We haven't brought in professional fundraisers. Instead, Quincy does everything himself.",
"fcc-budget": "El presupuesto de freeCodeCamp es mucho menor que el de la mayoría de organizaciones benéficas comparables. No hemos contratado a profesionales para recaudar fondos. En su lugar, Quincy lo hace todo él mismo.",
"help-millions": "Sin embargo, con un presupuesto de tan sólo unos cientos de miles de dólares al año, hemos podido ayudar a millones de personas.",
"how-one-time": "¿Cómo puedo realizar una donación única?",
"one-time": "Si prefieres hacer donaciones únicas, puedes apoyar la misión de freeCodeCamp cuando tengas dinero suficiente para hacer una aportación. Puedes usar <0>este enlace para donar cualquier cantidad que te sea cómodo a través de PayPal</0>.",
@ -518,7 +517,7 @@
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "Para reclamar una certificación, primero debes aceptar nuestra política de honestidad académica.",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "Sucedió algo realmente extraño. Si vuelve a ocurrir, considera hacer un reporte del problema en https://github.com/freeCodeCamp/freeCodeCamp/issues/new",
"not-right": "Algo no está bien. Se ha generado un informe y se ha notificado al equipo de freeCodeCamp.org",
"went-wrong": "Algo salió mal, verifica e intenta nuevamente",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>Attribut: Rosetta Code</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "Projekt Euler",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "{{cert}} Zertifikat",
"browse-other": "Stöbere in unseren anderen kostenlosen Zertifizierungen\n(Wir empfehlen, diese der Reihe nach zu erledigen)",

View File

@ -15,8 +15,8 @@
"show-cert": "Zertifikat anzeigen",
"claim-cert": "Zertifizierung anfordern",
"save-progress": "Fortschritt speichern",
"accepted-honesty": "Du hast unsere Akademische Ehrlichkeitsrichtlinie akzeptiert.",
"agree": "Zustimmen",
"accepted-honesty": "You have agreed to our Academic Honesty Policy.",
"agree-honesty": "I agree to freeCodeCamp's Academic Honesty Policy.",
"save-portfolio": "Dieses Portfolioelement speichern",
"remove-portfolio": "Dieses Portfolioelement entfernen",
"add-portfolio": "Neues Portfolioelement hinzufügen",
@ -302,7 +302,6 @@
"certs": "{{title}} Zertifizierung"
},
"editor-tabs": {
"info": "Informationen",
"code": "Code",
"tests": "Tests",
"restart": "Neustart",
@ -518,7 +517,7 @@
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "Um eine Zertifizierung zu erlangen, musst du zunächst unsere Richtlinie zur akademischen Ehrlichkeit akzeptieren",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "Etwas wirklich Seltsames ist passiert. Wenn es wieder passiert, erwäge bitte, einen Fehler auf https://github.com/freeCodeCamp/freeCodeCamp/issues/new zu melden.",
"not-right": "Irgendetwas ist nicht in Ordnung. Es wurde ein Bericht erstellt und das freeCodeCamp.org Team wurde benachrichtigt",
"went-wrong": "Etwas ist schief gelaufen, bitte überprüfe und versuche es erneut",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>Fonte: Codice Rosetta</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "Progetto Eulero",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "Certificazione {{cert}}",
"browse-other": "Sfoglia le altre nostre certificazioni gratuite\n(consigliamo di seguirle in ordine)",

View File

@ -15,8 +15,8 @@
"show-cert": "Mostra la Certificazione",
"claim-cert": "Richiedi la Certificazione",
"save-progress": "Salva l'avanzamento",
"accepted-honesty": "Hai accettato la nostra Politica di Onestà Accademica.",
"agree": "Accetta",
"accepted-honesty": "You have agreed to our Academic Honesty Policy.",
"agree-honesty": "I agree to freeCodeCamp's Academic Honesty Policy.",
"save-portfolio": "Salva questo elemento del portfolio",
"remove-portfolio": "Rimuovi questo elemento del portfolio",
"add-portfolio": "Aggiungi un nuovo elemento nel portfolio",
@ -302,7 +302,6 @@
"certs": "Certificazione {{title}}"
},
"editor-tabs": {
"info": "Informazioni",
"code": "Codice",
"tests": "Test",
"restart": "Inizia da capo",
@ -518,7 +517,7 @@
"opens-new-window": "Apri in una nuova finestra"
},
"flash": {
"honest-first": "Per richiedere una certificazione, è necessario prima accettare la nostra politica di onestà accademica",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "È successo qualcosa di veramente strano, se succede di nuovo, ti preghiamo di considerare di sollevare un problema su https://github.com/freeCodeCamp/freeCodeCamp/issues/new",
"not-right": "Qualcosa non è del tutto giusto. Un rapporto è stato generato e il team freeCodeCamp.org è stato avvisato",
"went-wrong": "Qualcosa è andato storto, controlla e riprova",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>作: Rosetta Code</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["A description is to be determined"]
},
"the-odin-project-projects": {
"title": "The Odin Project Projects",
"intro": ["A description is to be determined"]
},
"project-euler": {
"title": "プロジェクト・オイラー",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project is one of those \"What I wish I had when I was learning\" resources. ",
"Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway.",
"This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Learn HTML Foundations",
"intro": ["A description is to be determined"]
},
"top-build-a-recipe-project": {
"title": "Learn HTML Foundations by Building a Recipe Page",
"intro": ["A description is to be determined"]
}
}
},
"misc-text": {
"certification": "{{cert}} 認定",
"browse-other": "他の無料の認定講座を閲覧する\n(上から順に受講することをお勧めします)",

View File

@ -15,8 +15,8 @@
"show-cert": "認定証を表示",
"claim-cert": "認定証を取得",
"save-progress": "進行状況を保存",
"accepted-honesty": "学問的誠実性ポリシーに同意しました。",
"agree": "同意する",
"accepted-honesty": "You have agreed to our Academic Honesty Policy.",
"agree-honesty": "I agree to freeCodeCamp's Academic Honesty Policy.",
"save-portfolio": "このポートフォリオアイテムを保存",
"remove-portfolio": "このポートフォリオアイテムを削除",
"add-portfolio": "新規ポートフォリオアイテムを追加",
@ -302,7 +302,6 @@
"certs": "{{title}} 認定講座"
},
"editor-tabs": {
"info": "詳細",
"code": "コード",
"tests": "テスト",
"restart": "リスタート",
@ -518,7 +517,7 @@
"opens-new-window": "新しいウィンドウで開く"
},
"flash": {
"honest-first": "認定証を請求するには、まず学問的誠実性ポリシーに同意する必要があります。",
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
"really-weird": "予期しない問題が発生しました。この問題が何度も発生するようであれば、https://github.com/freeCodeCamp/freeCodeCamp/issues/new への Issue 登録をご検討ください。",
"not-right": "問題が発生しました。レポートが生成され、freeCodeCamp.org チームへ通知されました。",
"went-wrong": "問題が発生しました。ご確認の上もう一度お試しください。",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>Attribute: Rosetta Code</a>"
]
},
"the-odin-project": {
"title": "The Odin Project",
"intro": ["Uma descrição deve ser determinada"]
},
"the-odin-project-projects": {
"title": "Os projetos do The Odin Project",
"intro": ["Uma descrição deve ser determinada"]
},
"project-euler": {
"title": "Projeto Euler",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "The Odin Project",
"intro": [
"The Odin Project é um daqueles recursos do tipo \"O que eu gostaria de ter visto quando estava aprendendo\". ",
"Nem todas as pessoas têm acesso à educação em ciência da computação ou aos fundos necessários para frequentar uma escola de programação intensiva. De qualquer modo, não necessariamente, essas duas sejam a solução final para todos que queiram aprender.",
"Este projeto destina-se a preencher a lacuna para aquelas pessoas que tentam buscar suas próprias soluções, mas que continuam procurando uma educação de alta qualidade."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Aprenda o básico de HTML",
"intro": ["Uma descrição deve ser determinada"]
},
"top-build-a-recipe-project": {
"title": "Aprenda o básico de HTML criando uma página de receitas",
"intro": ["Uma descrição deve ser determinada"]
}
}
},
"misc-text": {
"certification": "Certificação {{cert}}",
"browse-other": "Navegue por nossas outras certificações gratuitas\n(recomendamos fazer isto em sequência)",

View File

@ -15,8 +15,8 @@
"show-cert": "Exibir certificado",
"claim-cert": "Solicitar certificação",
"save-progress": "Salvar progresso",
"accepted-honesty": "Você aceitou nossa política de honestidade acadêmica.",
"agree": "Aceitar",
"accepted-honesty": "Você concordou com nossa política de honestidade acadêmica.",
"agree-honesty": "Concordo com a Política de Honestidade Acadêmica do FreeCodeCamp.",
"save-portfolio": "Salvar esse item de portfólio",
"remove-portfolio": "Remover este item de portfólio",
"add-portfolio": "Adicionar um novo item de portfólio",
@ -302,7 +302,6 @@
"certs": "Certificação {{title}}"
},
"editor-tabs": {
"info": "Informações",
"code": "Código",
"tests": "Testes",
"restart": "Reiniciar",
@ -518,7 +517,7 @@
"opens-new-window": "Abre em uma nova janela"
},
"flash": {
"honest-first": "Para solicitar uma certificação, você precisa primeiro aceitar nossa política de honestidade acadêmica",
"honest-first": "Para solicitar uma certificação, você precisa primeiro concordar com nossa política de honestidade acadêmica",
"really-weird": "Algo realmente estranho aconteceu. Se acontecer novamente, considere apresentar um problema pelo endereço https://github.com/freeCodeCamp/freeCodeCamp/issues/new",
"not-right": "Algo não está certo. Um relatório foi gerado e a equipe do freeCodeCamp.org foi notificada",
"went-wrong": "Algo deu errado. Verifique e tente novamente",

View File

@ -778,14 +778,6 @@
"<a href='https://rosettacode.org/wiki/Rosetta_Code' target='_blank' rel='noopener noreferrer nofollow'>Атрибут: Rosetta Code</a>"
]
},
"the-odin-project": {
"title": "Проєкт «Odin»",
"intro": ["Опис буде надано пізніше"]
},
"the-odin-project-projects": {
"title": "Проєкти «Odin»",
"intro": ["Опис буде надано пізніше"]
},
"project-euler": {
"title": "Проєкт «Ейлер»",
"intro": [
@ -795,6 +787,24 @@
}
}
},
"the-odin-project": {
"title": "Проєкт «Odin»",
"intro": [
"Проєкт «Odin» є одним з тих ресурсів, про які хотіли б дізнатися ще коли навчались. ",
"Не кожен має доступ до технологічної освіти або коштів, необхідних для відвідування інтенсивної школи. Однак це не остаточне рішення для тих, хто хоче вчитися.",
"Цей проєкт розроблений, щоб заповнити прогалину для людей, які намагаються вчитись самостійно, але все ж таки хочуть високоякісну освіту."
],
"blocks": {
"top-learn-html-foundations": {
"title": "Вивчіть основи HTML",
"intro": ["Опис буде надано пізніше"]
},
"top-build-a-recipe-project": {
"title": "Вивчіть основи HTML, побудувавши сторінку з рецептами",
"intro": ["Опис буде надано пізніше"]
}
}
},
"misc-text": {
"certification": "Сертифікація «{{cert}}»",
"browse-other": "Перегляньте інші безоплатні сертифікації\n(ми рекомендуємо виконувати їх послідовно)",

View File

@ -15,8 +15,8 @@
"show-cert": "Показати сертифікацію",
"claim-cert": "Отримати сертифікацію",
"save-progress": "Зберегти прогрес",
"accepted-honesty": "Ви прийняли нашу Політику академічної доброчесності.",
"agree": "Прийняти",
"accepted-honesty": "Ви погодились з нашою політикою академічної доброчесності.",
"agree-honesty": "Я погоджуюсь з політикою академічної доброчесності freeCodeCamp.",
"save-portfolio": "Зберегти цей елемент портфоліо",
"remove-portfolio": "Видалити цей елемент портфоліо",
"add-portfolio": "Додати новий елемент портфоліо",
@ -302,7 +302,6 @@
"certs": "Сертифікація «{{title}}»"
},
"editor-tabs": {
"info": "Інформація",
"code": "Код",
"tests": "Тести",
"restart": "Перезапустити",
@ -518,7 +517,7 @@
"opens-new-window": "Відкривається у новому вікні"
},
"flash": {
"honest-first": "Щоб отримати сертифікацію, ви повинні спочатку прийняти нашу політику академічної доброчесності",
"honest-first": "Щоб отримати сертифікацію, ви повинні спочатку погодитись з нашою політикою академічної доброчесності",
"really-weird": "Щось пішло не так. Якщо це повториться, будь ласка, повідомте про це за посиланням: https://github.com/freeCodeCamp/freeCodeCamp/issues/new",
"not-right": "Щось пішло не так. Звіт було сформовано і команду freeCodeCamp.org вже сповістили.",
"went-wrong": "Щось пішло не так. Будь ласка, перевірте та повторіть спробу.",

View File

@ -215,7 +215,7 @@ const schemaValidation = (
if (
fileName === 'motivation' &&
!(fileJson.motivationalQuotes as MotivationalQuotes).every(
(object: object) =>
object =>
Object.prototype.hasOwnProperty.call(object, 'quote') &&
Object.prototype.hasOwnProperty.call(object, 'author')
)

View File

@ -39,16 +39,16 @@
"@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.18.6",
"@babel/standalone": "7.20.15",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-brands-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/fontawesome-svg-core": "6.3.0",
"@fortawesome/free-brands-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@freecodecamp/curriculum-helpers": "1.1.0",
"@freecodecamp/loop-protect": "3.0.0",
"@freecodecamp/react-bootstrap": "0.32.3",
"@freecodecamp/react-calendar-heatmap": "1.0.0",
"@freecodecamp/react-calendar-heatmap": "1.1.0",
"@freecodecamp/strip-comments": "3.0.1",
"@growthbook/growthbook-react": "0.11.1",
"@growthbook/growthbook-react": "0.11.2",
"@loadable/component": "5.15.3",
"@reach/router": "1.3.4",
"@sentry/gatsby": "6.19.7",
@ -133,8 +133,8 @@
},
"devDependencies": {
"@babel/types": "7.20.7",
"@codesee/babel-plugin-instrument": "0.494.0",
"@codesee/tracker": "0.494.0",
"@codesee/babel-plugin-instrument": "0.498.0",
"@codesee/tracker": "0.498.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@types/react-gtm-module": "2.0.1",

View File

@ -1,17 +1,18 @@
import React from 'react';
import { SuperBlocks } from '../../../../config/certification-settings';
import APIIcon from './API-icon';
import D3Icon from './D3-icon';
import DatabaseIcon from './Database-icon';
import JavaScriptIcon from './JavaScript-icon';
import ReactIcon from './React-icon';
import TensorflowIcon from './Tensorflow-icon';
import APIIcon from './api';
import D3Icon from './d3';
import DatabaseIcon from './database';
import JavaScriptIcon from './javascript';
import ReactIcon from './react';
import TensorflowIcon from './tensorflow';
import Algorithm from './algorithm';
import Analytics from './analytics';
import Clipboard from './clipboard';
import PythonIcon from './python-icon';
import PythonIcon from './python';
import ResponsiveDesign from './responsive-design';
import Shield from './shield';
import VikingHelmet from './viking-helmet';
const iconMap = {
[SuperBlocks.RespWebDesignNew]: ResponsiveDesign,
@ -27,7 +28,8 @@ const iconMap = {
[SuperBlocks.DataAnalysisPy]: Analytics,
[SuperBlocks.InfoSec]: Shield,
[SuperBlocks.MachineLearningPy]: TensorflowIcon,
[SuperBlocks.CodingInterviewPrep]: Algorithm
[SuperBlocks.CodingInterviewPrep]: Algorithm,
[SuperBlocks.TheOdinProject]: VikingHelmet
};
const generateIconComponent = (

View File

@ -0,0 +1,17 @@
import React from 'react';
function VikingHelmet(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
): JSX.Element {
return (
<>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' {...props}>
<path d='M52.441 53.88c-35.103 34.696-41.31 73.89-33.228 117.837 6.29 34.202 22.079 70.807 40.892 107.767 17.738-27.114 41.117-56.824 68.676-78.517-20.7-17.164-38.261-35.891-51.367-56.447-17.29-27.12-26.504-57.61-24.973-90.64zm407.118 0c1.531 33.03-7.683 63.52-24.973 90.64-13.106 20.556-30.667 39.283-51.367 56.447 27.559 21.693 50.938 51.403 68.676 78.517 18.813-36.96 34.603-73.565 40.892-107.767 8.082-43.947 1.875-83.141-33.228-117.836zM256 179c-8.702 0-17.061 2.757-23 7.316v22.38c6.7-2.648 14.535-4.016 23-4.016s16.3 1.368 23 4.015v-22.379c-5.939-4.559-14.298-7.316-23-7.316zm-41 30.053c-30.485 11.577-60.043 34.66-84.166 62.804C98.718 309.326 76.784 355.501 73.482 391H215V209.053zm82 0V391h141.518c-3.301-35.499-25.236-81.674-57.352-119.143-24.123-28.143-53.681-51.227-84.166-62.804zm-153.502 3.49c-29.097 22.175-55.189 56.212-73.732 85.506a2034.036 2034.036 0 0 0 9.447 17.562c10.162-19.226 23.088-38.126 37.953-55.468 11.983-13.98 25.289-26.965 39.557-38.155a416.25 416.25 0 0 1-13.225-9.445zm225.004 0a416.25 416.25 0 0 1-13.225 9.445c14.268 11.19 27.574 24.175 39.557 38.155 14.865 17.342 27.79 36.242 37.953 55.468 3.179-5.85 6.339-11.705 9.447-17.562-18.543-29.294-44.635-63.33-73.732-85.506zM256 222.68c-7.62 0-14.449 1.66-18.602 3.736-3.262 1.631-4.103 2.973-4.318 3.264.215.29 1.056 1.632 4.318 3.263 4.153 2.077 10.981 3.737 18.602 3.737 7.62 0 14.449-1.66 18.602-3.737 3.262-1.63 4.103-2.972 4.318-3.263-.215-.291-1.056-1.633-4.318-3.264-4.153-2.077-10.981-3.736-18.602-3.736zm22.92 7c.059.08.08.095.08 0 0-.096-.021-.08-.08 0zm-45.84 0c-.059-.08-.08-.096-.08 0 0 .095.021.08.08 0zm-.08 20.984v48.352c6.7-2.648 14.535-4.016 23-4.016s16.3 1.368 23 4.016v-48.352c-6.7 2.648-14.535 4.016-23 4.016s-16.3-1.368-23-4.016zM256 313c-7.62 0-14.449 1.66-18.602 3.736-3.262 1.632-4.103 2.973-4.318 3.264.215.291 1.056 1.632 4.318 3.264C241.551 325.34 248.38 327 256 327c7.62 0 14.449-1.66 18.602-3.736 3.262-1.632 4.103-2.973 4.318-3.264-.215-.291-1.056-1.632-4.318-3.264C270.449 314.66 263.62 313 256 313zm22.92 7c.059.08.08.096.08 0s-.021-.08-.08 0zm-45.84 0c-.059-.08-.08-.096-.08 0s.021.08.08 0zm-.08 20.984v45.87c6.7-2.649 14.535-4.016 23-4.016s16.3 1.367 23 4.016v-45.87c-6.7 2.648-14.535 4.016-23 4.016s-16.3-1.368-23-4.016zm23 59.854c-7.62 0-14.449 1.66-18.602 3.736-3.262 1.631-4.103 2.973-4.318 3.264.215.29 1.056 1.632 4.318 3.264 4.153 2.076 10.981 3.736 18.602 3.736 7.62 0 14.449-1.66 18.602-3.736 3.262-1.632 4.103-2.973 4.318-3.264-.215-.291-1.056-1.633-4.318-3.264-4.153-2.076-10.981-3.736-18.602-3.736zm22.92 7c.059.08.08.095.08 0 0-.096-.021-.08-.08 0zm-45.84 0c-.059-.08-.08-.096-.08 0 0 .095.021.08.08 0zM73 409v30h18.455c-2.78-4.422-4.455-9.52-4.455-15s1.676-10.578 4.455-15H73zm55 0c-7.013 0-13.194 2.204-17.227 5.229C106.74 417.253 105 420.615 105 424c0 3.385 1.74 6.747 5.773 9.771C114.806 436.796 120.987 439 128 439s13.194-2.204 17.227-5.229C149.26 430.747 151 427.385 151 424c0-3.385-1.74-6.747-5.773-9.771C141.194 411.204 135.013 409 128 409zm36.545 0c2.78 4.422 4.455 9.52 4.455 15s-1.676 10.578-4.455 15H215v-30h-50.455zM297 409v30h50.455c-2.78-4.422-4.455-9.52-4.455-15s1.676-10.578 4.455-15H297zm87 0c-7.013 0-13.194 2.204-17.227 5.229C362.74 417.253 361 420.615 361 424c0 3.385 1.74 6.747 5.773 9.771C370.806 436.796 376.987 439 384 439s13.194-2.204 17.227-5.229C405.26 430.747 407 427.385 407 424c0-3.385-1.74-6.747-5.773-9.771C397.194 411.204 391.013 409 384 409zm36.545 0c2.78 4.422 4.455 9.52 4.455 15s-1.676 10.578-4.455 15H439v-30h-18.455zM233 428.822v16.453l23 34.5 23-34.5v-16.453c-6.7 2.648-14.535 4.016-23 4.016s-16.3-1.368-23-4.016z' />
</svg>
</>
);
}
VikingHelmet.displayName = 'VikingHelmet';
export default VikingHelmet;

View File

@ -9,7 +9,7 @@ import { createSelector } from 'reselect';
import envData from '../../../config/env.json';
import { getLangCode } from '../../../config/i18n';
import FreeCodeCampLogo from '../assets/icons/FreeCodeCamp-logo';
import FreeCodeCampLogo from '../assets/icons/freecodecamp';
import DonateForm from '../components/Donation/donate-form';
import { createFlashMessage } from '../components/Flash/redux';

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FreeCodeCampLogo from '../../../assets/icons/FreeCodeCamp-logo';
import FreeCodeCampLogo from '../../../assets/icons/freecodecamp';
const NavLogo = (): JSX.Element => {
const { t } = useTranslation();

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { createStore } from '../../redux/createStore';
import { createStore } from '../../redux/create-store';
import Intro from '.';

View File

@ -9,7 +9,7 @@ type Props = {
};
type Solution = Pick<ChallengeFile, 'ext' | 'contents' | 'fileKey'>;
function SolutionViewer({ challengeFiles, solution }: Props) {
function SolutionViewer({ challengeFiles, solution }: Props): JSX.Element {
const isLegacy = !challengeFiles || !challengeFiles.length;
const solutions = isLegacy
? [

View File

@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
import { i18nextCodes } from '../../../config/i18n';
import i18nTestConfig from '../../i18n/config-for-tests';
import { createStore } from '../redux/createStore';
import { createStore } from '../redux/create-store';
import AppMountNotifier from './app-mount-notifier';
jest.unmock('react-i18next');

View File

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BlockSaveWrapper /> snapshot 1`] = `
<div>
<div
style="padding: 0px 15px;"
/>
</div>
`;

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react';
import React from 'react';
import BlockSaveWrapper from './block-save-wrapper';
test('<BlockSaveWrapper /> snapshot', () => {
const { container } = render(<BlockSaveWrapper />);
expect(container).toMatchSnapshot();
});

View File

@ -1,17 +0,0 @@
import React from 'react';
const style = {
padding: '0 15px'
};
function BlockSaveWrapper({
children
}: {
children?: React.ReactElement | null;
}): JSX.Element {
return <div style={style}>{children}</div>;
}
BlockSaveWrapper.displayName = 'BlockSaveWrapper';
export default BlockSaveWrapper;

View File

@ -1,6 +1,5 @@
import {
Alert,
Col,
ControlLabel,
FormControl,
FormGroup,
@ -80,7 +79,7 @@ function FormFields(props: FormFieldsProps): JSX.Element {
) : null;
};
return (
<div>
<>
{formFields
.filter(formField => !ignored.includes(formField.name))
.map(({ name, label }) => (
@ -93,35 +92,33 @@ function FormFields(props: FormFieldsProps): JSX.Element {
name in placeholders ? placeholders[name] : '';
const isURL = types[name] === 'url';
return (
<Col key={key} xs={12}>
<FormGroup>
{type === 'hidden' ? null : (
<ControlLabel htmlFor={key}>{label}</ControlLabel>
)}
<FormControl
componentClass={type === 'textarea' ? type : 'input'}
id={key}
name={name}
onChange={onChange}
placeholder={placeholder}
required={required.includes(name)}
rows={4}
type={type}
value={value as string}
/>
{nullOrWarning(
value as string,
!pristine && error,
isURL,
name
)}
</FormGroup>
</Col>
<FormGroup key={key}>
{type === 'hidden' ? null : (
<ControlLabel htmlFor={key}>{label}</ControlLabel>
)}
<FormControl
componentClass={type === 'textarea' ? type : 'input'}
id={key}
name={name}
onChange={onChange}
placeholder={placeholder}
required={required.includes(name)}
rows={4}
type={type}
value={value as string}
/>
{nullOrWarning(
value as string,
!pristine && error,
isURL,
name
)}
</FormGroup>
);
}}
</Field>
))}
</div>
</>
);
}

View File

@ -12,7 +12,6 @@ import {
import FormFields, { FormOptions } from './form-fields';
import { default as BlockSaveButton } from './block-save-button';
import { default as BlockSaveWrapper } from './block-save-wrapper';
type URLValues = {
[key: string]: string;
@ -105,15 +104,13 @@ function DynamicForm({
style={{ width: '100%' }}
>
<FormFields formFields={formFields} options={options} />
<BlockSaveWrapper>
{hideButton ? null : (
<BlockSaveButton
disabled={(pristine && !enableSubmit) || (error as boolean)}
>
{buttonText ? buttonText : null}
</BlockSaveButton>
)}
</BlockSaveWrapper>
{hideButton ? null : (
<BlockSaveButton
disabled={(pristine && !enableSubmit) || (error as boolean)}
>
{buttonText ? buttonText : null}
</BlockSaveButton>
)}
</form>
)}
</Form>

View File

@ -1,5 +1,4 @@
import { default as Form, ValidatedValues } from './form';
export { default as BlockSaveButton } from './block-save-button';
export { default as BlockSaveWrapper } from './block-save-wrapper';
export { Form, ValidatedValues };

View File

@ -6,15 +6,16 @@ interface FullWidthRowProps {
className?: string;
}
const FullWidthRow = ({ children, className }: FullWidthRowProps) => {
return (
<Row className={className}>
<Col sm={8} smOffset={2} xs={12}>
{children}
</Col>
</Row>
);
};
const FullWidthRow = ({
children,
className
}: FullWidthRowProps): JSX.Element => (
<Row className={className}>
<Col sm={8} smOffset={2} xs={12}>
{children}
</Col>
</Row>
);
FullWidthRow.displayName = 'FullWidthRow';

View File

@ -1,10 +1,10 @@
import React, { Component, ReactNode } from 'react';
import React, { ReactNode, useEffect } from 'react';
import Helmet from 'react-helmet';
import { TFunction, withTranslation } from 'react-i18next';
// import TagManager from 'react-gtm-module';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { useStaticQuery, graphql } from 'gatsby';
import latoBoldURL from '../../../static/fonts/lato/Lato-Bold.woff';
import latoLightURL from '../../../static/fonts/lato/Lato-Light.woff';
@ -17,7 +17,8 @@ import { isBrowser } from '../../../utils';
import {
fetchUser,
onlineStatusChange,
serverStatusChange
serverStatusChange,
updateAllChallengesInfo
} from '../../redux/actions';
import {
isSignedInSelector,
@ -26,7 +27,13 @@ import {
isServerOnlineSelector,
userFetchStateSelector
} from '../../redux/selectors';
import { UserFetchState, User } from '../../redux/prop-types';
import {
UserFetchState,
User,
AllChallengeNode,
CertificateNode
} from '../../redux/prop-types';
import BreadCrumb from '../../templates/Challenges/components/bread-crumb';
import Flash from '../Flash';
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
@ -41,6 +48,7 @@ import './fonts.css';
import './global.css';
import './variables.css';
import './rtl-layout.css';
import { Themes } from '../settings/theme';
const mapStateToProps = createSelector(
isSignedInSelector,
@ -76,7 +84,8 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
fetchUser,
removeFlashMessage,
onlineStatusChange,
serverStatusChange
serverStatusChange,
updateAllChallengesInfo
},
dispatch
);
@ -100,55 +109,54 @@ const getSystemTheme = () =>
: 'light-palette'
}`;
class DefaultLayout extends Component<DefaultLayoutProps> {
static displayName = 'DefaultLayout';
componentDidMount() {
const { isSignedIn, fetchUser } = this.props;
function DefaultLayout({
children,
hasMessage,
fetchState,
flashMessage,
isOnline,
isServerOnline,
isSignedIn,
removeFlashMessage,
showFooter = true,
isChallenge = false,
block,
superBlock,
t,
theme = Themes.Default,
user,
fetchUser,
updateAllChallengesInfo
}: DefaultLayoutProps): JSX.Element {
const { challengeEdges, certificateNodes } = useGetAllBlockIds();
useEffect(() => {
// componentDidMount
updateAllChallengesInfo({ challengeEdges, certificateNodes });
if (!isSignedIn) {
fetchUser();
}
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
componentWillUnmount() {
window.removeEventListener('online', this.updateOnlineStatus);
window.removeEventListener('offline', this.updateOnlineStatus);
}
return () => {
// componentWillUnmount.
window.removeEventListener('online', updateOnlineStatus);
window.removeEventListener('offline', updateOnlineStatus);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
updateOnlineStatus = () => {
const { onlineStatusChange } = this.props;
const updateOnlineStatus = () => {
const isOnline =
isBrowser() && 'navigator' in window ? window.navigator.onLine : null;
return typeof isOnline === 'boolean' ? onlineStatusChange(isOnline) : null;
};
render() {
const {
children,
hasMessage,
fetchState,
flashMessage,
isOnline,
isServerOnline,
isSignedIn,
removeFlashMessage,
showFooter = true,
isChallenge = false,
block,
superBlock,
t,
theme = 'default',
user
} = this.props;
const useSystemTheme = fetchState.complete && isSignedIn === false;
if (fetchState.pending) {
return <Loader fullScreen={true} messageDelay={5000} />;
}
const useSystemTheme = fetchState.complete && isSignedIn === false;
if (fetchState.pending) {
return <Loader fullScreen={true} messageDelay={5000} />;
} else {
return (
<div className='page-wrapper'>
<Helmet
@ -240,6 +248,52 @@ class DefaultLayout extends Component<DefaultLayoutProps> {
}
}
// TODO: get challenge nodes directly rather than wrapped in edges
const useGetAllBlockIds = () => {
const {
allChallengeNode: { edges: challengeEdges },
allCertificateNode: { nodes: certificateNodes }
}: {
allChallengeNode: AllChallengeNode;
allCertificateNode: { nodes: CertificateNode[] };
} = useStaticQuery(graphql`
query getBlockNode {
allChallengeNode(
sort: {
fields: [
challenge___superOrder
challenge___order
challenge___challengeOrder
]
}
) {
edges {
node {
challenge {
block
id
}
}
}
}
allCertificateNode {
nodes {
challenge {
certification
tests {
id
}
}
}
}
}
`);
return { challengeEdges, certificateNodes };
};
DefaultLayout.displayName = 'DefaultLayout';
export default connect(
mapStateToProps,
mapDispatchToProps

View File

@ -232,7 +232,7 @@ exports[`<Profile/> renders correctly 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"
d="M562.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L405.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C189.5 251.2 196 330 246 380c56.5 56.5 148 56.5 204.5 0L562.8 267.7zM43.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C57 372 57 321 88.5 289.5L200.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C416.5 260.8 410 182 360 132c-56.5-56.5-148-56.5-204.5 0L43.2 244.3z"
fill="currentColor"
/>
</svg>

View File

@ -4,7 +4,7 @@ import { useStaticQuery } from 'gatsby';
import React from 'react';
import { render, screen } from '../../../../utils/test-utils';
import { createStore } from '../../../redux/createStore';
import { createStore } from '../../../redux/create-store';
import TimeLine from './time-line';
const store = createStore();

View File

@ -14,7 +14,7 @@ import {
getTitleFromId
} from '../../../../../utils';
import { regeneratePathAndHistory } from '../../../../../utils/polyvinyl';
import CertificationIcon from '../../../assets/icons/certification-icon';
import CertificationIcon from '../../../assets/icons/certification';
import { CompletedChallenge } from '../../../redux/prop-types';
import ProjectPreviewModal from '../../../templates/Challenges/components/project-preview-modal';
import { openModal } from '../../../templates/Challenges/redux/actions';

View File

@ -3,7 +3,7 @@ import React from 'react';
import renderer from 'react-test-renderer';
import { Provider } from 'react-redux';
import { createStore } from '../../../redux/createStore';
import { createStore } from '../../../redux/create-store';
import completedChallenges from '../../../__mocks__/completed-challenges.json';
import Timeline from './time-line';

View File

@ -1,7 +1,7 @@
import React, { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import Magnifier from '../../../assets/icons/Magnifier';
import InputReset from '../../../assets/icons/inputReset';
import Magnifier from '../../../assets/icons/magnifier';
import InputReset from '../../../assets/icons/input-reset';
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
type Props = {

View File

@ -17,10 +17,23 @@ interface SiteData {
};
}
interface Item {
'@type': 'Course';
url: string;
name: string;
description?: string;
provider: {
'@type': 'Organization';
name: string;
sameAs: string;
nonprofitStatus: string;
};
}
interface ListItem {
'@type': 'ListItem';
position: number;
item: object;
item: Item;
}
interface StructuredData {

View File

@ -2,7 +2,6 @@
exports[`<Honesty /> <Honesty /> snapshot when isHonest is false: Honesty 1`] = `
<section
className="honesty-policy"
id="honesty-policy"
>
<SectionHeader>
@ -14,16 +13,16 @@ exports[`<Honesty /> <Honesty /> snapshot when isHonest is false: Honesty 1`] =
>
<HonestyPolicy />
</Uncontrolled(Panel)>
<br />
<Button
active={false}
aria-disabled={false}
block={true}
bsClass="btn"
bsStyle="primary"
disabled={false}
onClick={[Function]}
>
buttons.agree
buttons.agree-honesty
</Button>
</FullWidthRow>
</section>
@ -31,7 +30,6 @@ exports[`<Honesty /> <Honesty /> snapshot when isHonest is false: Honesty 1`] =
exports[`<Honesty /> <Honesty /> snapshot when isHonest is true: HonestyAccepted 1`] = `
<section
className="honesty-policy"
id="honesty-policy"
>
<SectionHeader>
@ -43,18 +41,16 @@ exports[`<Honesty /> <Honesty /> snapshot when isHonest is true: HonestyAccepted
>
<HonestyPolicy />
</Uncontrolled(Panel)>
<br />
<Button
active={false}
aria-disabled={true}
block={true}
bsClass="btn"
bsStyle="primary"
className="disabled-agreed"
disabled={true}
disabled={false}
onClick={[Function]}
>
<p>
buttons.accepted-honesty
</p>
buttons.accepted-honesty
</Button>
</FullWidthRow>
</section>

View File

@ -48,6 +48,18 @@ type AboutState = {
isPictureUrlValid: boolean;
};
const ShowImageValidationWarning = ({
alertContent
}: {
alertContent: string;
}) => {
return (
<HelpBlock>
<Alert bsStyle='info'>{alertContent}</Alert>
</HelpBlock>
);
};
class AboutSettings extends Component<AboutProps, AboutState> {
validationImage: HTMLImageElement;
static displayName: string;
@ -79,7 +91,6 @@ class AboutSettings extends Component<AboutProps, AboutState> {
picture === formValues.picture &&
about === formValues.about
) {
// eslint-disable-next-line react/no-did-update-set-state
return this.setState({
originalValues: {
name,
@ -170,21 +181,6 @@ class AboutSettings extends Component<AboutProps, AboutState> {
}));
};
showImageValidationWarning = () => {
const { t } = this.props;
if (this.state.isPictureUrlValid === false) {
return (
<HelpBlock>
<Alert bsStyle='info' closeLabel={t('buttons.close')}>
{t('validation.url-not-image')}
</Alert>
</HelpBlock>
);
} else {
return true;
}
};
handleAboutChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
@ -210,9 +206,9 @@ class AboutSettings extends Component<AboutProps, AboutState> {
toggleKeyboardShortcuts
} = this.props;
return (
<div className='about-settings'>
<>
<UsernameSettings username={username} />
<br />
<Spacer />
<SectionHeader>{t('settings.headings.personal-info')}</SectionHeader>
<FullWidthRow>
<form id='camper-identity' onSubmit={this.handleSubmit}>
@ -246,7 +242,11 @@ class AboutSettings extends Component<AboutProps, AboutState> {
type='url'
value={picture}
/>
{this.showImageValidationWarning()}
{!this.state.isPictureUrlValid && (
<ShowImageValidationWarning
alertContent={t('validation.url-not-image')}
/>
)}
</FormGroup>
<FormGroup controlId='about-about'>
<ControlLabel>
@ -279,7 +279,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
/>
</FullWidthRow>
</div>
</>
);
}
}

View File

@ -2,7 +2,7 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from '../../redux/createStore';
import { createStore } from '../../redux/create-store';
import { CertificationSettings } from './certification';

View File

@ -1,3 +1,14 @@
#honesty-policy
:is(
button[aria-disabled='true'],
button[aria-disabled='true']:is(:focus, :hover)
) {
background-color: var(--quaternary-background);
color: var(--secondary-color);
opacity: 0.65;
cursor: not-allowed;
}
.honesty-panel p {
margin-inline: 10px;
font-family: 'Lato', sans-serif;
@ -7,11 +18,6 @@
padding-top: 15px;
}
.honesty-policy .disabled-agreed p {
margin-top: 0;
margin-bottom: 0;
}
.honesty-panel .btn-invert {
color: var(--primary-background);
}

View File

@ -15,33 +15,25 @@ type HonestyProps = {
const Honesty = ({ isHonest, updateIsHonest }: HonestyProps): JSX.Element => {
const { t } = useTranslation();
const button = isHonest ? (
<Button
block={true}
bsStyle='primary'
className='disabled-agreed'
disabled={true}
>
<p>{t('buttons.accepted-honesty')}</p>
</Button>
) : (
<Button
block={true}
bsStyle='primary'
onClick={() => updateIsHonest({ isHonest: true })}
>
{t('buttons.agree')}
</Button>
);
const buttonText = isHonest
? t('buttons.accepted-honesty')
: t('buttons.agree-honesty');
return (
<section className='honesty-policy' id='honesty-policy'>
<section id='honesty-policy'>
<SectionHeader>{t('settings.headings.honesty')}</SectionHeader>
<FullWidthRow>
<Panel className='honesty-panel'>
<HonestyPolicy />
</Panel>
<br />
{button}
<Button
block={true}
bsStyle='primary'
aria-disabled={isHonest}
onClick={() => !isHonest && updateIsHonest({ isHonest: true })}
>
{buttonText}
</Button>
</FullWidthRow>
</section>
);

View File

@ -22,7 +22,7 @@ export function SolutionDisplayWidget({
showUserCode,
showProjectPreview,
displayContext
}: Props) {
}: Props): JSX.Element | null {
const { id, solution, githubLink } = completedChallenge;
const { t } = useTranslation();
const viewText = t('buttons.view');

View File

@ -0,0 +1,9 @@
---
title: The Odin Project
superBlock: the-odin-project
certification: the-odin-project
---
## The Odin project
The Odin Project is one of those "What I wish I had when I was learning" resources. Not everyone has access to a computer science education or the funds to attend an intensive coding school and neither of those is right for everyone anyway. This project is designed to fill in the gap for people who are trying to hack it on their own but still want a high quality education.

View File

@ -0,0 +1,9 @@
---
title: The Odin Project
superBlock: the-odin-project
certification: the-odin-project
---
## The Odin project
Description is to be determined

View File

@ -0,0 +1,9 @@
---
title: The Odin Project
superBlock: the-odin-project
certification: the-odin-project
---
## The Odin project
Description is to be determined

View File

@ -27,6 +27,7 @@ export const actionTypes = createTypes(
'updateDonationFormState',
'updateUserToken',
'postChargeProcessing',
'updateAllChallengesInfo',
...createAsyncTypes('fetchUser'),
...createAsyncTypes('postCharge'),
...createAsyncTypes('fetchProfileForUser'),

View File

@ -53,6 +53,10 @@ export const fetchUser = createAction(actionTypes.fetchUser);
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
export const fetchUserError = createAction(actionTypes.fetchUserError);
export const updateAllChallengesInfo = createAction(
actionTypes.updateAllChallengesInfo
);
export const postCharge = createAction(actionTypes.postCharge);
export const postChargeProcessing = createAction(
actionTypes.postChargeProcessing

View File

@ -5,9 +5,9 @@ import createSagaMiddleware from 'redux-saga';
import envData from '../../../config/env.json';
import { isBrowser } from '../../utils';
import rootEpic from './rootEpic';
import rootReducer from './rootReducer';
import rootSaga from './rootSaga';
import rootEpic from './root-epic';
import rootReducer from './root-reducer';
import rootSaga from './root-saga';
const { environment } = envData;
@ -47,8 +47,8 @@ export const createStore = () => {
epicMiddleware.run(rootEpic);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('./rootReducer', () => {
const nextRootReducer = require('./rootReducer');
module.hot.accept('./root-reducer', () => {
const nextRootReducer = require('./root-reducer');
store.replaceReducer(nextRootReducer);
});
}

View File

@ -7,7 +7,7 @@ import {
fetchUserComplete,
fetchUserError
} from './actions';
import { jwt } from './cookieValues';
import { jwt } from './cookie-values';
function* fetchSessionUser() {
if (!jwt) {

View File

@ -56,6 +56,10 @@ const initialState = {
userFetchState: {
...defaultFetchState
},
allChallengesInfo: {
challengeEdges: [],
certificateNodes: []
},
userProfileFetchState: {
...defaultFetchState
},
@ -153,7 +157,10 @@ export const reducer = handleActions(
...state,
donationFormState: { ...defaultDonationFormState, error: payload }
}),
[actionTypes.updateAllChallengesInfo]: (state, { payload }) => ({
...state,
allChallengesInfo: { ...payload }
}),
[actionTypes.fetchUser]: state => ({
...state,
userFetchState: { ...defaultFetchState }

View File

@ -142,6 +142,19 @@ export type ChallengeNode = {
};
};
export type CertificateNode = {
challenge: {
// TODO: use enum
certification: string;
tests: { id: string }[];
};
};
export type AllChallengesInfo = {
challengeEdges: { node: ChallengeNode }[];
certificateNodes: CertificateNode[];
};
export type AllChallengeNode = {
edges: [
{

View File

@ -1,7 +1,7 @@
import { combineEpics } from 'redux-observable';
import { epics as challengeEpics } from '../templates/Challenges/redux';
import { epics as appEpics } from './';
import { epics as appEpics } from '.';
const rootEpic = combineEpics(...appEpics, ...challengeEpics);

View File

@ -16,7 +16,7 @@ import {
import { ns as appNameSpace } from './action-types';
import { ns as settingsNameSpace, reducer as settings } from './settings';
import { FlashApp as flashNameSpace } from './types';
import { reducer as app } from './';
import { reducer as app } from '.';
export default combineReducers({
[appNameSpace]: app,

View File

@ -3,7 +3,7 @@ import { all } from 'redux-saga/effects';
import { sagas as challengeSagas } from '../templates/Challenges/redux';
import errorSagas from './error-saga';
import { sagas as settingsSagas } from './settings';
import { sagas as appSagas } from './';
import { sagas as appSagas } from '.';
export default function* rootSaga() {
yield all([...errorSagas, ...appSagas, ...challengeSagas, ...settingsSagas]);

View File

@ -202,6 +202,8 @@ export const certificatesByNameSelector = username => state => {
};
export const userFetchStateSelector = state => state[MainApp].userFetchState;
export const allChallengesInfoSelector = state =>
state[MainApp].allChallengesInfo;
export const userProfileFetchStateSelector = state =>
state[MainApp].userProfileFetchState;
export const usernameSelector = state => state[MainApp].appUsername;

View File

@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next';
const HonestyPolicy = (): JSX.Element => {
const { t } = useTranslation();
const email = 'team@freecodecamp.org';
const email = 'support@freecodecamp.org';
return (
<>

View File

@ -39,17 +39,17 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
currentTab: this.props.hasEditableBoundaries ? Tab.Editor : Tab.Instructions
};
switchTab = (tab: Tab) => {
switchTab = (tab: Tab): void => {
this.setState({
currentTab: tab
});
};
handleKeyDown = () => this.props.updateUsingKeyboardInTablist(true);
handleKeyDown = (): void => this.props.updateUsingKeyboardInTablist(true);
handleClick = () => this.props.updateUsingKeyboardInTablist(false);
handleClick = (): void => this.props.updateUsingKeyboardInTablist(false);
render() {
render(): JSX.Element {
const { currentTab } = this.state;
const {
hasEditableBoundaries,
@ -87,7 +87,7 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
{!hasEditableBoundaries && (
<TabPane
eventKey={Tab.Instructions}
title={i18next.t('learn.editor-tabs.info')}
title={i18next.t('learn.editor-tabs.instructions')}
tabIndex={0}
>
{instructions}
@ -103,7 +103,7 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
</TabPane>
<TabPane
eventKey={Tab.Console}
title={i18next.t('learn.editor-tabs.tests')}
title={i18next.t('learn.editor-tabs.console')}
{...editorTabPaneProps}
>
{testOutput}

View File

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby';
import { noop } from 'lodash-es';
import React, { Component } from 'react';
import { TFunction, withTranslation } from 'react-i18next';
@ -13,8 +12,11 @@ import { dasherize } from '../../../../../utils/slugs';
import { isFinalProject } from '../../../../utils/challenge-types';
import Login from '../../../components/Header/components/Login';
import { executeGA, allowBlockDonationRequests } from '../../../redux/actions';
import { isSignedInSelector } from '../../../redux/selectors';
import { AllChallengeNode, ChallengeFiles } from '../../../redux/prop-types';
import {
isSignedInSelector,
allChallengesInfoSelector
} from '../../../redux/selectors';
import { AllChallengesInfo, ChallengeFiles } from '../../../redux/prop-types';
import { closeModal, submitChallenge } from '../redux/actions';
import {
@ -34,6 +36,7 @@ const mapStateToProps = createSelector(
completedChallengesIds,
isCompletionModalOpenSelector,
isSignedInSelector,
allChallengesInfoSelector,
successMessageSelector,
(
challengeFiles: ChallengeFiles,
@ -45,6 +48,7 @@ const mapStateToProps = createSelector(
completedChallengesIds: string[],
isOpen: boolean,
isSignedIn: boolean,
allChallengesInfo: AllChallengesInfo,
message: string
) => ({
challengeFiles,
@ -54,6 +58,7 @@ const mapStateToProps = createSelector(
completedChallengesIds,
isOpen,
isSignedIn,
allChallengesInfo,
message
})
);
@ -118,6 +123,7 @@ interface CompletionModalsProps {
id: string;
isOpen: boolean;
isSignedIn: boolean;
allChallengesInfo: AllChallengesInfo;
message: string;
submitChallenge: () => void;
superBlock: string;
@ -324,58 +330,13 @@ interface Options {
isFinalProjectBlock: boolean;
}
interface CertificateNode {
challenge: {
// TODO: use enum
certification: string;
tests: { id: string }[];
};
}
const useCurrentBlockIds = (
allChallengesInfo: AllChallengesInfo,
block: string,
certification: string,
options?: Options
) => {
const {
allChallengeNode: { edges: challengeEdges },
allCertificateNode: { nodes: certificateNodes }
}: {
allChallengeNode: AllChallengeNode;
allCertificateNode: { nodes: CertificateNode[] };
} = useStaticQuery(graphql`
query getCurrentBlockNodes {
allChallengeNode(
sort: {
fields: [
challenge___superOrder
challenge___order
challenge___challengeOrder
]
}
) {
edges {
node {
challenge {
block
id
}
}
}
}
allCertificateNode {
nodes {
challenge {
certification
tests {
id
}
}
}
}
}
`);
const { challengeEdges, certificateNodes } = allChallengesInfo;
const currentCertificateIds = certificateNodes
.filter(
node => dasherize(node.challenge.certification) === certification
@ -390,6 +351,7 @@ const useCurrentBlockIds = (
const CompletionModal = (props: CompletionModalsProps) => {
const currentBlockIds = useCurrentBlockIds(
props.allChallengesInfo,
props.block || '',
props.certification || '',
// eslint-disable-next-line @typescript-eslint/no-unsafe-call

View File

@ -6,8 +6,6 @@ import { bindActionCreators, Dispatch } from 'redux';
import { openModal } from '../redux/actions';
import './tool-panel.css';
const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch: Dispatch) =>
@ -30,7 +28,7 @@ function ToolPanel({
t
}: ToolPanelProps): JSX.Element {
return (
<div className='tool-panel-group project-tool-panel'>
<>
{guideUrl && (
<Button
block={true}
@ -50,7 +48,7 @@ function ToolPanel({
>
{t('buttons.ask-for-help')}
</Button>
</div>
</>
);
}

View File

@ -10,7 +10,7 @@ export function concatHtml({
required = [],
template,
contents
}: ConcatHTMLOptions) {
}: ConcatHTMLOptions): string {
const embedSource = template
? _template(template)
: ({ source }: { source: ConcatHTMLOptions['contents'] }) => source;

View File

@ -107,11 +107,13 @@ const buildFunctions = {
[challengeTypes.multifileCertProject]: buildDOMChallenge
};
export function canBuildChallenge(challengeData: BuildChallengeData) {
export function canBuildChallenge(challengeData: BuildChallengeData): boolean {
const { challengeType } = challengeData;
return Object.prototype.hasOwnProperty.call(buildFunctions, challengeType);
}
// TODO: Figure out and (hopefully) simplify the return type.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function buildChallenge(
challengeData: BuildChallengeData,
options: BuildOptions
@ -131,6 +133,8 @@ const testRunners = {
[challengeTypes.pythonProject]: getDOMTestRunner,
[challengeTypes.multifileCertProject]: getDOMTestRunner
};
// TODO: Figure out and (hopefully) simplify the return type.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getTestRunner(
buildData: BuildChallengeData,
runnerConfig: TestRunnerConfig,
@ -185,10 +189,16 @@ async function getDOMTestRunner(
runTestInTestFrame(document, testString, testTimeout);
}
type BuildResult = {
challengeType: number;
build: string;
sources: Source | undefined;
};
export function buildDOMChallenge(
{ challengeFiles, required = [], template = '' }: BuildChallengeData,
{ usesTestRunner } = { usesTestRunner: false }
) {
): Promise<BuildResult> | undefined {
const finalRequires = [...required];
if (usesTestRunner) finalRequires.push(...frameRunner);
@ -225,7 +235,7 @@ export function buildDOMChallenge(
export function buildJSChallenge(
{ challengeFiles }: { challengeFiles: ChallengeFiles },
options: BuildOptions
) {
): Promise<BuildResult> | undefined {
const pipeLine = composeFunctions(...getTransformers(options));
const finalFiles = challengeFiles?.map(pipeLine);
@ -262,7 +272,7 @@ export function updatePreview(
buildData: BuildChallengeData,
document: Document,
proxyLogger: ProxyLogger
) {
): void {
if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multifileCertProject
@ -293,7 +303,7 @@ function getDocumentTitle(buildData: BuildChallengeData) {
export function updateProjectPreview(
buildData: BuildChallengeData,
document: Document
) {
): void {
if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multifileCertProject
@ -309,7 +319,7 @@ export function updateProjectPreview(
}
}
export function challengeHasPreview({ challengeType }: ChallengeMeta) {
export function challengeHasPreview({ challengeType }: ChallengeMeta): boolean {
return (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern ||
@ -317,13 +327,15 @@ export function challengeHasPreview({ challengeType }: ChallengeMeta) {
);
}
export function isJavaScriptChallenge({ challengeType }: ChallengeMeta) {
export function isJavaScriptChallenge({
challengeType
}: ChallengeMeta): boolean {
return (
challengeType === challengeTypes.js ||
challengeType === challengeTypes.jsProject
);
}
export function isLoopProtected(challengeMeta: ChallengeMeta) {
export function isLoopProtected(challengeMeta: ChallengeMeta): boolean {
return challengeMeta.superBlock !== 'coding-interview-prep';
}

Some files were not shown because too many files have changed in this diff Show More