feat: new feature!

This commit is contained in:
Sboonny
2023-01-22 08:11:54 +00:00
parent db3c262eaf
commit f6c403cc80
1853 changed files with 58792 additions and 27401 deletions

View File

@@ -30,10 +30,10 @@ jobs:
language: ['javascript']
steps:
- name: Checkout repository
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Setup CodeQL
uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
uses: github/codeql-action/init@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
with:
languages: ${{ matrix.language }}
- name: Perform Analysis
uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # v2
uses: github/codeql-action/analyze@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
token: ${{ secrets.CROWDIN_CAMPERBOT_PAT }}

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
token: ${{ secrets.CROWDIN_CAMPERBOT_PAT }}
@@ -278,7 +278,7 @@ jobs:
# All languages should go ABOVE this. #
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
token: ${{ secrets.CROWDIN_CAMPERBOT_PAT }}

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Generate Crowdin Config
uses: freecodecamp/crowdin-action@main

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Generate Crowdin Config
uses: freecodecamp/crowdin-action@main

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Generate Crowdin Config
uses: freecodecamp/crowdin-action@main

View File

@@ -20,16 +20,16 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Checkout mobile
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
repository: freeCodeCamp/mobile
path: mobile
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

View File

@@ -29,16 +29,16 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Checkout client-config
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
repository: freeCodeCamp/client-config
path: client-config
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

View File

@@ -22,16 +22,16 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Checkout client-config
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
with:
repository: freeCodeCamp/client-config
path: client-config
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
@@ -51,13 +51,13 @@ jobs:
run: tar -cf client-artifact.tar client/public
- name: Upload Client Artifact
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: client-artifact
path: client-artifact.tar
- name: Upload Webpack Stats
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
with:
name: webpack-stats
path: client/public/stats.json
@@ -96,9 +96,9 @@ jobs:
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
with:
name: client-artifact
@@ -116,7 +116,7 @@ jobs:
sudo ln -s /opt/firefox/firefox /usr/bin/firefox
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

View File

@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -21,10 +21,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

View File

@@ -26,10 +26,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
@@ -61,10 +61,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Check number of lockfiles
run: |
@@ -35,7 +35,7 @@ jobs:
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
@@ -69,10 +69,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
@@ -103,10 +103,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}
@@ -139,10 +139,10 @@ jobs:
steps:
- name: Checkout Source Files
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: ${{ matrix.node-version }}

4
.gitignore vendored
View File

@@ -174,6 +174,10 @@ utils/block-nameify.test.js
utils/slugs.js
utils/slugs.test.js
utils/index.js
utils/get-lines.js
utils/get-lines.test.js
utils/validate.js
utils/validate.test.js
### vim ###
# Swap

View File

@@ -16,6 +16,10 @@ utils/block-nameify.test.js
utils/slugs.js
utils/slugs.test.js
utils/index.js
utils/get-lines.js
utils/get-lines.test.js
utils/validate.js
utils/validate.test.js
**/package-lock.json
web/.next
curriculum-server/data/curriculum.json

View File

@@ -281,12 +281,6 @@ function handleInvalidUpdate(res) {
function updateUserFlag(req, res, next) {
const { user, body: update } = req;
const allowedKeys = [
'theme',
'sound',
'keyboardShortcuts',
'isHonest',
'portfolio',
'sendQuincyEmail',
'isGithub',
'isLinkedIn',
'isTwitter',

View File

@@ -270,6 +270,8 @@ exports.onCreatePage = async ({ page, actions }) => {
}
};
// Take care to QA the challenges when modifying this. It has broken certain
// types of challenge in the past.
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
const typeDefs = `
@@ -280,6 +282,7 @@ exports.createSchemaCustomization = ({ actions }) => {
challengeFiles: [FileContents]
notes: String
url: String
assignments: [String]
}
type FileContents {
fileKey: String
@@ -293,44 +296,3 @@ exports.createSchemaCustomization = ({ actions }) => {
`;
createTypes(typeDefs);
};
// TODO: this broke the React challenges, not sure why, but I'll investigate
// further and reimplement if it's possible and necessary (Oliver)
// I'm still not sure why, but the above schema seems to work.
// Typically the schema can be inferred, but not when some challenges are
// skipped (at time of writing the Chinese only has responsive web design), so
// this makes the missing fields explicit.
// exports.createSchemaCustomization = ({ actions }) => {
// const { createTypes } = actions;
// const typeDefs = `
// type ChallengeNode implements Node {
// question: Question
// videoId: String
// required: ExternalFile
// files: ChallengeFile
// }
// type Question {
// text: String
// answers: [String]
// solution: Int
// }
// type ChallengeFile {
// indexhtml: FileContents
// indexjs: FileContents
// indexjsx: FileContents
// }
// type ExternalFile {
// link: String
// src: String
// }
// type FileContents {
// key: String
// ext: String
// name: String
// contents: String
// head: String
// tail: String
// }
// `;
// createTypes(typeDefs);
// };

View File

@@ -3,23 +3,23 @@
"title": "تراث تصميم مواقع الويب المتجاوبة",
"intro": [
"في شهادة تصميم الويب المستجيب هذه، ستتعلم اللغات التي يستخدمها المطورين لبناء صفحات الويب: HTML (Hypertext Markup Language) للمحتوى، و CSS (Cascading Style Sheets) للتصميم.",
"أولاً، سوف تقوم ببناء تطبيق صورة القطة لتعلم أساسيات HTML و CSS. لاحقاً، سوف تتعلم التقنيات الحديثة مثل متغيرات CSS عن طريق بناء بطريق، وأفضل الممارسات في اتاحة الوصول (accessibility) عن طريق بناء نموذج ويب.",
"أخيراً، سوف تتعلم كيف تنشئ صفحات ويب تستجيب لأحجام الشاشات المختلفة من خلال بناء بطاقة تويتر باستخدام Flexbox، وتخطيط معقد لمدونة باستخدام CSS Grid."
"أولاً، سوف تبني تطبيق صورة قطة لتتعلم أساسيات HTML و CSS. لاحقاً، سوف تتعلم التقنيات الحديثة مثل متغيرات CSS عن طريق بناء بِطْرِيق، وأفضل الممارسات في إتاحة الوصول (accessibility) عن طريق بناء نموذج ويب.",
"أخيراً، سوف تتعلم كيف تنشئ صفحات ويب تستجيب لأحجام الشاشات المختلفة عندما تبني بطاقة تويتر باستخدام Flexbox، وتخطيط معقد لمدونة باستخدام CSS Grid."
],
"note": "ملاحظة: بعض إضافات المتصفح, مثل Ad-blockers وإضافات الوضع الليلي قد تتصادم مع نتائج الاختبارات, إذا واجهت مشاكل, ننصحك بتعطيل الإضافات التي قد تؤثر أو تعدل على مخطط الصفحات, أثناء حضورك للدورة.",
"note": "ملاحظة: بعض إضافات المتصفح, مثل Ad-blockers وإضافات الوضع الليلي قد تتصادم مع نتائج الاختبارات, إذا واجهت مشكلات, ننصحك بتعطيل الإضافات التي قد تؤثر أو تعدل على مخطط الصفحات في أثناء حضورك للدورة.",
"blocks": {
"basic-html-and-html5": {
"title": "أساسيات HTML و HTML5",
"intro": [
"HTML هي لغة ترميز تستخدم سياق خاص أو رموز لوصف الهيكل الخاص بصفحة الويب للمتصفح، عناصر HTML عادة لديها وسم فتح واغلاق تحيط المحتوى وتضيف معنى له. على سبيل المثال، عناصر مختلفة بامكانها وصف النص كعنوان أو فقرة أو عنصر من قائمة.",
"في هذه الدورة، ستبني تطبيق صورة القطة لتعلم بعض من أكثر عناصر الـ HTML شيوعاً، كتل البناء لأي صفحة على الويب."
"HTML هي لغة ترميز تستخدم سياق خاص أو رموز لوصف الهيكل الخاص بصفحة الويب للمتصفح، عناصر HTML عادة لديها علامة فتح وإغلاق تحيط المحتوى وتضيف معنى له. على سبيل المثال، عناصر مختلفة بإمكانها وصف النص كعنوان أو فَقَرة أو عنصر من قائمة.",
"في هذه الدورة، ستبني تطبيق صورة القطة لتتعلم بعض من أكثر عناصر لغة HTML شيوعاً، وهي كتل البناء لأي صفحة على الويب."
]
},
"basic-css": {
"title": "أساسيات CSS",
"intro": [
"CSS أو Cascading Style Sheets تخبر المتصفح كيف يعرض النص والعناصر الأخرى التي قمت بكتابتها في ملف الـ HTML، مع CSS تستطيع التحكم باللون والخط والحجم والمسافة والكثير من الأوجه المختلفة لعناصر HTML.",
"الآن بعد أن وصفت بنية تطبيق صورة القطة الخاص بك، أضف له بعض التصاميم باستخدام CSS."
"CSS أو Cascading Style Sheets تخبر المتصفح كيف يعرض النص والعناصر الأخرى الذي قمت بكتابتها في مِلَفّ HTML. و بواسطة CSS تستطيع التحكم باللون والخط والحجم والمسافة والكثير من الأوجه المختلفة لعناصر HTML.",
"الآن بعد أن وصفت بنية تطبيق صورة القطة الخاصة بك، أضف له بعض التصاميم باستخدام CSS."
]
},
"applied-visual-design": {
@@ -32,7 +32,7 @@
"applied-accessibility": {
"title": "امكانية الوصول المطبقة",
"intro": [
"في تطوير المواقع، اماكنية الوصول تشير إلى محتوى ويب وواجهة مستخدم يمكن فهمها والتنقل بها والتفاعل معها من قبل جمهور عريض. هذا يتضمن أشخاص مصابين بإعاقات بصرية أو سمعية أو حركية أو ادراكية.",
"في تطوير المواقع، إماكنية الوصول تشير إلى محتوى ويب وواجهة مستخدم (UI - User Interface) يمكن فهمها والتنقل بها والتفاعل معها من قبل جَمهور عريض. هذا يتضمن أشخاص مصابين بإعاقات بصرية أو سمعية أو حركية أو إدراكية.",
"في هذه الدورة، ستتعلم أفضل الممارسات لبناء صفحات الويب التي يمكن للجميع الوصول إليها وفهمها."
]
},
@@ -76,80 +76,80 @@
"note": "ملاحظة: بعض إضافات المتصفح, مثل Ad-blockers وإضافات الوضع الليلي قد تتصادم مع نتائج الاختبارات, إذا واجهت مشاكل, ننصحك بتعطيل الإضافات التي قد تؤثر أو تعدل على مخطط الصفحات, أثناء حضورك للدورة.",
"blocks": {
"build-a-tribute-page-project": {
"title": "صفحة الإشادة",
"title": "صفحة الثناء",
"intro": [
"هذا هو أحد المشاريع المطلوبة للحصول على شهادتك.",
"في هذا المشروع، سوف تقوم ببناء صفحة ثناء لموضوع من اختيارك سواء كان الموضوع من وحي خيالك أو حقيقي."
"في هذا المشروع، سوف تنشئ صفحة إثناء لموضوع من اختيارك سواء كان الموضوع من وحي خيالك أو حقيقي."
]
},
"build-a-personal-portfolio-webpage-project": {
"title": "صفحه الويب لعرض نموذجات الأعمال الشخصية",
"title": "معرضا لأعمالك الخاصة",
"intro": [
"هذا هو أحد المشاريع المطلوبة للحصول على شهادتك.",
"في هذا المشروع، سوف تقوم ببناء صفحة ويب لعرض نماذج الاعمال الشخصية الخاصة بك."
"هذا هو أحد المشروعات المطلوبة للحصول على شهادتك.",
"في هذا المشروع، سوف تتشئ صفحة ويب معرضا لأعمالك الخاصة."
]
},
"build-a-product-landing-page-project": {
"title": "صفحة الهبوط للمنتج",
"title": "صفحة لعرض المنتج",
"intro": [
"هذا هو أحد المشاريع المطلوبة للحصول على شهادتك.",
"في هذا المشروع، سوف تقوم ببناء صفحة هبوط لمنتج من اختيارك للتسويق له."
"هذا هو أحد المشروعات المطلوبة للحصول على شهادتك.",
"في هذا المشروع، ستنشئ صفحة لعرض المنتج من اختيارك للتسويق له."
]
},
"build-a-survey-form-project": {
"title": "نموذج استطلاع رأي",
"intro": [
"هذا هو أحد المشاريع المطلوبة للحصول على شهادتك.",
"في هذا المشروع، سوف تقوم ببناء نموذج الدراسة الاستقصائية لجمع بيانات من المستخدمين."
"هذا هو أحد المشروعات المطلوبة للحصول على شهادتك.",
"في هذا المشروع، سوف تنشئ نموذج استطلاع رأي لجمع بيانات من مستخدميك."
]
},
"build-a-technical-documentation-page-project": {
"title": "صفحة الوثائق التقنية",
"title": "صفحة التوثيق التقنية",
"intro": [
"هذا هو أحد المشاريع المطلوبة للحصول على شهادتك.",
"في هذا المشروع، ستنشئ صفحة التوثيق التقني لتكون بمثابة تعليمات أو مرجع لموضوع ما."
"هذا هو أحد المشروعات المطلوبة للحصول على شهادتك.",
"في هذا المشروع، ستنشئ صفحة نموذج استطلاع رأي لتقوم مقام التعليمات أو المرجع لموضوع ما."
]
},
"learn-html-by-building-a-cat-photo-app": {
"title": "تعلم HTML عن طريق بناء تطبيق صورة القطة",
"intro": [
"تعطي وسوم HTML صفحة الويب هيكلها. يمكنك استخدام وسوم HTML لإضافة الصور والأزرار والعناصر الأخرى إلى صفحة الويب الخاصة بك.",
"في هذه الدورة، ستتعلم وسوم HTML الأكثر شيوعاً عن طريق بناء تطبيقك لصورة القطة."
"في هذه الدورة، ستتعلم وسوم HTML الأكثر شيوعاً عن طريق بناء تطبيقك صورة القطة."
]
},
"learn-basic-css-by-building-a-cafe-menu": {
"title": "تعلم اساسيات CSS عن طريق بناء قائمة مقهى",
"intro": [
"CSS يخبر المتصفح كيفية عرض صفحة الويب الخاصة بك. يمكنك استخدام CSS لتعيين اللون والخطوط والحجم والجوانب الأخرى لعناصر HTML.",
"في هذه الدورة، سوف تتعلم CSS عن طريق تصميم صفحة قائمة لصفحة ويب لمقهي."
"يخبر CSS المتصفح كيف تعرض صفحتك الويب. يمكنك استخدام CSS لتعيين اللون والخطوط والحجم والجوانب الأخرى لعناصر HTML.",
"في هذه الدورة، سوف تتعلم CSS عن طريق تصميم صفحة قائمة لصفحة ويب لمقهى."
]
},
"learn-the-css-box-model-by-building-a-rothko-painting": {
"title": "تعلم CSS Box Model عن طريق بناء رسم لـRothko",
"intro": [
"كل عنصر من عناصر HTML هو صندوق خاص به - مع مساحته الخاصة وحدوده. وهذا يسمى نموذج Box Model.",
"في هذه الدورة، ستستخدم CSS و Box Model لإنشاء قطع فنية مستطيلة على نمط Rothko-الخاص بك."
"كل عنصر من عناصر HTML هو صندوقه - مع مساحته الخاصة وحدوده. وهذا يسمى نموذج الصندوق Box Model.",
"في هذه الدورة، ستستخدم CSS و Box Model لإنشاء قطع فنية مستطيلة على تصممك لوحة روثكو."
]
},
"learn-css-variables-by-building-a-city-skyline": {
"title": "تعلم متغيرات CSS عن طريق بناء خط افق المدينة",
"title": "تعلم متغيرات CSS عن طريق أنشئ ناطحة السحاب بالمدينة",
"intro": [
"تساعدك متغيرات CSS على تنظيم الأنماط الخاصة بك وإعادة استخدامها.",
"في هذه الدورة، ستبني خط افق المدينة. ستتعلم كيفية تهيئة متغيرات CSS حتى تتمكن من إعادة استخدامها كلما أردت."
"تساعدك متغيرات CSS على تنظيم تصميماتك وإعادة استخدامها.",
"في هذه الدورة، ستبني ناطحة سحب بالمدينة. ستتعلم كيفية إعداد متغيرات CSS حتى تتمكن من إعادة استخدامها كلما أردت."
]
},
"learn-html-forms-by-building-a-registration-form": {
"title": "تعلم نماذج HTML عن طريق بناء نموذج تسجيل",
"title": "تعلم نموذجات HTML عن طريق بناء نموذج تسجيل",
"intro": [
"يمكنك استخدام نماذج HTML لجمع المعلومات من الأشخاص الذين يزورون صفحة الويب الخاصة بك.",
"يمكنك استخدام نموذجات HTML لجمع المعلومات من الأشخاص الذين يزورون صفحتك الويب.",
"في هذه الدورة، ستتعلم نماذج HTML عن طريق بناء صفحة التسجيل. ستتعلم كيفية التحكم في أنواع البيانات التي يمكن للناس كتابتها في النموذج، وبعض أدوات CSS الجديدة لتصميم صفحتك."
]
},
"learn-accessibility-by-building-a-quiz": {
"title": "تعلم إمكانية الوصول Accessibility عن طريق بناء اختبار",
"title": "تعلم تسهيل المنال Accessibility عن طريق بناء اختبار",
"intro": [
"إمكانية الوصول تجعل صفحة الويب الخاصة بك سهلة الاستخدام لجميع الناس - حتى الأشخاص ذوي الإعاقة.",
"في هذه الدورة، ستبني صفحة ويب لاختبار. ستتعلم أدوات الوصول مثل اختصارات لوحة المفاتيح، سمات ARIA وأفضل الممارسات في التصميم."
"الاهتمام بتسهيل المنال إلى صفحتك الويب سهلة الاستخدام لجميع الناس - حتى الأشخاص ذوي الإعاقة.",
"في هذه الدورة، ستبني صفحة ويب لاختبار. ستتعلم أدوات تسهل المنال مثل اختصارات لوحة المفاتيح، سمات ARIA وأفضل الممارسات في التصميم."
]
},
"learn-intermediate-css-by-building-a-picasso-painting": {
@@ -159,31 +159,31 @@
]
},
"learn-responsive-web-design-by-building-a-piano": {
"title": "تعلم تصميم الويب المتجاوب Responsive Web Design عن طريق بناء بيانو",
"title": "تعلم تصميم الويب المستجيب Responsive Web Design عن طريق بناء بيانو",
"intro": [
"التصميم المستجيب يخبر صفحة الويب الخاصة بك كيف ينبغي أن تبدو على شاشات مختلفة الحجم.",
"يخبر التصميم المستجيب صفحتك الويب كيف ينبغي أن تبدو على شاشات مختلفة الحجم.",
"في هذه الدورة، ستستخدم CSS والتصميم المستجيب لبرمجة وبناء بيانو. كما ستتعلم المزيد عن media queries و pseudo selectors."
]
},
"learn-css-flexbox-by-building-a-photo-gallery": {
"title": "تعلم CSS Flexbox من خلال بناء معرض للصور",
"intro": [
"يساعدك Flexbox في تصميم صفحة الويب الخاصة بك بحيث تبدو جيدة على أي حجم شاشة.",
"يساعدك Flexbox في تصميم صفحتك الويب بحيث تبدو جيدة على أي حجم شاشة.",
"في هذه الدورة، ستستخدم Flexbox لإنشاء موقع ويب مستجيب Responsive لمعرض صور."
]
},
"learn-css-grid-by-building-a-magazine": {
"title": "تعلم CSS Grid عن طريق بناء مجلة",
"intro": [
"تمنحك CSS Grid التحكم في صفوف وأعمدة تصميم صفحة الويب الخاصة بك.",
"تمنحك CSS Grid التحكم في صفوف وأعمدة تصميم صفحتك الويب.",
"في هذه الدورة، ستقوم ببناء مقال في مجلة. ستتعلم كيفية استخدام CSS Grid، بما في ذلك مفاهيم مثل صفوف الشبكة Grid rows وأعمدة الشبكة Grid columns."
]
},
"learn-typography-by-building-a-nutrition-label": {
"title": "تعلم الطباعة Typography عن طريق بناء علامة التغذية",
"intro": [
"التيبوغرافي هو فن تصميم النص الخاص بك ليكون سهل القراءة ويناسب الغرض منه.",
"في هذه الدورة، ستستخدم typography لإنشاء صفحة ويب لتسمية التغذية. ستتعلم كيفية تغيير نمط النص، وتعديل ارتفاع الخط line-height، و تغيير وضع position نصك باستخدام CSS."
"إن تشكيل الخط فن لتصميم نصك ليسهل قراءة ويلائم غرضه.",
"في هذه الدورة، ستستخدم typography لإنشاء صفحة ويب لتسمية التغذية. ستتعلم كيفية تغيير تشكيل النص، وتعديل ارتفاع الخط line-height، و تغيير وضع position نصك باستخدام CSS."
]
},
"learn-css-transforms-by-building-a-penguin": {
@@ -204,7 +204,7 @@
"title": "تعلم المزيد عن Selectors Pseudo في CSS عن طريق بناء كشف ميزانية",
"intro": [
"يمكنك استخدام CSS pseudo selectors لتغيير عناصر HTML محددة.",
"في هذه الدورة ، ستبني ميزانية عامة باستخدام pseudo selectors. سوف تتعلم كيفية تغيير نمط عنصر ما عندما تحوم فوقه بالماوس الخاص بك، وتفعيل أحداث أخرى على صفحة الويب الخاصة بك."
"في هذه الدورة، ستبني ميزانية عامة باستخدام المنتقات الزائفة pseudo selectors. سوف تتعلم كيفية تغيير تصميم عنصر ما عندما تحوم فوقه بماوسك، وتفعيل أحداث أخرى على صفحتك الويب."
]
},
"learn-css-colors-by-building-a-set-of-colored-markers": {
@@ -219,7 +219,7 @@
"javascript-algorithms-and-data-structures": {
"title": "الخوارزميات وهياكل البيانات في JavaScript",
"intro": [
"في حين أن HTML و CSS يتحكمان في محتوى الصفحة وتصميم الصفحة، يستخدم Javascript لجعلها تفاعلية. في شهادة الخوارزميات وهياكل البيانات في JavaScript، ستتعلم أساسيات JavaScript بما في ذلك المتغيرات variables, والقوائم arrays, والكائنات Objects والحلقات Loops, والوظائف functions.",
"في حين أن HTML و CSS يتحكمان في محتوى الصفحة وتصميم الصفحة، يستخدم Javascript لجعلها تفاعلية. في شهادة الخوارزميات وهياكل البيانات في JavaScript، ستتعلم أساسيات JavaScript بما في ذلك المتغيرات variables, والقوائم arrays, والكائنات objects والحلقات loops, والوظائف functions.",
"بمجرد أن يكون لديك الأساسيات، ستطبق تلك المعرفة عن طريق إنشاء خوارزميات للتلاعب بالسلاسل strings، تحديد الأعداد، بل وحساب مدار محطة الفضاء الدولية.",
"في هذا المشوار ستتعلم أيضًا أسلوبين أو نموذجين مهمين للبرمجة: البرمجة كائنية التوجه (OOP) ، والبرمجة الوظيفية (FP)."
],
@@ -229,7 +229,7 @@
"title": "أساسيات JavaScript",
"intro": [
"إن JavaScript لغة برمجة يمكنك استخدامها لجعل صفحات الويب تفاعلية. إنها إحدى التكنولوجيات الأساسية للويب، إلى جانب HTML و CSS، وهي مدعومة بجميع المتصفحات الحديثة.",
"في هذه الدورة، ستتعلم مفاهيم البرمجة الأساسية في Javascript. ستبدأ بهياكل البيانات الأساسية مثل الأرقام والنصوص. ثم سوف تتعلم العمل مع الكائنات والقائمات والوظائف والحلقات والشروط وتعبيرات if/else وأكثر من ذلك."
"في هذه الدورة، ستتعلم مفاهيم البرمجة الأساسية في Javascript. ستبدأ بهياكل البيانات الأساسية مثل الأرقام والنصوص. ثم سوف تتعلم العمل مع الكائنات والقوائم والوظائف والحلقات والشروط وتعبيرات if/else وأكثر من ذلك."
]
},
"es6": {
@@ -241,9 +241,9 @@
]
},
"regular-expressions": {
"title": "التعبيرات العادية Regular Expressions",
"title": "العبارات العادية Regular Expressions",
"intro": [
"التعبيرات العادية Regular expressions، غالباً ما تسمي \"regex\" أو \"regexp\"، هي أنماط تساعد المبرمجين على المطابقة والبحث واستبدال النص. التعابير العادية قوية جداً، ولكن يمكن أن يكون من الصعب قراءتها لأنها تستخدم رموز خاصة لعمل مطابقات أكثر تعقيداً ومرونة.",
"العبارات العادية Regular expressions، غالباً ما تسمي \"regex\" أو \"regexp\"، هي أنماط تساعد المبرمجين على المطابقة والبحث واستبدال النص. التعابير العادية قوية جداً، ولكن يمكن أن يكون من الصعب قراءتها لأنها تستخدم رموز خاصة لعمل مطابقات أكثر تعقيداً ومرونة.",
"في هذه الدورة، ستتعلم كيفية استخدام الرموز الخاصة، التقاط المجموعات، النظرات الإيجابية والسلبية، وتقنيات أخرى لمطابقة أي نص تريده."
]
},
@@ -258,8 +258,8 @@
"basic-data-structures": {
"title": "هياكل البيانات الأساسية",
"intro": [
"يمكن تخزين البيانات والوصول إليها بعدة طرق. أنت تعرف بالفعل بعض هياكل بيانات javascript الشائعة - المصفوفات والكائنات.",
"في هذه الدورة لهياكل البيانات الأساسية، ستتعلم المزيد عن الاختلافات بين المصفوفات والكائنات، وعن كيفية استخدامها في أوضاع مختلفة. سوف تتعلم أيضاً كيفية استخدام أساليب Javascript المفيدة مثل <code>splice()</code> و<code> <code>Object.keys()</code> للوصول إلى البيانات والتلاعب بها."
"يمكن تخزين البيانات والوصول إليها بعدة طرق. أنت تعرف بالفعل بعض هياكل بيانات javascript الشائعة - القوائم والكائنات.",
"في هذه الدورة لهياكل البيانات الأساسية، ستتعلم المزيد عن الاختلافات بين القوائم والكائنات، وعن كيفية استخدامها في أوضاع مختلفة. سوف تتعلم أيضاً كيفية استخدام أساليب Javascript المفيدة مثل <code>splice()</code> و <code>Object.keys()</code> للوصول إلى البيانات والتلاعب بها."
]
},
"basic-algorithm-scripting": {
@@ -267,7 +267,7 @@
"intro": [
"الخوارزمية هي سلسلة من التعليمات خطوة بخطوة تصف كيفية القيام بشيء ما.",
"يساعدك تفكيك المشكلة إلى أجزاء أصغر والتفكير بعناية حول كيفية حل كل جزء علي حدي بواسطة الكود البرمجي على كتابة خوارزميات اكثر فاعلية.",
"في هذه الدورة، ستتعلم أساسيات التفكير الخوارزمي عن طريق كتابة الخوارزميات التي تفعل كل شيء من تحويل درجات الحرارة إلى التعامل مع المصفوفات ثنائية الأبعاد 2D arrays المعقدة."
"في هذه الدورة، ستتعلم أساسيات التفكير الخوارزمي عن طريق كتابة خوارزميات تفعل كل شيء من تحويل درجات الحرارة إلى التعامل مع القوائم ثنائية الأبعاد 2D arrays المعقدة."
]
},
"object-oriented-programming": {
@@ -280,7 +280,7 @@
"functional-programming": {
"title": "البرمجة الوظيفية Functional Programming",
"intro": [
"البرمجة الوظيفية هي نهج شائع آخر لتطوير البرمجيات. في البرمجة الوظيفية، يتم تنظيم الكود في وظائف أصغر وأساسية يمكن الجمع بينها لبناء برامج معقدة.",
"إن البرمجة الوظيفية نهج شائع آخر لتطوير البرمجيات. في البرمجة الوظيفية، ينظم الكود في وظائف أصغر وأساسية يمكن الجمع بينها لبناء برامج معقدة.",
"في هذه الدورة، ستتعلم المفاهيم الأساسية للبرمجة الوظيفية بما في تلك الوظيفة العادية، كيفية تجنب الطفرات وكيفية كتابة كود أنظف مع أساليب مثل <code>.map()</code> و <code>.filter()</code>."
]
},
@@ -305,23 +305,23 @@
"note": "",
"blocks": {
"build-a-caesars-cipher-project": {
"title": "بناء مشروع Caesars Cipher",
"title": "بناء مشروع شفرة قيصر",
"intro": ["", ""]
},
"build-a-cash-register-project": {
"title": "بناء مشروع Cash Register",
"title": "بناء مشروع مكنة لتسجيل النقود",
"intro": ["", ""]
},
"build-a-palindrome-checker-project": {
"title": "بناء مشروع Palindrome Checker",
"title": "أنشئ مشروع مدقق لمعاكس المقطع النصي",
"intro": ["", ""]
},
"build-a-roman-numeral-converter-project": {
"title": "بناء مشروع Roman Numeral Converter",
"title": "أنشئ مشروع محول للأرقام الرومانية",
"intro": ["", ""]
},
"build-a-telephone-number-validator-project": {
"title": "بناء مشروع Telephone Number Validator",
"title": "بناء مشروع مدقق الأرقام الهواتف",
"intro": ["", ""]
},
"learn-basic-javascript-by-building-a-role-playing-game": {
@@ -372,7 +372,7 @@
"react": {
"title": "React",
"intro": [
"React هي مكتبة شائعة من مكتبات JavaScript لبناء واجهة مستخدم قابلة لإعادة البناء اعتماداً على المكون لصفحات الويب أو التطبيقات.",
"إن React مكتبة شائعة من مكتبات JavaScript لبناء واجهة مستخدم قابلة لإعادة البناء اعتماداً على المكون لصفحات الويب أو التطبيقات.",
"يجمع React بين الـ HTML ووظائف الـ JavaScript في لغة ترميز خاصة به تدعى JSX. كما أن React يجعل من السهل إدارة تدفق البيانات في التطبيق.",
"في هذه الدورة ، ستتعلم كيفية إنشاء مكونات مختلفة من React وإدارة البيانات في شكل State و Props و استخدام أساليب دورة الحياة المختلفة مثل <code>componentDidMount</code>، وأكثر من ذلك بكثير."
]
@@ -389,13 +389,13 @@
"title": "React and Redux",
"intro": [
"كثيرا ما يُشار إلى React و Redux معاً، ولسبب وجيه. المطور الذي أنشأ Redux كان مطور لـ React و اراد أن يسهل مشاركة البيانات عبر المكونات المختلفة.",
"الآن بعد أن عرفت كيفية إدارة تدفق البيانات المشتركة مع Redux، حان الوقت للجمع بين تلك المعرفة و React. في دورات React و Redux، ستبني مكون React وتتعلم كيفية إدارة الحالة state محلياً على مستوى المكون. وعبر التطبيق باكمله مع Redux."
"الآن بعد أن عرفت كيفية إدارة تدفق البيانات المشتركة مع Redux، حان الوقت للجمع بين تلك المعرفة و React. في دورات React و Redux، ستبني مكون React وتتعلم كيفية إدارة الحالة state محلياً على مستوى المكون. وعبر التطبيق برمته مع Redux."
]
},
"front-end-development-libraries-projects": {
"title": "مشاريع مكتبات تطوير واجهة المستخدم",
"intro": [
"حان الوقت لوضع مهاراتك في مكتبات تطوير واجهات المستخدم للاختبار، استخدم Bootstrap, jQuery, Sass, React و Redux لبناء 5 مشاريع لتختبر كل ما تعلمته حتى هذه النقطة.",
"حان الوقت لوضع مهاراتك في مكتبات تطوير واجهات المستخدم للاختبار. استخدم Bootstrap, و jQuery, و Sass, و React, و Redux لبناء 5 مشروعات لتختبر ما تعلمته حتى هذه النقطة.",
"أكمل جميع المشاريع الخمسة، وستحصل على شهادة في مكتبات تطوير واجهة المستخدم."
]
}
@@ -551,9 +551,9 @@
"back-end-development-and-apis": {
"title": "تطوير الواجهات الخلفية للمواقع و واجهات برمجة التطبيقات - Back End Development and APIs",
"intro": [
"حتى هذه النقطة، لقد استخدمت JavaScript فقط في الواجهة الأمامية لإضافة تفاعل إلى صفحة، أو حل تحديات الخوارزميات، أو بناء SPA. ولكن يمكن أيضاً استخدام JavaScript في الواجهة الخلفية back end، أو الخادم، لبناء تطبيقات ويب بأكملها.",
"حتى هذه النقطة، لقد استخدمت JavaScript فقط في الواجهة الأمامية لإضافة تفاعل إلى صفحة، أو حل تحديات الخوارزميات، أو بناء SPA. ولكن يمكن أيضاً استخدام JavaScript في الواجهة الخلفية back end، أو السيرفر، لبناء تطبيقات ويب برمتها.",
"واليوم، فإن إحدى الطرق الشائعة لبناء التطبيقات هي من خلال الخدمات المصّغرة microservices، وهي تطبيقات صغيرة ومحددة تعمل معاً لتشًكل وحدة اكبر.",
"في شهادة واجهة برمجة التطبيقات والـAPIs، ستتعلم كيفية انشاء تطبيقات back end جاهزة باستخدام Node.js و npm. سوف تقوم أيضا ببناء تطبيقات ويب باستخدام إطار Express وبناء microservice لـ People Finder باستخدام MongoDB ومكتبة Mongoose."
"في شهادة تطوير واجهات الBack End و الAPIs، سوف تتعلم كيف تنشئ تطبيقات back end باستخدام Node.js و npm. سوف تقوم أيضا ببناء تطبيقات الويب باستخدام إطار العمل Express، و بناء microservice لإيجاد الأفراد باستخدام MongoDB ومكتبة Mongoose."
],
"note": "",
"blocks": {
@@ -584,7 +584,7 @@
"back-end-development-and-apis-projects": {
"title": "مشاريع تطوير الواجهات الخلفية للمواقع و واجهات برمجة التطبيقات APIs",
"intro": [
"لقد عملت مع واجهة برمجة التطبيقات APIs من قبل، ولكن الآن بعد أن عرفت npm، وNode, Express، وMongoDB وMongoose، حان الوقت لبناء API بنفسك، بالاعتماد على كل ما تعلمته حتى هذه النقطة لإنشاء 5 خدمات مصغّرة microservices مختلفة، وهي تطبيقات أصغر محدودة النطاق.",
"لقد عملت مع واجهة برمجة التطبيقات APIs من قبل، ولكن الآن بعد أن عرفت npm، و Node, و Express، و MongoDB، و Mongoose، حان الوقت لبناء API بنفسك، بالاعتماد على ما تعلمته حتى هذه النقطة لإنشاء 5 خدمات مصغّرة microservices مختلفة، وهي تطبيقات أصغر محدودة النطاق.",
"بعد إنشاء هذه البرمجيات، سيكون لديك 5 APIs رائعة للخدمات الصغرى يمكنك عرضها للأصدقاء والعائلة وأصحاب العمل المحتملين، وستحصل أيضًا على شهادة تطوير الواجهة الخلفية وواجهات برمجة التطبيقات."
]
}
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "مشروع Euler",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "عرض",
"view-code": "أظهار الكود",
"view-project": "إظهار المشروع",
"view-cert-title": "عرض {{certTitle}}",
"show-cert": "عرض الشهادة",
"claim-cert": "المطالبة بالشهادة",
"save-progress": "حفظ التقدم",
@@ -52,7 +53,7 @@
"check-code": "تيقن من كودك (Ctrl + Enter)",
"check-code-2": "تيقن من كودك",
"reset": "إعادة ضبط",
"reset-code": "أعد جميع الكود ألى حالتهم الأولية",
"reset-step": "إعادة الخطوة إلى حالتها الأولية",
"help": "مساعدة",
"get-help": "الحصول على المساعدة",
"watch-video": "شاهد الفيديو",
@@ -157,7 +158,8 @@
"honesty": "سياسة الصدق الأكاديمي",
"internet": "حضورك على الإنترنت",
"portfolio": "إعدادات المحفظة",
"privacy": "إعدادات الخصوصية"
"privacy": "إعدادات الخصوصية",
"personal-info": "Personal Information"
},
"danger": {
"heading": "منطقة الخطر",
@@ -272,6 +274,7 @@
"add-subtitles": "المساعدة في تحسين أو إضافة ترجمات",
"wrong-answer": "عذراً، هذه ليست الإجابة الصحيحة. قم بالمحاولة مرة أخرى؟",
"check-answer": "انقر على الزر أدناه للتحقق من إجابتك.",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "رابط الحل",
"github-link": "رابط Github",
"submit-and-go": "أرسل وانتقل إلى التحدي التالي",
@@ -339,6 +342,7 @@
"title": "ادعم مؤسستنا الخيرية",
"processing": "نحن نقوم بمعالجة تبرعك.",
"redirecting": "جارٍ إعادة توجيهك...",
"thanks": "شكرا على التبرع",
"thank-you": "شكراً لكونك داعماً.",
"additional": "يمكنك تقديم تبرع إضافي لمرة واحدة بأي مبلغ باستخدام هذا الرابط: <0>{{url}}</0>",
"help-more": "ساعدنا على القيام بالمزيد",
@@ -462,7 +466,8 @@
"iframe-preview": "معاينة {{title}}",
"iframe-alert": "عادة هذا الرابط من شأنه أن يجلبك إلى موقع آخر! إنه يعمل. هذا رابط: {{externalLink}}",
"iframe-form-submit-alert": "عادة سيتم تقديم هذا النموذج! إنه يعمل. سيتم إرسال هذا إلى: {{externalLink}}",
"document-notfound": "لم يوجد المستند"
"document-notfound": "لم يوجد المستند",
"slow-load-msg": "يبدو أن هذا يستغرق وقتاً أطول من المعتاد، الرجاء محاولة تحديث الصفحة."
},
"icons": {
"gold-cup": "كأس الذهب",
@@ -507,7 +512,8 @@
"step": "الخطوة",
"steps": "الخطوات",
"steps-for": "خطوات {{blockTitle}}",
"code-example": "{{codeName}} مثال على الكود"
"code-example": "{{codeName}} مثال على الكود",
"opens-new-window": "فتح في نافذة جديدة"
},
"flash": {
"honest-first": "للمطالبة بشهادة ، يجب عليك أولاً قبول سياسة الصدق الأكاديمي الخاصة بنا",
@@ -604,10 +610,10 @@
"source": "المصدر",
"footnote": "إذا كنت تشك في أن أيا من هذه المشاريع ينتهك <2>سياسة الأمانة الأكاديمية</2>، يرجى <5>إبلاغ فريقنا بهذا </5>.",
"title": {
"Build a Personal Portfolio Webpage": "قم ببناء مشروع صفحة ويب لعرض نماذج الاعمال الشخصية",
"Build a Personal Portfolio Webpage": "أنشئ معرضا لأعمالك الخاصة",
"Build a Random Quote Machine": "بناء آلة عرض اقتباسات عشوائية",
"Build a 25 + 5 Clock": "بناء ساعة 25 + 5",
"Build a JavaScript Calculator": "بناء حاسبة بإستخدام JavaScript",
"Build a JavaScript Calculator": "بناء آلة حاسبة باستخدام JavaScript",
"Show the Local Weather": "إظهار الطقس المحلي",
"Use the TwitchTV JSON API": "استخدام TwitchTV JSON API",
"Stylize Stories on Camper News": "نسق قصص على أخبار Camper",
@@ -641,14 +647,14 @@
"Anonymous Message Board": "لوحة الرسائل المجهولة",
"Build a Tribute Page": "بناء مشروع صفحة الثناء",
"Build a Survey Form": "بناء مشروع نموذج الدراسة الاستقصائية",
"Build a Product Landing Page": "بناء صفحة هبوط لمنتج",
"Build a Product Landing Page": "أنشئ صفحة لعرض المنتج",
"Build a Technical Documentation Page": "بناء مشروع صفحة التوثيق التقني",
"Palindrome Checker": "مدقق باليندروم",
"Roman Numeral Converter": "تحويل الأرقام الرومانية",
"Caesars Cipher": "Caesars Cipher",
"Telephone Number Validator": "مدقق رقم الهاتف",
"Cash Register": "سجل النقدية",
"Build a Drum Machine": "بناء آلة الدرامز",
"Build a Drum Machine": "أنشئ آلة الطبول",
"Visualize Data with a Choropleth Map": "التصوير المرئي للبيانات باستخدام خريطة التمثيل اللوني",
"Visualize Data with a Treemap Diagram": "التصوير المرئي للبيانات باستخدام الخريطة الشجرية",
"Exercise Tracker": "متعقب التمارين",
@@ -701,19 +707,19 @@
"data-analysis-with-python-v7": "شهادة تحليل البيانات باستخدام Python",
"Information Security": "أمن المعلومات",
"information-security-v7": "شهادة امن المعلومات",
"Machine Learning with Python": "Machine Learning with Python",
"machine-learning-with-python-v7": "Machine Learning with Python Certification",
"Legacy Front End": "Legacy Front End",
"legacy-front-end": "Front End Certification",
"Legacy Back End": "Legacy Back End",
"legacy-back-end": "Back End Certification",
"Legacy Data Visualization": "Legacy Data Visualization",
"legacy-data-visualization": "Data Visualization Certification",
"Machine Learning with Python": "تعلم الآلة باستخدام Python",
"machine-learning-with-python-v7": "مشاريع تعلم الآله باستخدام Python",
"Legacy Front End": "الواجهة الأمامية التراثية",
"legacy-front-end": "شهادة الواجهة الأمامية التراثية",
"Legacy Back End": "الواجهة الخلفية التراثية",
"legacy-back-end": "شهادة الواجهة الخلفية التراثية",
"Legacy Data Visualization": "التصوير المرئي للبيانات التراثي",
"legacy-data-visualization": "شهادة التصوير المرئي للبيانات",
"Legacy Information Security and Quality Assurance": "أمن المعلومات وشهادة ضمان الجودة التراثية",
"information-security-and-quality-assurance": "شهادة أمن المعلومات وشهادة ضمان الجودة",
"Legacy Full Stack Certification": "Legacy Full Stack Certification",
"Legacy Full Stack": "Legacy Full Stack",
"full-stack": "Full Stack Certification"
"Legacy Full Stack Certification": "شهادة الشامل الخلفية التراثية",
"Legacy Full Stack": "الشامل الخلفية التراثية",
"full-stack": "شهادة الشامل الخلفية"
}
},
"certification-card": {

View File

@@ -553,7 +553,7 @@
"intro": [
"在這之前,你只是在前端使用 JavaScript 來給頁面添加交互、解決算法挑戰,或構建一個 SPA單頁應用程序。但 JavaScript 也可以用於後端或者服務器來構建整個 web 應用程序。",
"今天,構建應用軟件的廣受歡迎的方法之一是微服務,這些微服務是一種小型模塊化的應用,能夠共同形成一個更大的整體。",
"在後端開發和 APIs 認證中,你將學習如何使用 Node.js npmNode 包管理工具)來寫後端。你還將使用 Express 框架構建 web 應用程序,並使用 MongoDB Mongoose 庫構建一個 People Finder 微服務。"
"In the Back End Development and APIs Certification, you'll learn how to write back end apps with Node.js and npm. You'll also build web applications with the Express framework, and build a People Finder microservice with MongoDB and the Mongoose library."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "歐拉計劃",
"intro": [

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"view": "查看",
"view-code": "查看代碼",
"view-project": "查看項目",
"view-cert-title": "View {{certTitle}}",
"show-cert": "顯示認證",
"claim-cert": "申請認證",
"save-progress": "保存進度",
@@ -52,7 +53,7 @@
"check-code": "檢查您的代碼 (Ctrl + Enter)",
"check-code-2": "檢查你的代碼",
"reset": "重置",
"reset-code": "重置所有代碼",
"reset-step": "Reset This Step",
"help": "幫助",
"get-help": "獲得幫助",
"watch-video": "觀看視頻",
@@ -157,7 +158,8 @@
"honesty": "學術誠信條例",
"internet": "你在各平臺的賬號",
"portfolio": "作品集設置",
"privacy": "隱私設置"
"privacy": "隱私設置",
"personal-info": "Personal Information"
},
"danger": {
"heading": "危險區域",
@@ -272,6 +274,7 @@
"add-subtitles": "幫助我們完善或添加字幕",
"wrong-answer": "抱歉,這個答案不正確。再試一次?",
"check-answer": "點擊下方按鈕,查看你的答案。",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "解決方案鏈接",
"github-link": "GitHub 鏈接",
"submit-and-go": "提交併訪問下一個挑戰",
@@ -339,6 +342,7 @@
"title": "支持我們的慈善組織",
"processing": "我們正在處理你的捐款。",
"redirecting": "重新引導中...",
"thanks": "Thanks for donating",
"thank-you": "謝謝你成爲我們的支持者。",
"additional": "你可以使用這個鏈接 <0>{{url}}</0> 額外進行一次性捐款:",
"help-more": "幫助我們做更多",
@@ -462,7 +466,8 @@
"iframe-preview": "{{title}} 預覽",
"iframe-alert": "通常,此鏈接會將你帶到另一個網站!一切正常,這個鏈接指向:{{externalLink}}。",
"iframe-form-submit-alert": "通常這個表單將被提交!工作正常,這將被提交到:{{externalLink}}",
"document-notfound": "找不到文件"
"document-notfound": "找不到文件",
"slow-load-msg": "Looks like this is taking longer than usual, please try refreshing the page."
},
"icons": {
"gold-cup": "金獎盃",
@@ -507,7 +512,8 @@
"step": "步驟",
"steps": "步驟",
"steps-for": "{{blockTitle}} 的步驟",
"code-example": "{{codeName}} 代碼示例"
"code-example": "{{codeName}} 代碼示例",
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "申請認證之前,你必須先接受我們的《學術誠信條例》",

View File

@@ -553,7 +553,7 @@
"intro": [
"在这之前,你只是在前端使用 JavaScript 来给页面添加交互、解决算法挑战,或构建一个 SPA单页应用程序。但 JavaScript 也可以用于后端或者服务器来构建整个 web 应用程序。",
"今天,构建应用软件的广受欢迎的方法之一是微服务,这些微服务是一种小型模块化的应用,能够共同形成一个更大的整体。",
"在后端开发和 APIs 认证中,你将学习如何使用 Node.js npmNode 包管理工具)来写后端。你还将使用 Express 框架构建 web 应用程序,并使用 MongoDB Mongoose 库构建一个 People Finder 微服务。"
"In the Back End Development and APIs Certification, you'll learn how to write back end apps with Node.js and npm. You'll also build web applications with the Express framework, and build a People Finder microservice with MongoDB and the Mongoose library."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "欧拉计划",
"intro": [

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"view": "查看",
"view-code": "查看代码",
"view-project": "查看项目",
"view-cert-title": "View {{certTitle}}",
"show-cert": "显示认证",
"claim-cert": "申请认证",
"save-progress": "保存进度",
@@ -52,7 +53,7 @@
"check-code": "检查您的代码 (Ctrl + Enter)",
"check-code-2": "检查你的代码",
"reset": "重置",
"reset-code": "重置所有代码",
"reset-step": "Reset This Step",
"help": "帮助",
"get-help": "获得帮助",
"watch-video": "观看视频",
@@ -157,7 +158,8 @@
"honesty": "学术诚信条例",
"internet": "你在各平台的账号",
"portfolio": "作品集设置",
"privacy": "隐私设置"
"privacy": "隐私设置",
"personal-info": "Personal Information"
},
"danger": {
"heading": "危险区域",
@@ -272,6 +274,7 @@
"add-subtitles": "帮助我们完善或添加字幕",
"wrong-answer": "抱歉,这个答案不正确。再试一次?",
"check-answer": "点击下方按钮,查看你的答案。",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "解决方案链接",
"github-link": "GitHub 链接",
"submit-and-go": "提交并访问下一个挑战",
@@ -339,6 +342,7 @@
"title": "支持我们的慈善组织",
"processing": "我们正在处理你的捐款。",
"redirecting": "重新引导中...",
"thanks": "Thanks for donating",
"thank-you": "谢谢你成为我们的支持者。",
"additional": "你可以使用这个链接 <0>{{url}}</0> 额外进行一次性捐款:",
"help-more": "帮助我们做更多",
@@ -462,7 +466,8 @@
"iframe-preview": "{{title}} 预览",
"iframe-alert": "通常,此链接会将你带到另一个网站!一切正常,这个链接指向:{{externalLink}}。",
"iframe-form-submit-alert": "通常这个表单将被提交!工作正常,这将被提交到:{{externalLink}}",
"document-notfound": "找不到文件"
"document-notfound": "找不到文件",
"slow-load-msg": "Looks like this is taking longer than usual, please try refreshing the page."
},
"icons": {
"gold-cup": "金奖杯",
@@ -507,7 +512,8 @@
"step": "步骤",
"steps": "步骤",
"steps-for": "{{blockTitle}} 的步骤",
"code-example": "{{codeName}} 代码示例"
"code-example": "{{codeName}} 代码示例",
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "申请认证之前,你必须先接受我们的《学术诚信条例》",

View File

@@ -553,7 +553,7 @@
"intro": [
"Until this point, you've only used JavaScript on the front end to add interactivity to a page, solve algorithm challenges, or build an SPA. But JavaScript can also be used on the back end, or server, to build entire web applications.",
"Today, one of the popular ways to build applications is through microservices, which are small, modular applications that work together to form a larger whole.",
"In the Back End Development and APIs Certification, you'll learn how to write back end apps with Node.js and npm (Node Package Manager). You'll also build web applications with the Express framework, and build a People Finder microservice with MongoDB and the Mongoose library."
"In the Back End Development and APIs Certification, you'll learn how to write back end apps with Node.js and npm. You'll also build web applications with the Express framework, and build a People Finder microservice with MongoDB and the Mongoose library."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "Project Euler",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "View",
"view-code": "View Code",
"view-project": "View Project",
"view-cert-title": "View {{certTitle}}",
"show-cert": "Show Certification",
"claim-cert": "Claim Certification",
"save-progress": "Save Progress",
@@ -52,7 +53,7 @@
"check-code": "Check Your Code (Ctrl + Enter)",
"check-code-2": "Check Your Code",
"reset": "Reset",
"reset-code": "Reset All Code",
"reset-step": "Reset This Step",
"help": "Help",
"get-help": "Get Help",
"watch-video": "Watch a Video",
@@ -157,7 +158,8 @@
"honesty": "Academic Honesty Policy",
"internet": "Your Internet Presence",
"portfolio": "Portfolio Settings",
"privacy": "Privacy Settings"
"privacy": "Privacy Settings",
"personal-info": "Personal Information"
},
"danger": {
"heading": "Danger Zone",
@@ -272,6 +274,7 @@
"add-subtitles": "Help improve or add subtitles",
"wrong-answer": "Sorry, that's not the right answer. Give it another try?",
"check-answer": "Click the button below to check your answer.",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "Solution Link",
"github-link": "GitHub Link",
"submit-and-go": "Submit and go to my next challenge",
@@ -463,7 +466,8 @@
"iframe-preview": "{{title}} preview",
"iframe-alert": "Normally this link would bring you to another website! It works. This is a link to: {{externalLink}}",
"iframe-form-submit-alert": "Normally this form would be submitted! It works. This will be submitted to: {{externalLink}}",
"document-notfound": "document not found"
"document-notfound": "document not found",
"slow-load-msg": "Looks like this is taking longer than usual, please try refreshing the page."
},
"icons": {
"gold-cup": "Gold Cup",
@@ -508,7 +512,8 @@
"step": "Step",
"steps": "Steps",
"steps-for": "Steps for {{blockTitle}}",
"code-example": "{{codeName}} code example"
"code-example": "{{codeName}} code example",
"opens-new-window": "Opens in new window"
},
"flash": {
"honest-first": "To claim a certification, you must first accept our academic honesty policy",

View File

@@ -553,7 +553,7 @@
"intro": [
"Hasta este punto, solo has usado JavaScript en la parte de front-end para agregar interactividad a una página, resolver los desafíos de algoritmos o construir un SPA. Pero JavaScript también se puede utilizar en el back-end, o servidor, para construir aplicaciones web completas.",
"Hoy en día, una de las formas populares para construir aplicaciones es a través de microservicios, que son pequeñas aplicaciones modulares que trabajan juntas para formar una aplicación más grande.",
"En la Certificación Desarrollo de Back End y APIs, aprenderás cómo escribir aplicaciones de back-end con Node.js y npm (Node Package Manager). También construirás aplicaciones web con el framework Express, y un microservicio \"People Finder\" con MongoDB y la biblioteca Mongoose."
"In the Back End Development and APIs Certification, you'll learn how to write back end apps with Node.js and npm. You'll also build web applications with the Express framework, and build a People Finder microservice with MongoDB and the Mongoose library."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "Project Euler",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "Ver",
"view-code": "Mostrar Código",
"view-project": "Mostrar Proyecto",
"view-cert-title": "View {{certTitle}}",
"show-cert": "Mostrar certificación",
"claim-cert": "Solicitar certificación",
"save-progress": "Guardar progreso",
@@ -52,7 +53,7 @@
"check-code": "Comprueba tu código (Ctrl + Enter)",
"check-code-2": "Comprueba tu código",
"reset": "Restablecer",
"reset-code": "Restablecer todo el código",
"reset-step": "Reset This Step",
"help": "Ayuda",
"get-help": "Obtener ayuda",
"watch-video": "Ver un Video",
@@ -157,7 +158,8 @@
"honesty": "Política de Honestidad Académica",
"internet": "Tu presencia en Internet",
"portfolio": "Ajustes de portafolio",
"privacy": "Ajustes de privacidad"
"privacy": "Ajustes de privacidad",
"personal-info": "Personal Information"
},
"danger": {
"heading": "Zona de peligro",
@@ -272,6 +274,7 @@
"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",
"solution-link": "Enlace a la solución",
"github-link": "Enlace de GitHub",
"submit-and-go": "Enviar y pasar a mi siguiente desafío",
@@ -339,6 +342,7 @@
"title": "Support our charity",
"processing": "Estamos procesando tu donación.",
"redirecting": "Redirigiendo...",
"thanks": "Thanks for donating",
"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",
@@ -462,7 +466,8 @@
"iframe-preview": "{{title}} Vista previa",
"iframe-alert": "¡Normalmente este link te llevaría a otro sitio web! Funciona. Este es un enlace a: {{externalLink}}",
"iframe-form-submit-alert": "¡Normalmente, se enviaría este formulario! Funciona. Esto se enviará a: {{externalLink}}",
"document-notfound": "documento no encontrado"
"document-notfound": "documento no encontrado",
"slow-load-msg": "Looks like this is taking longer than usual, please try refreshing the page."
},
"icons": {
"gold-cup": "Copa de Oro",
@@ -507,7 +512,8 @@
"step": "Paso",
"steps": "Pasos",
"steps-for": "Pasos para {{blockTitle}}",
"code-example": "ejemplo de código de {{codeName}}"
"code-example": "ejemplo de código de {{codeName}}",
"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.",

View File

@@ -553,7 +553,7 @@
"intro": [
"Bis zu diesem Punkt hast du JavaScript nur im Front-End verwendet, um einer Seite Interaktivität hinzuzufügen, Aufgaben mit Algorithmen zu lösen oder eine SPA zu bauen. Aber JavaScript kann auch im Back-End, also auf dem Server, verwendet werden, um ganze Webanwendungen zu erstellen.",
"Heutzutage ist eine der beliebtesten Arten, Anwendungen zu erstellen, Microservices. Das sind kleine, modulare Anwendungen, die zusammenarbeiten und ein größeres Ganzes bilden.",
"In der Zertifizierung für Back-End-Entwicklung und APIs lernst du, wie du Back-End-Apps mit Node.js und npm (Node Package Manager) schreibst. Du wirst auch Webanwendungen mit dem Express-Framework erstellen und einen Personensuche-Microservice mit MongoDB und der Mongoose-Bibliothek bauen."
"In the Back End Development and APIs Certification, you'll learn how to write back end apps with Node.js and npm. You'll also build web applications with the Express framework, and build a People Finder microservice with MongoDB and the Mongoose library."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "Projekt Euler",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "Anzeigen",
"view-code": "Code anschauen",
"view-project": "Projekt anschauen",
"view-cert-title": "View {{certTitle}}",
"show-cert": "Zertifikat anzeigen",
"claim-cert": "Zertifizierung anfordern",
"save-progress": "Fortschritt speichern",
@@ -52,7 +53,7 @@
"check-code": "Überprüfe deinen Code (Strg + Enter)",
"check-code-2": "Prüfe deinen Code",
"reset": "Zurücksetzen",
"reset-code": "Den Quellcode zurücksetzen",
"reset-step": "Reset This Step",
"help": "Hilfe",
"get-help": "Hilfe bekommen",
"watch-video": "Ein Video ansehen",
@@ -157,7 +158,8 @@
"honesty": "Akademischer Ehrlichkeitskodex",
"internet": "Dein Internetauftritt",
"portfolio": "Portfolio-Einstellungen",
"privacy": "Privatsphäre-Einstellungen"
"privacy": "Privatsphäre-Einstellungen",
"personal-info": "Personal Information"
},
"danger": {
"heading": "Gefahrenzone",
@@ -272,6 +274,7 @@
"add-subtitles": "Hilf mit Untertitel hinzuzufügen oder zu verbessern",
"wrong-answer": "Sorry, das ist nicht die richtige Antwort. Möchtest du noch einen Anlauf wagen?",
"check-answer": "Klick unten auf den Button, um deine Antwort zu überprüfen.",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "Lösungs-Link",
"github-link": "GitHub Link",
"submit-and-go": "Absenden und zur nächsten Herausforderung gehen",
@@ -339,6 +342,7 @@
"title": "Support our charity",
"processing": "Wir bearbeiten deine Spende.",
"redirecting": "Weiterleiten...",
"thanks": "Thanks for donating",
"thank-you": "Vielen Dank für deine Unterstützung.",
"additional": "Du kannst eine zusätzliche einmalige Spende in beliebiger Höhe über diesen Link tätigen: <0>{{url}}</0>",
"help-more": "Hilf uns, mehr zu tun",
@@ -462,7 +466,8 @@
"iframe-preview": "{{title}} Vorschau",
"iframe-alert": "Normalerweise würde dieser Link dich auf eine andere Website führen! Er funktioniert. Dies ist ein Link zu: {{externalLink}}",
"iframe-form-submit-alert": "Normalerweise würde dieses Formular übermittelt werden! Es funktioniert. Dies wird übermittelt an: {{externalLink}}",
"document-notfound": "Dokument wurde nicht gefunden"
"document-notfound": "Dokument wurde nicht gefunden",
"slow-load-msg": "Looks like this is taking longer than usual, please try refreshing the page."
},
"icons": {
"gold-cup": "Goldpokal",
@@ -507,7 +512,8 @@
"step": "Schritt",
"steps": "Schritte",
"steps-for": "Schritte für {{blockTitle}}",
"code-example": "{{codeName}} code example"
"code-example": "{{codeName}} code example",
"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",

View File

@@ -553,7 +553,7 @@
"intro": [
"Fino a questo punto, hai usato solo JavaScript sul front-end per aggiungere interattività a una pagina, risolvere sfide algoritmiche o costruire una Single Page Application. Ma JavaScript può essere utilizzato anche nel backend (cioè lato server), per costruire intere applicazioni web.",
"Oggi, uno dei modi più popolari per costruire applicazioni è attraverso i microservizi, che sono piccole applicazioni modulari che lavorano insieme per formare qualcosa di più grande.",
"Nella Certificazione Sviluppo Back End e API, imparerai a scrivere app per il backend con Node.js e npm (Node Package Manager). Costruirai anche applicazioni web con il framework Express, e costruirai un microservizio Trova Persone con MongoDB e la libreria Mongoose."
"Nella Certificazione Sviluppo Back End e API, imparerai a scrivere app per il backend con Node.js e npm. Costruirai anche applicazioni web con il framework Express, e costruirai un microservizio Trova Persone con MongoDB e la libreria Mongoose."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "Progetto Eulero",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "Visualizza",
"view-code": "Visualizza il Codice",
"view-project": "Visualizza il Progetto",
"view-cert-title": "Visualizza {{certTitle}}",
"show-cert": "Mostra la Certificazione",
"claim-cert": "Richiedi la Certificazione",
"save-progress": "Salva l'avanzamento",
@@ -52,7 +53,7 @@
"check-code": "Verifica il tuo codice (Ctrl + Invio)",
"check-code-2": "Verifica il tuo codice",
"reset": "Resetta",
"reset-code": "Resetta tutto il codice",
"reset-step": "Resetta questo step",
"help": "Guida",
"get-help": "Ottieni Aiuto",
"watch-video": "Guarda un video",
@@ -80,7 +81,7 @@
"big-heading-1": "Impara a programmare — gratis.",
"big-heading-2": "Costruisci progetti.",
"big-heading-3": "Ottieni certificazioni.",
"h2-heading": "Dal 2014, più di 40.000 laureati di freeCodeCamp.org hanno ottenuto posti di lavoro presso aziende tecnologiche, tra cui:",
"h2-heading": "Dal 2014, più di 40.000 studenti di freeCodeCamp.org hanno ottenuto posti di lavoro presso aziende tecnologiche, tra cui:",
"hero-img-description": "Studenti di un gruppo di studio locale di freeCodeCamp in Corea del Sud.",
"as-seen-in": "Hanno parlato di noi:",
"testimonials": {
@@ -157,7 +158,8 @@
"honesty": "Politica di Onestà Accademica",
"internet": "La Tua presenza su Internet",
"portfolio": "Impostazioni Portfolio",
"privacy": "Impostazioni Privacy"
"privacy": "Impostazioni Privacy",
"personal-info": "Personal Information"
},
"danger": {
"heading": "Zona di Pericolo",
@@ -272,6 +274,7 @@
"add-subtitles": "Aiuta a migliorare o aggiungi sottotitoli",
"wrong-answer": "Siamo spiacenti, non è la risposta giusta. Vuoi riprovare?",
"check-answer": "Fai clic sul pulsante qui sotto per controllare la tua risposta.",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "Link alla soluzione",
"github-link": "Link GitHub",
"submit-and-go": "Invia e vai alla prossima sfida",
@@ -339,6 +342,7 @@
"title": "Supporta il nostro ente",
"processing": "Stiamo elaborando la tua donazione.",
"redirecting": "Reindirizzamento...",
"thanks": "Grazie per la donazione",
"thank-you": "Grazie per essere un sostenitore.",
"additional": "Puoi effettuare una donazione una tantum aggiuntiva di qualsiasi importo utilizzando questo link: <0>{{url}}</0>",
"help-more": "Aiutaci a fare di più",
@@ -462,7 +466,8 @@
"iframe-preview": "Anteprima {{title}}",
"iframe-alert": "Solitamente questo link ti porterebbe su un altro sito web! Funziona. Questo è un link per {{externalLink}}",
"iframe-form-submit-alert": "Normalmente questo modulo sarebbe stato inviato! Funziona. Sarà inviato a: {{externalLink}}",
"document-notfound": "documento non trovato"
"document-notfound": "documento non trovato",
"slow-load-msg": "Sembra che sia necessario più tempo del solito, per favore prova ad aggiornare la pagina."
},
"icons": {
"gold-cup": "Coppa d'Oro",
@@ -507,7 +512,8 @@
"step": "Step",
"steps": "Step",
"steps-for": "Step per {{blockTitle}}",
"code-example": "{{codeName}} esempio di codice"
"code-example": "{{codeName}} esempio di codice",
"opens-new-window": "Apri in una nuova finestra"
},
"flash": {
"honest-first": "Per richiedere una certificazione, è necessario prima accettare la nostra politica di onestà accademica",

View File

@@ -553,7 +553,7 @@
"intro": [
"ここまでは、JavaScript をフロントエンドで使用して、ページに対話性を加えたり、アルゴリズムチャレンジを解決したり、SPA を構築したりしてきました。しかし JavaScript は、バックエンド (サーバー) でも使用でき、ウェブアプリケーション全体を構築することができます。",
"現在、アプリケーションを構築する一般的な方法の一つはマイクロサービスを使用する方法です。これは、小さなモジュール式のアプリケーションを組み合わせて、より大きな全体を形成する方法です。",
"バックエンド開発と API 認定講座では、Node.js と npm (Node Package Manager) を使用してバックエンドアプリを記述する方法を学習します。また、Express フレームワークでウェブアプリケーションを構築し、そして MongoDB と Mongoose ライブラリで People Finder マイクロサービスを構築します。"
"バックエンド開発と API 認定講座では、Node.js と npm を使用してバックエンドアプリを記述する方法を学習します。Express フレームワークでウェブアプリケーションを構築し、MongoDB と Mongoose ライブラリで People Finder マイクロサービスを構築します。"
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "プロジェクト・オイラー",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "表示",
"view-code": "コードを表示",
"view-project": "プロジェクトを表示",
"view-cert-title": "{{certTitle}}を表示",
"show-cert": "認定証を表示",
"claim-cert": "認定証を取得",
"save-progress": "進行状況を保存",
@@ -52,7 +53,7 @@
"check-code": "コードをチェック (Ctrl + Enter)",
"check-code-2": "コードをチェック",
"reset": "リセット",
"reset-code": "全てのコードをリセット",
"reset-step": "Reset This Step",
"help": "ヘルプ",
"get-help": "助けを求める",
"watch-video": "動画を見る",
@@ -157,7 +158,8 @@
"honesty": "学問的誠実性ポリシー",
"internet": "他サービスのリンク",
"portfolio": "ポートフォリオ設定",
"privacy": "プライバシー設定"
"privacy": "プライバシー設定",
"personal-info": "Personal Information"
},
"danger": {
"heading": "危険な操作",
@@ -272,6 +274,7 @@
"add-subtitles": "字幕の改善や追加を支援する",
"wrong-answer": "残念、正しい答えではありません。もう一度挑戦してみましょう。",
"check-answer": "以下のボタンをクリックして解答を確認してください。",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "回答のリンク",
"github-link": "GitHub のリンク",
"submit-and-go": "提出して次のチャレンジに進む",
@@ -339,6 +342,7 @@
"title": "当団体への支援",
"processing": "いただいた寄付を処理中です。",
"redirecting": "リダイレクト中...",
"thanks": "寄付ありがとうございます",
"thank-you": "サポーターとなってくださり、ありがとうございます。",
"additional": "任意の金額を、追加で 1 回ずつご寄付いただけるリンクはこちら: <0>{{url}}</0>",
"help-more": "私たちのさらなる活動をご支援ください",
@@ -375,8 +379,8 @@
"need-help": "現在または過去の寄付についてお困りでしょうか?",
"forward-receipt": "寄付の領収書のコピーを添えて、お問い合わせ内容を donors@freecodecamp.org までお送りください。",
"efficiency": "freeCodeCamp は非常に効率的な、教育分野の慈善団体です。",
"why-donate-1": "freeCodeCamp にご寄付いただくことにより、新しい技能を習して家を支えようとしている人々を助けることに繋がります。",
"why-donate-2": "また、あなた自身の技術スキルを高めるための、学習リソースの作成を支援することにもなります。",
"why-donate-1": "freeCodeCamp への寄付を通して、新しい技能を習して家を支えようと努力している人々を支援できます。",
"why-donate-2": "また、あなた自身の技術スキルを高める学習リソースの作成を支援することにもなります。",
"bigger-donation": "より多くの金額による一回の寄付や、小切手の送付、または他の方法をお考えでしょうか?",
"other-ways": "<0>当団体の使命を支援する方法</0> は他にも多数ございます。",
"failed-pay": "処理が完了しませんでした。再度お試しください。",
@@ -462,7 +466,8 @@
"iframe-preview": "{{title}} のプレビュー",
"iframe-alert": "このリンクは通常の環境では他のウェブサイトを開きます。正しく動作しています。これは {{externalLink}} へのリンクです。",
"iframe-form-submit-alert": "通常はこれでフォームが送信されます。正しく動作しています。フォームは {{externalLink}} へ送信されます。",
"document-notfound": "ドキュメントが見つかりませんでした"
"document-notfound": "ドキュメントが見つかりませんでした",
"slow-load-msg": "通常より処理に時間がかかっているようです。ページの再読み込みをお試しください。"
},
"icons": {
"gold-cup": "ゴールドカップ",
@@ -477,7 +482,7 @@
"hint": "ヒント",
"heart": "ハート",
"initial": "初期状態",
"input-reset": "Clear search terms",
"input-reset": "検索キーワードをクリア",
"info": "導入情報",
"spacer": "スペーサー",
"toggle": "トグルチェックマーク",
@@ -507,7 +512,8 @@
"step": "ステップ",
"steps": "ステップ一覧",
"steps-for": "「{{blockTitle}}」のステップ一覧",
"code-example": "{{codeName}} のコード例"
"code-example": "{{codeName}} のコード例",
"opens-new-window": "新しいウィンドウで開く"
},
"flash": {
"honest-first": "認定証を請求するには、まず学問的誠実性ポリシーに同意する必要があります。",
@@ -708,7 +714,7 @@
"Legacy Back End": "(レガシー) バックエンド",
"legacy-back-end": "バックエンド認定証",
"Legacy Data Visualization": "(レガシー) データ可視化",
"legacy-data-visualization": "Data Visualization Certification",
"legacy-data-visualization": "データ可視化認定証",
"Legacy Information Security and Quality Assurance": "(レガシー) 情報セキュリティと品質保証認定証",
"information-security-and-quality-assurance": "情報セキュリティと品質保証認定証",
"Legacy Full Stack Certification": "(レガシー) フルスタック認定証",
@@ -769,7 +775,7 @@
},
"signout": {
"heading": "アカウントからサインアウト",
"p1": "注意: サインアウトは進行状況が保存されません。",
"p1": "注意: サインアウト状態では進行状況が保存されません。",
"p2": "この操作では、このデバイスおよびブラウザーセッションのみを対象にサインアウトされます。サインアウトしてよろしいですか?",
"certain": "はい、サインアウトします",
"nevermind": "いいえ、サインアウトしません"

View File

@@ -553,7 +553,7 @@
"intro": [
"Até esse ponto, você usou JavaScript apenas no front-end para adicionar interatividade a uma página, resolver desafios de algoritmos ou construir uma SPA. Mas o JavaScript também pode ser usado no back-end, ou servidor, para construir aplicações web inteiras.",
"Hoje em dia, uma das formas populares de se criar aplicações é através do uso de microsserviços, que são pequenas aplicações modulares que trabalham em conjunto para formar uma aplicação maior inteira.",
"Na certificação de APIs e desenvolvimento de back-end, você aprenderá a escrever aplicações de back-end com o Node.js e o npm (Node Package Manager). Você também vai construir aplicações web com o framework Express e criará um microsserviço de localização de pessoas com o MongoDB e com a biblioteca Mongoose."
"Na certificação de APIs e desenvolvimento de back-end, você aprenderá a escrever aplicações de back-end com o Node.js e o npm. Você também vai construir aplicações web com o framework Express e criará um microsserviço de localização de pessoas com o MongoDB e com a biblioteca Mongoose."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "Projeto Euler",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "Visualizar",
"view-code": "Ver código",
"view-project": "Ver o projeto",
"view-cert-title": "Ver {{certTitle}}",
"show-cert": "Exibir certificado",
"claim-cert": "Solicitar certificação",
"save-progress": "Salvar progresso",
@@ -52,7 +53,7 @@
"check-code": "Verifique seu código (Ctrl + Enter)",
"check-code-2": "Verifique seu código",
"reset": "Redefinir",
"reset-code": "Redefinir todo o código",
"reset-step": "Reiniciar este passo",
"help": "Ajuda",
"get-help": "Obter ajuda",
"watch-video": "Assistir a um vídeo",
@@ -157,7 +158,8 @@
"honesty": "Política de honestidade acadêmica",
"internet": "Sua presença na internet",
"portfolio": "Configurações do portfólio",
"privacy": "Configurações de privacidade"
"privacy": "Configurações de privacidade",
"personal-info": "Personal Information"
},
"danger": {
"heading": "Zona de perigo",
@@ -272,6 +274,7 @@
"add-subtitles": "Ajudar a melhorar ou adicionar legendas",
"wrong-answer": "Desculpe, essa não é a resposta correta. Quer tentar novamente?",
"check-answer": "Clique no botão abaixo para verificar sua resposta.",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "Link da solução",
"github-link": "Link do GitHub",
"submit-and-go": "Enviar e ir para o meu próximo desafio",
@@ -339,6 +342,7 @@
"title": "Ajude nossa instituição",
"processing": "Estamos processando a sua doação.",
"redirecting": "Redirecionando...",
"thanks": "Agradecemos por sua doação",
"thank-you": "Obrigado pelo seu apoio.",
"additional": "Você pode fazer uma doação adicional de qualquer valor usando este link: <0>{{url}}</0>",
"help-more": "Ajude-nos a fazer mais",
@@ -462,7 +466,8 @@
"iframe-preview": "Visualização de {{title}}",
"iframe-alert": "Normalmente, este link levaria você para outro site da web! Funciona. Esse é um link para: {{externalLink}}",
"iframe-form-submit-alert": "Normalmente, esse formulário seria enviado! Funciona. Ele será enviado para: {{externalLink}}",
"document-notfound": "documento não encontrado"
"document-notfound": "documento não encontrado",
"slow-load-msg": "Parece que está demorando mais do que o normal. Tente atualizar a página."
},
"icons": {
"gold-cup": "Taça de ouro",
@@ -507,7 +512,8 @@
"step": "Passo",
"steps": "Passos",
"steps-for": "Passos para {{blockTitle}}",
"code-example": "Exemplo de código de {{codeName}}"
"code-example": "Exemplo de código de {{codeName}}",
"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",

View File

@@ -553,7 +553,7 @@
"intro": [
"До цього моменту ви використовували JavaScript тільки для фронтенду, аби зробити вебсторінку інтерактивнішою, розв'язати завдання з алгоритмами, або створити SPA. Але JavaScript можна використовувати й в бекенд, або на сервері, для створення цілих вебзастосунків.",
"Сьогодні один із найпопулярніших способів створення застосунків за допомогою мікросервісів, маленьких модульних застосунків, які разом формують єдине ціле.",
"У сертифікації «Розробка Back End та API» ви навчитеся писати бекенд програми за допомогою Node.js та npm (Node Package Manager). Також, ви створите вебзастосунок за допомогою фреймворку Express та мікросервіс для пошуку людей за допомогою MongoDB і Mongoose library."
"У сертифікації «Розробка Back End та API» ви навчитеся писати бекенд програми за допомогою Node.js та npm. Ви також створите вебзастосунок за допомогою фреймворку Express та мікросервіс для пошуку людей за допомогою MongoDB і бібліотеки Mongoose."
],
"note": "",
"blocks": {
@@ -778,6 +778,10 @@
"<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"]
},
"project-euler": {
"title": "Проєкт «Ейлер»",
"intro": [

View File

@@ -11,6 +11,7 @@
"view": "Перегляд",
"view-code": "Переглянути код",
"view-project": "Переглянути проєкт",
"view-cert-title": "Переглянути {{certTitle}}",
"show-cert": "Показати сертифікацію",
"claim-cert": "Отримати сертифікацію",
"save-progress": "Зберегти прогрес",
@@ -52,7 +53,7 @@
"check-code": "Перевірити код (Ctrl + Enter)",
"check-code-2": "Перевірити код",
"reset": "Скинути",
"reset-code": "Скинути весь код",
"reset-step": "Скинути цей крок",
"help": "Допомога",
"get-help": "Отримати допомогу",
"watch-video": "Подивитися відео",
@@ -157,7 +158,8 @@
"honesty": "Політика академічної доброчесності",
"internet": "Ваші соціальні мережі",
"portfolio": "Налаштування портфоліо",
"privacy": "Налаштування конфіденційності"
"privacy": "Налаштування конфіденційності",
"personal-info": "Personal Information"
},
"danger": {
"heading": "Небезпечна зона",
@@ -272,6 +274,7 @@
"add-subtitles": "Допомогти покращити або додати субтитри",
"wrong-answer": "Нам шкода, але це неправильна відповідь. Спробуєте ще раз?",
"check-answer": "Натисніть кнопку нижче, щоб перевірити свою відповідь.",
"assignment-not-complete": "Please finish the assignments",
"solution-link": "Посилання на рішення",
"github-link": "Посилання на GitHub",
"submit-and-go": "Відправити та перейти до мого наступного завдання",
@@ -339,6 +342,7 @@
"title": "Підтримайте нашу організацію",
"processing": "Ми опрацьовуємо ваш внесок.",
"redirecting": "Переадресація...",
"thanks": "Дякуємо за внесок",
"thank-you": "Дякуємо за вашу підтримку.",
"additional": "Ви можете зробити додатковий одноразовий внесок на будь-яку суму за цим посиланням: <0>{{url}}</0>",
"help-more": "Допоможіть нам розвиватися",
@@ -462,7 +466,8 @@
"iframe-preview": "Передперегляд {{title}}",
"iframe-alert": "Зазвичай це посилання перенесло б вас на інший вебсайт! Воно працює. Це посилання на: {{externalLink}}",
"iframe-form-submit-alert": "Зазвичай ця форма буде відправлена! Вона буде відправлена на: {{externalLink}}",
"document-notfound": "документ не знайдено"
"document-notfound": "документ не знайдено",
"slow-load-msg": "Схоже, це займає більше часу, ніж зазвичай. Будь ласка, спробуйте оновити сторінку."
},
"icons": {
"gold-cup": "Золотий кубок",
@@ -507,7 +512,8 @@
"step": "Крок",
"steps": "Кроки",
"steps-for": "Кроки для {{blockTitle}}",
"code-example": "Приклад коду {{codeName}}"
"code-example": "Приклад коду {{codeName}}",
"opens-new-window": "Відкривається у новому вікні"
},
"flash": {
"honest-first": "Щоб отримати сертифікацію, ви повинні спочатку прийняти нашу політику академічної доброчесності",

View File

@@ -38,7 +38,7 @@
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.18.6",
"@babel/standalone": "7.20.11",
"@babel/standalone": "7.20.12",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-brands-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
@@ -52,7 +52,7 @@
"@loadable/component": "5.15.2",
"@reach/router": "1.3.4",
"@sentry/gatsby": "6.19.7",
"@stripe/react-stripe-js": "1.16.1",
"@stripe/react-stripe-js": "1.16.3",
"@stripe/stripe-js": "1.46.0",
"@types/react-scrollable-anchor": "0.6.1",
"algoliasearch": "4.14.3",
@@ -67,9 +67,10 @@
"date-fns": "2.27.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.7",
"final-form": "4.20.7",
"final-form": "4.20.8",
"gatsby": "3.15.0",
"gatsby-cli": "3.15.0",
"gatsby-link": "3.15.0",
"gatsby-plugin-advanced-sitemap": "2.1.0",
"gatsby-plugin-create-client-paths": "3.15.0",
"gatsby-plugin-manifest": "3.15.0",
@@ -87,7 +88,7 @@
"nanoid": "3.3.4",
"normalize-url": "4.5.1",
"path-browserify": "1.0.1",
"postcss": "8.4.20",
"postcss": "8.4.21",
"prismjs": "1.29.0",
"process": "0.11.10",
"prop-types": "15.8.1",
@@ -96,11 +97,12 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-final-form": "6.5.9",
"react-ga": "3.3.1",
"react-gtm-module": "2.0.11",
"react-helmet": "6.1.0",
"react-hotkeys": "2.0.0",
"react-i18next": "11.18.6",
"react-instantsearch-dom": "6.38.1",
"react-instantsearch-core": "6.38.3",
"react-instantsearch-dom": "6.38.3",
"react-lazy-load": "3.1.14",
"react-monaco-editor": "0.40.0",
"react-redux": "5.1.2",
@@ -131,10 +133,11 @@
},
"devDependencies": {
"@babel/types": "7.20.7",
"@codesee/babel-plugin-instrument": "0.470.0",
"@codesee/tracker": "0.470.0",
"@codesee/babel-plugin-instrument": "0.479.0",
"@codesee/tracker": "0.479.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@types/react-gtm-module": "2.0.1",
"autoprefixer": "10.4.13",
"babel-plugin-transform-imports": "2.0.0",
"chokidar": "3.5.3",

View File

@@ -0,0 +1,25 @@
import TagManager from 'react-gtm-module';
import {
devAnalyticsId,
prodAnalyticsId,
prodAnalyticsESId
} from '../../../config/analytics-settings';
import envData from '../../../config/env.json';
const { deploymentEnv, clientLocale } = envData;
const analyticsIDSelector = () => {
if (deploymentEnv === 'staging') return devAnalyticsId;
else if (clientLocale === 'espanol') return prodAnalyticsESId;
else return prodAnalyticsId;
};
const gtmId = analyticsIDSelector();
if (typeof document !== `undefined`) {
TagManager.initialize({ gtmId });
}
export default TagManager;

View File

@@ -1,15 +0,0 @@
import ReactGA from 'react-ga';
import {
devAnalyticsId,
prodAnalyticsId
} from '../../../config/analytics-settings';
import envData from '../../../config/env.json';
const { deploymentEnv } = envData;
const analyticsId =
deploymentEnv === 'staging' ? devAnalyticsId : prodAnalyticsId;
ReactGA.initialize(analyticsId);
export default ReactGA;

View File

@@ -155,12 +155,8 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
) {
setIsDonationDisplayed(true);
executeGA({
type: 'event',
data: {
category: 'Donation View',
action: 'Displayed Certificate Donation',
nonInteraction: true
}
event: 'donationview',
action: 'Displayed Certificate Donation'
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -83,6 +83,7 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
<SolutionDisplayWidget
completedChallenge={completedProject}
dataCy={`${projectTitle} solution`}
projectTitle={projectTitle}
displayContext='certification'
showUserCode={showUserCode}
showProjectPreview={showProjectPreview}

View File

@@ -19,8 +19,7 @@ import {
userSelector,
isDonatingSelector,
signInLoadingSelector,
donationFormStateSelector,
isVariantASelector
donationFormStateSelector
} from '../../redux/selectors';
import Spacer from '../helpers/spacer';
import { Themes } from '../settings/theme';
@@ -87,7 +86,6 @@ type DonateFormProps = {
) => string;
theme: Themes;
updateDonationFormState: (state: DonationApprovalData) => unknown;
isVariantA: boolean;
paymentContext: PaymentContext;
};
@@ -97,22 +95,19 @@ const mapStateToProps = createSelector(
isDonatingSelector,
donationFormStateSelector,
userSelector,
isVariantASelector,
(
showLoading: DonateFormProps['showLoading'],
isSignedIn: DonateFormProps['isSignedIn'],
isDonating: DonateFormProps['isDonating'],
donationFormState: DonateFormState,
{ email, theme }: { email: string; theme: Themes },
isVariantA: boolean
{ email, theme }: { email: string; theme: Themes }
) => ({
isSignedIn,
isDonating,
showLoading,
donationFormState,
email,
theme,
isVariantA
theme
})
);
@@ -280,8 +275,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
t,
isMinimalForm,
isSignedIn,
isDonating,
isVariantA
isDonating
} = this.props;
const priorityTheme = defaultTheme ? defaultTheme : theme;
const isOneTime = donationDuration === 'one-time';
@@ -335,7 +329,6 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
processing={processing}
t={t}
theme={priorityTheme}
isVariantA={isVariantA}
/>
</>
)}

View File

@@ -66,16 +66,12 @@ function DonateModal({
useEffect(() => {
if (show) {
void playTone('donation');
executeGA({ type: 'modal', data: '/donation-modal' });
executeGA({ event: 'pageview', pagePath: '/donation-modal' });
executeGA({
type: 'event',
data: {
category: 'Donation View',
action: `Displayed ${
recentlyClaimedBlock ? 'block' : 'progress'
} donation modal`,
nonInteraction: true
}
event: 'donationview',
action: `Displayed ${
recentlyClaimedBlock ? 'Block' : 'Progress'
} Donation Modal`
});
}
}, [show, recentlyClaimedBlock, executeGA]);

View File

@@ -47,11 +47,13 @@ const FaqItem = (
const [isExpanded, setExpanded] = useState(false);
return (
<div className={`faq-item ${isExpanded ? 'open' : ''}`} key={key}>
<button className='map-title' onClick={() => setExpanded(!isExpanded)}>
<button
className='map-title'
onClick={() => setExpanded(!isExpanded)}
aria-expanded={isExpanded}
>
<Caret />
<h4>
<b>{title}</b>
</h4>
<h3>{title}</h3>
</button>
{isExpanded && (
<>

View File

@@ -526,6 +526,12 @@ a.patreon-button:hover {
margin-inline-start: 0.25em;
}
.faq-item h3 {
font-size: 1rem;
line-height: 1.5;
margin: 0;
}
.faq-item div {
width: 100%;
padding: 10px 15px;

View File

@@ -59,10 +59,7 @@ const {
deploymentEnv: 'staging' | 'live';
};
export class PaypalButton extends Component<
PaypalButtonProps,
PaypalButtonState
> {
class PaypalButton extends Component<PaypalButtonProps, PaypalButtonState> {
static displayName = 'PaypalButton';
state: PaypalButtonState = {
amount: defaultDonation.donationAmount,

View File

@@ -17,7 +17,6 @@ import { PaymentProvider } from '../../../../config/donation-settings';
import envData from '../../../../config/env.json';
import { Themes } from '../settings/theme';
import { DonationApprovalData, PostPayment } from './types';
import SecurityLockIcon from './security-lock-icon';
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
@@ -27,7 +26,6 @@ interface FormPropTypes {
t: (label: string) => string;
theme: Themes;
processing: boolean;
isVariantA: boolean;
}
interface Element {
@@ -43,8 +41,7 @@ const StripeCardForm = ({
t,
onDonationStateChange,
postPayment,
processing,
isVariantA
processing
}: FormPropTypes): JSX.Element => {
const [isSubmissionValid, setSubmissionValidity] = useState(true);
const [isTokenizing, setTokenizing] = useState(false);
@@ -166,7 +163,6 @@ const StripeCardForm = ({
disabled={!stripe || !elements || isSubmitting}
type='submit'
>
{!isVariantA && <SecurityLockIcon />}
{t('buttons.donate')}
</Button>
</Form>

View File

@@ -65,7 +65,7 @@ const mapDispatchToProps = {
openSignoutModal
};
export class NavLinks extends Component<NavLinksProps, NavlinkStates> {
class NavLinks extends Component<NavLinksProps, NavlinkStates> {
static displayName: string;
langButtonRef: React.RefObject<HTMLButtonElement>;
firstLangOptionRef: React.RefObject<HTMLButtonElement>;

View File

@@ -449,9 +449,7 @@ button.nav-link[aria-disabled='true'] {
}
.universal-nav-right .fcc_searchBar .ais-SearchBox-form {
width: 100vw;
max-width: unset;
padding: 0 15px;
}
/* In mobile layout, prevent search input from hanging around if the
@@ -472,12 +470,9 @@ button.nav-link[aria-disabled='true'] {
display: none;
}
.universal-nav-right .ais-SearchBox-input {
.universal-nav-right .ais-SearchBox-form {
width: calc(100vw - 17rem);
}
.universal-nav-right .ais-SearchBox-reset {
right: 291px;
margin-inline-start: 15px;
}
.universal-nav-right .fcc_searchBar .ais-Hits {
@@ -485,6 +480,9 @@ button.nav-link[aria-disabled='true'] {
}
.ais-SearchBox-input:focus {
box-sizing: content-box;
margin-inline-start: -30px;
padding-inline: 35px;
outline: 3px solid var(--blue-mid);
}
@@ -523,8 +521,6 @@ button.nav-link[aria-disabled='true'] {
}
.fcc_searchBar .ais-SearchBox-form {
display: flex;
justify-content: flex-end;
max-width: calc(100vw - 350px);
}
@@ -553,21 +549,13 @@ button.nav-link[aria-disabled='true'] {
}
.universal-nav-right .fcc_searchBar .ais-SearchBox-form {
width: 100%;
width: calc(100% - 30px);
}
.display-menu {
max-height: calc(100vh - var(--header-height) * 2);
}
.universal-nav-right .ais-SearchBox-input {
width: calc(100vw - 30px);
}
.universal-nav-right .ais-SearchBox-reset {
right: 15px;
}
.universal-nav-right .fcc_searchBar .ais-Hits {
width: calc(100% - 30px);
}

View File

@@ -8,7 +8,6 @@ import i18nTestConfig from '../../i18n/config-for-tests';
import { createStore } from '../redux/createStore';
import AppMountNotifier from './app-mount-notifier';
jest.mock('react-ga');
jest.unmock('react-i18next');
type Language = keyof typeof i18nextCodes;

View File

@@ -14,6 +14,12 @@ exports[`<Loader /> matches the fullScreen render snapshot 1`] = `
<div />
<div />
</div>
<br />
<p
class="text-center"
>
misc.slow-load-msg
</p>
</div>
`;

View File

@@ -2,8 +2,10 @@
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1em;
}
.fcc-loader .sk-spinner,

View File

@@ -1,27 +1,49 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Spinner from 'react-spinkit';
import './loader.css';
interface LoaderProps {
fullScreen?: boolean;
timeout?: number;
loaderDelay?: number;
messageDelay?: number;
}
function Loader({ fullScreen, timeout }: LoaderProps): JSX.Element {
const [showSpinner, setShowSpinner] = useState(!timeout);
function Loader({
fullScreen,
loaderDelay,
messageDelay
}: LoaderProps): JSX.Element {
const { t } = useTranslation();
const [showSpinner, setShowSpinner] = useState(!loaderDelay);
const [showMessage, setShowMessage] = useState(!messageDelay);
useEffect(() => {
let timerId: ReturnType<typeof setTimeout>;
if (!showSpinner) {
timerId = setTimeout(() => setShowSpinner(true), timeout);
if (loaderDelay) {
const timerId = setTimeout(() => setShowSpinner(true), loaderDelay);
return () => clearTimeout(timerId);
}
return () => clearTimeout(timerId);
}, [setShowSpinner, showSpinner, timeout]);
}, [loaderDelay]);
useEffect(() => {
if (messageDelay) {
const timerId = setTimeout(() => setShowMessage(true), messageDelay);
return () => clearTimeout(timerId);
}
}, [messageDelay]);
return (
<div
className={`fcc-loader ${fullScreen ? 'full-screen-wrapper' : ''}`}
data-testid='fcc-loader'
>
{showSpinner && <Spinner name='line-scale-pulse-out' />}
{showMessage && fullScreen && (
<>
<br />
<p className='text-center'>{t('misc.slow-load-msg')}</p>
</>
)}
</div>
);
}

View File

@@ -3,12 +3,11 @@ import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchUser, executeGA } from '../../redux/actions';
import { fetchUser } from '../../redux/actions';
import { isSignedInSelector } from '../../redux/selectors';
interface CertificationProps {
children?: React.ReactNode;
executeGA?: (args: { type: string; data: string }) => void;
fetchUser: () => void;
isSignedIn?: boolean;
pathname: string;
@@ -18,18 +17,15 @@ const mapStateToProps = createSelector(isSignedInSelector, isSignedIn => ({
isSignedIn
}));
const mapDispatchToProps = { fetchUser, executeGA };
const mapDispatchToProps = { fetchUser };
class CertificationLayout extends Component<CertificationProps> {
static displayName = 'CertificationLayout';
componentDidMount() {
const { isSignedIn, fetchUser, pathname } = this.props;
const { isSignedIn, fetchUser } = this.props;
if (!isSignedIn) {
fetchUser();
}
if (this.props.executeGA) {
this.props.executeGA({ type: 'page', data: pathname });
}
}
render(): JSX.Element {

View File

@@ -1,6 +1,7 @@
import React, { Component, ReactNode } 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';
@@ -16,8 +17,7 @@ import { isBrowser } from '../../../utils';
import {
fetchUser,
onlineStatusChange,
serverStatusChange,
executeGA
serverStatusChange
} from '../../redux/actions';
import {
isSignedInSelector,
@@ -76,8 +76,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
fetchUser,
removeFlashMessage,
onlineStatusChange,
serverStatusChange,
executeGA
serverStatusChange
},
dispatch
);
@@ -105,24 +104,14 @@ class DefaultLayout extends Component<DefaultLayoutProps> {
static displayName = 'DefaultLayout';
componentDidMount() {
const { isSignedIn, fetchUser, pathname, executeGA } = this.props;
const { isSignedIn, fetchUser } = this.props;
if (!isSignedIn) {
fetchUser();
}
executeGA({ type: 'page', data: pathname });
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
}
componentDidUpdate(prevProps: DefaultLayoutProps) {
const { pathname, executeGA } = this.props;
const { pathname: prevPathname } = prevProps;
if (pathname !== prevPathname) {
executeGA({ type: 'page', data: pathname });
}
}
componentWillUnmount() {
window.removeEventListener('online', this.updateOnlineStatus);
window.removeEventListener('offline', this.updateOnlineStatus);
@@ -157,7 +146,7 @@ class DefaultLayout extends Component<DefaultLayoutProps> {
const useSystemTheme = fetchState.complete && isSignedIn === false;
if (fetchState.pending) {
return <Loader fullScreen={true} />;
return <Loader fullScreen={true} messageDelay={5000} />;
}
return (

View File

@@ -1,3 +1,11 @@
/*
Increase the spacing in paragraphs
*/
[dir='rtl'] p {
line-height: 2rem;
}
/*
breadcrumbs section
*/
@@ -90,7 +98,7 @@ the "not" and "is" selector is used to stop it from affecting other section and
external fontawesome link icon
*/
[dir='rtl'] .nav-link > svg,
[dir='rtl'] .nav-link > .fa-up-right-from-square,
[dir='rtl'] td a[target='_blank'] > svg {
transform: rotate(270deg);
}
@@ -100,7 +108,7 @@ universal-nav-bar
and menu secion
*/
@media (min-width: 767.5px) {
@media (min-width: 601px) {
[dir='rtl'] .nav-list {
right: auto;
left: 0;
@@ -112,6 +120,16 @@ and menu secion
left: inherit;
right: 17px;
}
[dir='rtl'] .fcc_searchBar .ais-Hits {
left: auto;
right: 15px;
}
[dir='rtl'] .universal-nav-right .fcc_searchBar {
left: auto;
right: 0;
}
}
/* had to change overflow, because it clips the first words of the english articles */

View File

@@ -55,7 +55,9 @@ function CertButton({ username, cert }: CertButtonProps): JSX.Element {
to={`/certification/${username}/${cert.certSlug}`}
data-cy='claimed-certification'
>
View {t(`certification.title.${cert.certSlug}`)}
{t('buttons.view-cert-title', {
certTitle: t(`certification.title.${cert.certSlug}`)
})}
</Link>
</Col>
</Row>

View File

@@ -7,7 +7,6 @@ import { render, screen } from '../../../../utils/test-utils';
import { createStore } from '../../../redux/createStore';
import TimeLine from './time-line';
jest.mock('react-ga');
const store = createStore();
beforeEach(() => {
@@ -57,7 +56,9 @@ describe('<TimeLine />', () => {
it('Render button when only solution is present', () => {
// @ts-expect-error
render(<TimeLine {...propsForOnlySolution} />, store);
const showViewButton = screen.getByRole('link', { name: 'buttons.view' });
const showViewButton = screen.getByRole('link', {
name: 'buttons.view settings.labels.solution-for (aria.opens-new-window)'
});
expect(showViewButton).toHaveAttribute(
'href',
'https://github.com/freeCodeCamp/freeCodeCamp'
@@ -84,7 +85,9 @@ describe('<TimeLine />', () => {
// @ts-expect-error
render(<TimeLine {...propsForOnlySolution} />, store);
const viewButtons = screen.getAllByRole('button', { name: 'buttons.view' });
const viewButtons = screen.getAllByRole('button', {
name: 'buttons.view settings.labels.solution-for'
});
viewButtons.forEach(button => {
expect(button).toBeInTheDocument();
});

View File

@@ -103,12 +103,15 @@ function TimelineInner({
function renderViewButton(
completedChallenge: CompletedChallenge
): React.ReactNode {
const { id } = completedChallenge;
const projectTitle = idToNameMap.get(id)?.challengeTitle || '';
return (
<SolutionDisplayWidget
completedChallenge={completedChallenge}
projectTitle={projectTitle}
showUserCode={() => viewSolution(completedChallenge)}
showProjectPreview={() => viewProject(completedChallenge)}
displayContext={'timeline'}
displayContext='timeline'
></SolutionDisplayWidget>
);
}

View File

@@ -614,7 +614,6 @@ a[class^='ais-'] {
}
.ais-SearchBox-submit,
.ais-SearchBox-reset {
position: absolute;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@@ -623,15 +622,7 @@ a[class^='ais-'] {
height: 20px;
}
.ais-SearchBox-reset {
right: 0;
}
.ais-SearchBox-resetIcon,
.ais-SearchBox-loadingIcon {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
margin-inline-start: 10px;
}
.ais-SearchBox-submitIcon path,
.ais-SearchBox-resetIcon path {

View File

@@ -14,16 +14,15 @@
}
.ais-SearchBox-input {
padding: 0 10px;
padding-block: 0;
padding-inline-start: 5px;
font-size: 18px;
display: inline-block;
margin-top: 6px;
height: 26px;
}
.ais-SearchBox-submit,
.ais-SearchBox-reset {
margin-top: 6px;
height: 26px;
width: 26px;
background-color: var(--gray-75);
@@ -33,6 +32,10 @@
padding-inline: 3px;
}
.ais-SearchBox-reset {
padding-block: 0 2px;
}
.fcc_searchBar .ais-SearchBox-input,
.fcc_searchBar .ais-Hits {
z-index: 100;
@@ -40,10 +43,6 @@
color: var(--gray-00);
}
.fcc_searchBar .ais-SearchBox-input {
padding-inline: 2em;
}
.fcc_searchBar .ais-Hits {
left: 15px;
}
@@ -125,15 +124,26 @@ and arrow keys */
font-weight: 300;
}
.ais-SearchBox-input {
width: calc(100vw - 350px);
.fcc_searchBar .ais-SearchBox-form {
display: grid;
grid-template-columns: 26px auto 36px;
grid-template-areas: 'submit input reset';
margin-bottom: 0;
gap: 0.25em;
margin-top: 6px;
background-color: var(--gray-75);
}
.fcc_searchBar .ais-SearchBox-form {
margin-bottom: 0;
display: flex;
flex-direction: row-reverse;
gap: 0.25em;
.ais-SearchBox-input {
grid-area: input;
}
.ais-SearchBox-submit {
grid-area: submit;
}
.ais-SearchBox-reset {
grid-area: reset;
}
@media (min-width: 981px) {
@@ -152,8 +162,6 @@ and arrow keys */
left: 0px;
}
.fcc_searchBar .ais-SearchBox-form {
display: flex;
justify-content: flex-end;
top: auto;
}
}

View File

@@ -15,6 +15,7 @@ import SoundSettings from './sound';
import ThemeSettings, { Themes } from './theme';
import UsernameSettings from './username';
import KeyboardShortcutsSettings from './keyboard-shortcuts';
import SectionHeader from './section-header';
type FormValues = {
name: string;
@@ -212,6 +213,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
<div className='about-settings'>
<UsernameSettings username={username} />
<br />
<SectionHeader>{t('settings.headings.personal-info')}</SectionHeader>
<FullWidthRow>
<form id='camper-identity' onSubmit={this.handleSubmit}>
<FormGroup controlId='about-name'>

View File

@@ -213,9 +213,10 @@ export class CertificationSettings extends Component {
<SolutionDisplayWidget
completedChallenge={completedProject}
dataCy={projectTitle}
projectTitle={projectTitle}
showUserCode={showUserCode}
showProjectPreview={showProjectPreview}
displayContext={'settings'}
displayContext='settings'
></SolutionDisplayWidget>
);
};
@@ -285,7 +286,8 @@ export class CertificationSettings extends Component {
data-cy={`btn-for-${certSlug}`}
onClick={createClickHandler(certSlug)}
>
{isCert ? t('buttons.show-cert') : t('buttons.claim-cert')}
{isCert ? t('buttons.show-cert') : t('buttons.claim-cert')}{' '}
<span className='sr-only'>{certName}</span>
</Button>
</td>
</tr>

View File

@@ -20,7 +20,7 @@ describe('<certification />', () => {
expect(
screen.getByRole('link', {
name: 'buttons.show-cert'
name: /^buttons.show-cert\s+\S+/
})
).toHaveAttribute(
'href',
@@ -33,7 +33,7 @@ describe('<certification />', () => {
renderWithRedux(<CertificationSettings {...defaultTestProps} />);
const allClaimedCerts = screen.getAllByRole('link', {
name: 'buttons.show-cert'
name: /^buttons.show-cert\s+\S+/
});
allClaimedCerts.forEach(cert => {
@@ -49,7 +49,7 @@ describe('<certification />', () => {
renderWithRedux(<CertificationSettings {...defaultTestProps} />);
const allClaimedCerts = screen.getAllByRole('link', {
name: 'buttons.show-cert'
name: /^buttons.show-cert\s+\S+/
});
allClaimedCerts.forEach(cert => {
@@ -65,7 +65,7 @@ describe('<certification />', () => {
expect(
screen.getByRole('link', {
name: 'buttons.view'
name: 'buttons.view settings.labels.solution-for (aria.opens-new-window)'
})
).toHaveAttribute('href', 'https://github.com/freeCodeCamp/freeCodeCamp');
});

View File

@@ -1,17 +1,15 @@
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
Button,
DropdownButton,
MenuItem
} from '@freecodecamp/react-bootstrap';
import { Button, Dropdown, MenuItem } from '@freecodecamp/react-bootstrap';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CompletedChallenge } from '../../redux/prop-types';
import { getSolutionDisplayType } from '../../utils/solution-display-type';
import './solution-display-widget.css';
interface Props {
completedChallenge: CompletedChallenge;
dataCy?: string;
projectTitle: string;
showUserCode: () => void;
showProjectPreview?: () => void;
displayContext: 'timeline' | 'settings' | 'certification';
@@ -20,6 +18,7 @@ interface Props {
export function SolutionDisplayWidget({
completedChallenge,
dataCy,
projectTitle,
showUserCode,
showProjectPreview,
displayContext
@@ -29,37 +28,48 @@ export function SolutionDisplayWidget({
const viewText = t('buttons.view');
const viewCode = t('buttons.view-code');
const viewProject = t('buttons.view-project');
// We need to add a random number for dropdown button id's since there may be
// two dropdowns for the same project on the page.
const randomIdSuffix = Math.floor(Math.random() * 1_000_000);
const ShowFilesSolutionForCertification = (
<Button block={true} data-cy={dataCy} onClick={showUserCode}>
{t('buttons.view')}
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })}
</span>
</Button>
);
const ShowProjectAndGithubLinkForCertification = (
<DropdownButton
block={true}
bsStyle='primary'
className='btn-invert'
id={`dropdown-for-${id}`}
title={t('buttons.view')}
>
<MenuItem
bsStyle='primary'
href={solution ?? ''}
rel='noopener noreferrer'
target='_blank'
>
{t('certification.project.solution')}
</MenuItem>
<MenuItem
bsStyle='primary'
href={githubLink}
rel='noopener noreferrer'
target='_blank'
>
{t('certification.project.source')}
</MenuItem>
</DropdownButton>
<Dropdown id={`dropdown-for-${id}-${randomIdSuffix}`}>
<Dropdown.Toggle block={true} bsStyle='primary' className='btn-invert'>
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })}
</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem
bsStyle='primary'
href={solution ?? ''}
rel='noopener noreferrer'
target='_blank'
>
{t('certification.project.solution')}
<span className='sr-only'>({t('aria.opens-new-window')})</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</MenuItem>
<MenuItem
bsStyle='primary'
href={githubLink}
rel='noopener noreferrer'
target='_blank'
>
{t('certification.project.source')}
<span className='sr-only'>({t('aria.opens-new-window')})</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</MenuItem>
</Dropdown.Menu>
</Dropdown>
);
const ShowProjectLinkForCertification = (
<Button
@@ -69,7 +79,12 @@ export function SolutionDisplayWidget({
rel='noopener noreferrer'
target='_blank'
>
{t('buttons.view')}
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })} (
{t('aria.opens-new-window')})
</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</Button>
);
const MissingSolutionComponentForCertification = (
@@ -81,57 +96,67 @@ export function SolutionDisplayWidget({
bsStyle='primary'
className='btn-invert'
data-cy={dataCy}
id={`btn-for-${id}`}
onClick={showUserCode}
>
{viewText} <FontAwesomeIcon icon={faExternalLinkAlt} />
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })}
</span>
</Button>
);
const ShowMultifileProjectSolution = (
<div className='solutions-dropdown'>
<DropdownButton
block={true}
bsStyle='primary'
className='btn-invert'
id={`dropdown-for-${id}`}
title={t('buttons.view')}
>
<MenuItem bsStyle='primary' onClick={showUserCode}>
{viewCode}
</MenuItem>
<MenuItem bsStyle='primary' onClick={showProjectPreview}>
{viewProject}
</MenuItem>
</DropdownButton>
<Dropdown id={`dropdown-for-${id}-${randomIdSuffix}`}>
<Dropdown.Toggle block={true} bsStyle='primary' className='btn-invert'>
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })}
</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem bsStyle='primary' onClick={showUserCode}>
{viewCode}
</MenuItem>
<MenuItem bsStyle='primary' onClick={showProjectPreview}>
{viewProject}
</MenuItem>
</Dropdown.Menu>
</Dropdown>
</div>
);
const ShowProjectAndGithubLinks = (
<div className='solutions-dropdown'>
<DropdownButton
block={true}
bsStyle='primary'
className='btn-invert'
id={`dropdown-for-${id}`}
title={viewText}
>
<MenuItem
bsStyle='primary'
href={solution}
rel='noopener noreferrer'
target='_blank'
>
{t('buttons.frontend')}
</MenuItem>
<MenuItem
bsStyle='primary'
href={githubLink}
rel='noopener noreferrer'
target='_blank'
>
{t('buttons.backend')}
</MenuItem>
</DropdownButton>
<Dropdown id={`dropdown-for-${id}-${randomIdSuffix}`}>
<Dropdown.Toggle block={true} bsStyle='primary' className='btn-invert'>
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })}
</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem
bsStyle='primary'
href={solution}
rel='noopener noreferrer'
target='_blank'
>
{t('buttons.frontend')}{' '}
<span className='sr-only'>({t('aria.opens-new-window')})</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</MenuItem>
<MenuItem
bsStyle='primary'
href={githubLink}
rel='noopener noreferrer'
target='_blank'
>
{t('buttons.backend')}{' '}
<span className='sr-only'>({t('aria.opens-new-window')})</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</MenuItem>
</Dropdown.Menu>
</Dropdown>
</div>
);
const ShowProjectLink = (
@@ -140,11 +165,15 @@ export function SolutionDisplayWidget({
bsStyle='primary'
className='btn-invert'
href={solution}
id={`btn-for-${id}`}
rel='noopener noreferrer'
target='_blank'
>
{viewText} <FontAwesomeIcon icon={faExternalLinkAlt} />
{viewText}{' '}
<span className='sr-only'>
{t('settings.labels.solution-for', { projectTitle })} (
{t('aria.opens-new-window')})
</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</Button>
);
const MissingSolutionComponent =

View File

@@ -0,0 +1,3 @@
.solutions-dropdown a[role='menuitem'] {
text-decoration: none;
}

View File

@@ -22,14 +22,10 @@ import { signInLoadingSelector, userSelector } from '../redux/selectors';
import { PaymentContext } from '../../../config/donation-settings';
export interface ExecuteGaArg {
type: string;
data: {
category: string;
action: string;
nonInteraction?: boolean;
label?: string;
value?: number;
};
event: string;
action: string;
duration?: string;
amount?: number;
}
interface DonatePageProps {
executeGA: (arg: ExecuteGaArg) => void;
@@ -59,12 +55,8 @@ function DonatePage({
}: DonatePageProps) {
useEffect(() => {
executeGA({
type: 'event',
data: {
category: 'Donation View',
action: `Displayed donate page`,
nonInteraction: true
}
event: 'donationview',
action: `Displayed Donate Page`
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@@ -11,6 +11,7 @@ import Intro from '../components/Intro';
import Map from '../components/Map';
import { Spacer } from '../components/helpers';
import LearnLayout from '../components/layouts/learn';
import { defaultDonation } from '../../../config/donation-settings';
import {
isSignedInSelector,
userSelector,
@@ -82,11 +83,10 @@ function LearnPage({
const onDonationAlertClick = () => {
executeGA({
type: 'event',
data: {
category: 'Donation Related',
action: `learn donation alert click`
}
event: 'donationrelated',
action: `Learn Donation Alert Click`,
duration: defaultDonation.donationDuration,
amount: defaultDonation.donationAmount
});
};
return (

View File

@@ -23,7 +23,6 @@ export const actionTypes = createTypes(
'showCodeAlly',
'submitComplete',
'updateComplete',
'updateCurrentChallengeId',
'updateFailed',
'updateDonationFormState',
'updateUserToken',

View File

@@ -90,9 +90,5 @@ export const hideCodeAlly = createAction(actionTypes.hideCodeAlly);
export const showCodeAlly = createAction(actionTypes.showCodeAlly);
export const tryToShowCodeAlly = createAction(actionTypes.tryToShowCodeAlly);
export const updateCurrentChallengeId = createAction(
actionTypes.updateCurrentChallengeId
);
export const closeSignoutModal = createAction(actionTypes.closeSignoutModal);
export const openSignoutModal = createAction(actionTypes.openSignoutModal);

View File

@@ -103,16 +103,13 @@ export function* postChargeSaga({
}
yield put(
executeGA({
type: 'event',
data: {
category:
paymentProvider === PaymentProvider.Patreon
? 'Donation Related'
: 'Donation',
action: stringifyDonationEvents(paymentContext, paymentProvider),
label: duration,
value: amount
}
event:
paymentProvider === PaymentProvider.Patreon
? 'donationrelated'
: 'donation',
action: stringifyDonationEvents(paymentContext, paymentProvider),
duration,
amount
})
);
} catch (error) {

View File

@@ -22,13 +22,10 @@ const postChargeDataMock = {
};
const analyticsDataMock = {
type: 'event',
data: {
category: 'Donation',
action: 'Donate Page Stripe Payment Submission',
label: 'monthly',
value: '500'
}
event: 'donation',
action: 'Donate Page Stripe Payment Submission',
duration: 'monthly',
amount: '500'
};
describe('donation-saga', () => {
@@ -48,7 +45,7 @@ describe('donation-saga', () => {
};
const stripeCardAnalyticsDataMock = analyticsDataMock;
stripeCardAnalyticsDataMock.data.action =
stripeCardAnalyticsDataMock.action =
'Donate Page Stripe Card Payment Submission';
const { paymentMethodId, amount, duration } = stripeCardDataMock.payload;
@@ -68,8 +65,7 @@ describe('donation-saga', () => {
};
const paypalAnalyticsDataMock = analyticsDataMock;
paypalAnalyticsDataMock.data.action =
'Donate Page Paypal Payment Submission';
paypalAnalyticsDataMock.action = 'Donate Page Paypal Payment Submission';
const storeMock = {
app: {
@@ -94,8 +90,7 @@ describe('donation-saga', () => {
};
const paypalAnalyticsDataMock = analyticsDataMock;
paypalAnalyticsDataMock.data.action =
'Donate Page Paypal Payment Submission';
paypalAnalyticsDataMock.action = 'Donate Page Paypal Payment Submission';
const storeMock = {
app: {}
@@ -117,9 +112,8 @@ describe('donation-saga', () => {
};
const patreonAnalyticsDataMock = analyticsDataMock;
patreonAnalyticsDataMock.data.action =
'Donate Page Patreon Payment Redirection';
patreonAnalyticsDataMock.data.category = 'Donation Related';
patreonAnalyticsDataMock.action = 'Donate Page Patreon Payment Redirection';
patreonAnalyticsDataMock.event = 'donationrelated';
return expectSaga(postChargeSaga, patreonDataMock)
.not.call.fn(addDonation)
.not.call.fn(postChargeStripeCard)

View File

@@ -1,5 +1,5 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import store from 'store';
import { getSessionUser, getUserProfile } from '../utils/ajax';
import {
fetchProfileForUserComplete,
@@ -19,6 +19,13 @@ function* fetchSessionUser() {
data: { user = {}, result = '', sessionMeta = {} }
} = yield call(getSessionUser);
const appUser = user[result] || {};
const [userId] = Object.keys(user);
const sound = user[userId].sound;
store.set('fcc-sound', sound);
yield put(
fetchUserComplete({ user: appUser, username: result, sessionMeta })
);

View File

@@ -1,57 +1,34 @@
/* eslint-disable camelcase */
import { all, call, select, takeEvery } from 'redux-saga/effects';
import { all, call, takeEvery } from 'redux-saga/effects';
import TagManager from '../analytics';
import { aBTestConfig } from '../../../config/donation-settings';
import ga from '../analytics';
import { emailToABVariant } from '../utils/A-B-tester';
import {
completedChallengesSelector,
completionCountSelector,
emailSelector,
recentlyClaimedBlockSelector
} from './selectors';
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
function* callGaType({ payload: { type, data } }) {
if (
type === 'event' &&
data.category.toLowerCase().includes('donation') &&
aBTestConfig.isTesting
) {
const email = yield select(emailSelector);
// a b test results are only reported when user is signed in and has email
if (email) {
const completedChallengeTotal = yield select(completedChallengesSelector);
const completedChallengeSession = yield select(completionCountSelector);
let viewType = null;
// set the modal type
if (data.action.toLowerCase().includes('modal')) {
const recentlyClaimedBlock = yield select(recentlyClaimedBlockSelector);
viewType = recentlyClaimedBlock ? 'block' : 'progress';
function* callGaType({
payload: { action, duration, amount, event, pagePath }
}) {
if (event === 'pageview') {
yield call(TagManager.dataLayer, {
dataLayer: {
event,
pagePath
}
const customDimensions = {
// URL;
dimension1: window.location.href,
// Challenges_Completed_Session
dimension2: completedChallengeSession,
// Challenges_Completed_Total
dimension3: completedChallengeTotal.length,
// Test_Type
dimension4: aBTestConfig.type,
// Test_Variation
dimension5: emailToABVariant(email).isVariantA ? 'A' : 'B',
// View_Type
dimension6: viewType
};
ga.set(customDimensions);
}
});
} else if (event === 'donationview') {
yield call(TagManager.dataLayer, {
dataLayer: {
event,
action
}
});
} else {
// donation and donationrelated
yield call(TagManager.dataLayer, {
dataLayer: {
event,
action,
duration,
amount
}
});
}
yield call(GaTypes[type], data);
}
export function* createGaSaga(types) {

View File

@@ -1,24 +1,21 @@
import { expectSaga } from 'redux-saga-test-plan';
import ga from '../analytics';
import TagManager from '../analytics';
import { actionTypes } from './action-types';
import { createGaSaga } from './ga-saga';
jest.mock('../analytics');
describe('ga-saga', () => {
it('calls GA after executeGA action', () => {
const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview };
const mockEventPayload = {
type: 'event',
data: {
category: 'Map Challenge Click',
action: '/learn'
}
action: 'Learn Donation Alert Click',
amount: 500,
duration: 'month',
event: 'donationrelated'
};
return (
expectSaga(createGaSaga, actionTypes)
// Assert that the `call` with expected paramater will eventually happen.
.call(GaTypes.event, mockEventPayload.data)
.call(TagManager.dataLayer, { dataLayer: mockEventPayload })
// Dispatch any actions that the saga will `take`.
.dispatch({ type: actionTypes.executeGA, payload: mockEventPayload })

View File

@@ -23,7 +23,7 @@ import { createShowCertSaga } from './show-cert-saga';
import updateCompleteEpic from './update-complete-epic';
import { createUserTokenSaga } from './user-token-saga';
export const defaultFetchState = {
const defaultFetchState = {
pending: true,
complete: false,
errored: false,
@@ -41,7 +41,7 @@ export const defaultDonationFormState = {
}
};
export const initialState = {
const initialState = {
appUsername: '',
recentlyClaimedBlock: null,
canRequestProgressDonation: true,

View File

@@ -1,78 +1,8 @@
import PropTypes from 'prop-types';
import { HandlerProps } from 'react-reflex';
import { SuperBlocks } from '../../../config/certification-settings';
import { Themes } from '../components/settings/theme';
import { certMap } from '../resources/cert-and-project-map';
export const UserPropType = PropTypes.shape({
about: PropTypes.string,
completedChallenges: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
solution: PropTypes.string,
githubLink: PropTypes.string,
challengeType: PropTypes.number,
completedDate: PropTypes.number,
challengeFiles: PropTypes.array
})
),
email: PropTypes.string,
githubProfile: PropTypes.string,
is2018DataVisCert: PropTypes.bool,
isApisMicroservicesCert: PropTypes.bool,
isBackEndCert: PropTypes.bool,
isDataVisCert: PropTypes.bool,
isEmailVerified: PropTypes.bool,
isFrontEndCert: PropTypes.bool,
isFrontEndLibsCert: PropTypes.bool,
isFullStackCert: PropTypes.bool,
isHonest: PropTypes.bool,
isInfosecQaCert: PropTypes.bool,
isQaCertV7: PropTypes.bool,
isInfosecCertV7: PropTypes.bool,
isJsAlgoDataStructCert: PropTypes.bool,
isRelationalDatabaseCertV8: PropTypes.bool,
isRespWebDesignCert: PropTypes.bool,
isSciCompPyCertV7: PropTypes.bool,
isDataAnalysisPyCertV7: PropTypes.bool,
isMachineLearningPyCertV7: PropTypes.bool,
linkedin: PropTypes.string,
location: PropTypes.string,
name: PropTypes.string,
picture: PropTypes.string,
points: PropTypes.number,
portfolio: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string,
url: PropTypes.string,
image: PropTypes.string,
description: PropTypes.string
})
),
savedChallenges: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
challengeFiles: PropTypes.array
})
),
sendQuincyEmail: PropTypes.bool,
sound: PropTypes.bool,
theme: PropTypes.string,
keyboardShortcuts: PropTypes.bool,
twitter: PropTypes.string,
username: PropTypes.string,
website: PropTypes.string
});
export const CurrentCertsPropType = PropTypes.arrayOf(
PropTypes.shape({
show: PropTypes.bool,
title: PropTypes.string,
certSlug: PropTypes.string
})
);
export type Steps = {
isHonest?: boolean;
currentCerts?: Array<CurrentCert>;
@@ -96,7 +26,7 @@ export type MarkdownRemark = {
superBlock: SuperBlocks;
// TODO: make enum like superBlock
certification: string;
title: typeof certMap[number]['title'];
title: (typeof certMap)[number]['title'];
};
headings: [
{
@@ -117,7 +47,11 @@ export type MarkdownRemark = {
};
};
type Question = { text: string; answers: string[]; solution: number };
type Question = {
text: string;
answers: string[];
solution: number;
};
type Fields = { slug: string; blockName: string; tests: Test[] };
type Required = {
link: string;
@@ -185,6 +119,7 @@ export type ChallengeNode = {
isPrivate: boolean;
order: number;
question: Question;
assignments: string[];
required: Required[];
solutions: {
[T in FileKey]: FileKeyChallenge;

View File

@@ -17,7 +17,7 @@ import {
import { saveChallengeComplete } from './actions';
import { savedChallengesSelector } from './selectors';
export function* saveChallengeSaga() {
function* saveChallengeSaga() {
const { id, challengeType } = yield select(challengeMetaSelector);
const { challengeFiles } = yield select(challengeDataSelector);
const savedChallenges = yield select(savedChallengesSelector);

View File

@@ -1,5 +1,4 @@
import { SuperBlocks } from '../../../config/certification-settings';
import { emailToABVariant } from '../utils/A-B-tester';
import { ns as MainApp } from './action-types';
export const savedChallengesSelector = state =>
@@ -13,13 +12,7 @@ export const currentChallengeIdSelector = state =>
state[MainApp].currentChallengeId;
export const emailSelector = state => userSelector(state).email;
export const isVariantASelector = state => {
const email = emailSelector(state);
// if the user is not signed in and the user info is not available.
// always return A the control variant
if (!email) return true;
return emailToABVariant(email).isVariantA;
};
export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[MainApp].isOnline;
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
@@ -217,5 +210,3 @@ export const userSelector = state => {
return state[MainApp].user[username] || {};
};
export const sessionMetaSelector = state => state[MainApp].sessionMeta;

View File

@@ -49,17 +49,3 @@ interface DefaultDonationFormState {
success: boolean;
error: null | string;
}
export const defaultFetchState = {
pending: true,
complete: false,
errored: false,
error: null
};
export const defaultDonationFormState = {
redirecting: false,
processing: false,
success: false,
error: ''
};

View File

@@ -727,7 +727,7 @@ function getJavaScriptAlgoPath(project: string) {
}
const titles = certMap.map(({ title }) => title);
type Title = typeof titles[number];
type Title = (typeof titles)[number];
const legacyProjectMap: Partial<Record<Title, unknown>> = {};
const projectMap: Partial<Record<Title, unknown>> = {};

View File

@@ -1203,10 +1203,15 @@ const Editor = (props: EditorProps): JSX.Element => {
});
}
const { theme } = props;
const editorTheme = theme === Themes.Night ? 'vs-dark-custom' : 'vs-custom';
const { isSignedIn, theme } = props;
const preferDarkScheme = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
const isDarkTheme =
theme === Themes.Night || (preferDarkScheme && !isSignedIn);
const editorTheme = isDarkTheme ? 'vs-dark-custom' : 'vs-custom';
return (
<Suspense fallback={<Loader timeout={600} />}>
<Suspense fallback={<Loader loaderDelay={600} />}>
<span className='notranslate'>
<MonacoEditor
editorDidMount={editorDidMount}

View File

@@ -190,8 +190,8 @@ const LowerJaw = ({
<div className='lower-jaw-icon-bar'>
<button
className='btn fade-in'
title={t('buttons.reset-code')}
aria-label={t('buttons.reset-code')}
title={t('buttons.reset-step')}
aria-label={t('buttons.reset-step')}
data-cy='reset-code-button'
onClick={openResetModal}
>

View File

@@ -94,7 +94,6 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
)}
<TabPane
eventKey={Tab.Editor}
tabIndex='0'
title={i18next.t('learn.editor-tabs.code')}
{...editorTabPaneProps}
>

View File

@@ -48,7 +48,7 @@ function withActions(...fns: Array<() => void>) {
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
const { t } = useTranslation();
if (isOpen) {
executeGA({ type: 'modal', data: '/reset-modal' });
executeGA({ event: 'pageview', pagePath: '/reset-modal' });
}
return (
<Modal

View File

@@ -131,7 +131,7 @@ interface CompletionModalInnerState {
completedChallengesInBlock: number;
}
export class CompletionModalInner extends Component<
class CompletionModalInner extends Component<
CompletionModalsProps,
CompletionModalInnerState
> {
@@ -254,7 +254,7 @@ export class CompletionModalInner extends Component<
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
if (isOpen) {
executeGA({ type: 'modal', data: '/completion-modal' });
executeGA({ event: 'pageview', pagePath: '/completion-modal' });
}
// normally dashedName should be graphQL queried and then passed around,
// but it's only used to make a nice filename for downloading, so dasherize

View File

@@ -16,7 +16,7 @@ import './help-modal.css';
interface HelpModalProps {
closeHelpModal: () => void;
createQuestion: () => void;
executeGA: (attributes: { type: string; data: string }) => void;
executeGA: (attributes: { event: string; pagePath: string }) => void;
isOpen?: boolean;
t: (text: string) => string;
challengeTitle: string;
@@ -44,7 +44,7 @@ const generateSearchLink = (title: string, block: string) => {
return search;
};
export function HelpModal({
function HelpModal({
closeHelpModal,
createQuestion,
executeGA,
@@ -54,7 +54,7 @@ export function HelpModal({
challengeTitle
}: HelpModalProps): JSX.Element {
if (isOpen) {
executeGA({ type: 'modal', data: '/help-modal' });
executeGA({ event: 'pageview', pagePath: '/help-modal' });
}
return (
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>

View File

@@ -37,7 +37,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
dispatch
);
export function ShortcutsModal({
function ShortcutsModal({
closeShortcutsModal,
toggleKeyboardShortcuts,
isOpen,

View File

@@ -101,7 +101,7 @@ function ToolPanel({
className='btn-invert'
onClick={openResetModal}
>
{isMobile ? t('buttons.reset') : t('buttons.reset-code')}
{isMobile ? t('buttons.reset') : t('buttons.reset-lesson')}
</Button>
)}
<DropdownButton

View File

@@ -12,7 +12,7 @@ import './video-modal.css';
interface VideoModalProps {
closeVideoModal: () => void;
executeGA: (attributes: { type: string; data: string }) => void;
executeGA: (attributes: { event: string; pagePath: string }) => void;
isOpen?: boolean;
t: (attribute: string) => string;
videoUrl?: string;
@@ -28,7 +28,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
dispatch
);
export function VideoModal({
function VideoModal({
closeVideoModal,
executeGA,
isOpen,
@@ -36,7 +36,7 @@ export function VideoModal({
videoUrl
}: VideoModalProps): JSX.Element {
if (isOpen) {
executeGA({ type: 'modal', data: '/completion-modal' });
executeGA({ event: 'pageview', pagePath: '/completion-modal' });
}
return (
<Modal dialogClassName='video-modal' onHide={closeVideoModal} show={isOpen}>

View File

@@ -0,0 +1,427 @@
// Package Utilities
import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { graphql } from 'gatsby';
import React, { Component } from 'react';
import Helmet from 'react-helmet';
import { ObserveKeys } from 'react-hotkeys';
import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
// Local Utilities
import Loader from '../../../components/helpers/loader';
import Spacer from '../../../components/helpers/spacer';
import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta } from '../../../redux/prop-types';
import Hotkeys from '../components/Hotkeys';
import VideoPlayer from '../components/VideoPlayer';
import ChallengeTitle from '../components/challenge-title';
import CompletionModal from '../components/completion-modal';
import PrismFormatted from '../components/prism-formatted';
import {
challengeMounted,
updateChallengeMeta,
openModal,
updateSolutionFormValues
} from '../redux/actions';
import { isChallengeCompletedSelector } from '../redux/selectors';
// Styles
import './show.css';
import '../video.css';
// Redux Setup
const mapStateToProps = createSelector(
isChallengeCompletedSelector,
(isChallengeCompleted: boolean) => ({
isChallengeCompleted
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
challengeMounted,
updateSolutionFormValues,
openCompletionModal: () => openModal('completion')
},
dispatch
);
// Types
interface ShowOdinProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNode };
description: string;
isChallengeCompleted: boolean;
openCompletionModal: () => void;
pageContext: {
challengeMeta: ChallengeMeta;
};
t: TFunction;
updateChallengeMeta: (arg0: ChallengeMeta) => void;
updateSolutionFormValues: () => void;
}
interface ShowOdinState {
subtitles: string;
downloadURL: string | null;
selectedOption: number | null;
answer: number;
isWrongAnswer: boolean;
assignmentsCompleted: number;
allAssignmentsCompleted: boolean;
videoIsLoaded: boolean;
}
// Component
class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
static displayName: string;
private _container: HTMLElement | null | undefined;
constructor(props: ShowOdinProps) {
super(props);
this.state = {
subtitles: '',
downloadURL: null,
selectedOption: null,
answer: 1,
isWrongAnswer: false,
assignmentsCompleted: 0,
allAssignmentsCompleted: false,
videoIsLoaded: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount(): void {
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title, challengeType, helpCategory }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
updateChallengeMeta({
...challengeMeta,
title,
challengeType,
helpCategory
});
challengeMounted(challengeMeta.id);
this._container?.focus();
}
componentDidUpdate(prevProps: ShowOdinProps): void {
const {
data: {
challengeNode: {
challenge: { title: prevTitle }
}
}
} = prevProps;
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title: currentTitle, challengeType, helpCategory }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
if (prevTitle !== currentTitle) {
updateChallengeMeta({
...challengeMeta,
title: currentTitle,
challengeType,
helpCategory
});
challengeMounted(challengeMeta.id);
}
}
handleSubmit(
solution: number,
openCompletionModal: () => void,
assignments: string[]
) {
const hasAssignments = assignments[0] != '';
const completed = this.state.allAssignmentsCompleted;
const isCorrect = solution - 1 === this.state.selectedOption;
if (isCorrect) {
this.setState({
isWrongAnswer: false
});
if (!hasAssignments || completed) openCompletionModal();
} else {
this.setState({
isWrongAnswer: true
});
}
}
handleOptionChange = (
changeEvent: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
isWrongAnswer: false,
selectedOption: parseInt(changeEvent.target.value, 10)
});
};
handleAssignmentChange = (
event: React.ChangeEvent<HTMLInputElement>,
totalAssignments: number
): void => {
const assignmentsCompleted = event.target.checked
? this.state.assignmentsCompleted + 1
: this.state.assignmentsCompleted - 1;
const allAssignmentsCompleted = totalAssignments === assignmentsCompleted;
this.setState({
assignmentsCompleted,
allAssignmentsCompleted
});
};
onVideoLoad = () => {
this.setState({
videoIsLoaded: true
});
};
render() {
const {
data: {
challengeNode: {
challenge: {
fields: { blockName },
title,
description,
superBlock,
certification,
block,
translationPending,
videoId,
videoLocaleIds,
bilibiliIds,
question: { text, answers, solution },
assignments
}
}
},
openCompletionModal,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
},
t,
isChallengeCompleted
} = this.props;
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
return (
<Hotkeys
executeChallenge={() => {
this.handleSubmit(solution, openCompletionModal, assignments);
}}
innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
<LearnLayout>
<Helmet
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
/>
<Grid>
<Row>
<Spacer />
<ChallengeTitle
isCompleted={isChallengeCompleted}
translationPending={translationPending}
>
{title}
</ChallengeTitle>
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
<div className='video-wrapper'>
{!this.state.videoIsLoaded ? (
<div className='video-placeholder-loader'>
<Loader />
</div>
) : null}
<VideoPlayer
bilibiliIds={bilibiliIds}
onVideoLoad={this.onVideoLoad}
title={title}
videoId={videoId}
videoIsLoaded={this.state.videoIsLoaded}
videoLocaleIds={videoLocaleIds}
/>
</div>
</Col>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<h2>{title}</h2>
<PrismFormatted className={'line-numbers'} text={description} />
<Spacer />
<ObserveKeys>
{assignments[0] != '' && (
<>
<h2>Assignments</h2>
<div className='video-quiz-options'>
{assignments.map((assignment, index) => (
<label
className='video-quiz-option-label'
key={index}
>
<input
name='assignment'
type='checkbox'
className='video-quiz-checkbox-input'
onChange={event =>
this.handleAssignmentChange(
event,
assignments.length
)
}
/>
<PrismFormatted
className={'video-quiz-option'}
text={assignment}
/>
<Spacer />
</label>
))}
</div>{' '}
</>
)}
<Spacer />
<h2>Question</h2>
<PrismFormatted className={'line-numbers'} text={text} />
<div className='video-quiz-options'>
{answers.map((option, index) => (
<label className='video-quiz-option-label' key={index}>
<input
aria-label={t('aria.answer')}
checked={this.state.selectedOption === index}
className='video-quiz-input-hidden'
name='quiz'
onChange={this.handleOptionChange}
type='radio'
value={index}
/>{' '}
<span className='video-quiz-input-visible'>
{this.state.selectedOption === index ? (
<span className='video-quiz-selected-input' />
) : null}
</span>
<PrismFormatted
className={'video-quiz-option'}
text={option}
/>
</label>
))}
</div>
</ObserveKeys>
<Spacer />
<div
style={{
textAlign: 'center'
}}
>
{this.state.isWrongAnswer && (
<span>{t('learn.wrong-answer')}</span>
)}
{!this.state.allAssignmentsCompleted &&
assignments.length > 0 && (
<>
<br />
<span>{t('learn.assignment-not-complete')}</span>
</>
)}
</div>
<Spacer />
<Button
block={true}
bsSize='large'
bsStyle='primary'
onClick={() =>
this.handleSubmit(
solution,
openCompletionModal,
assignments
)
}
>
{t('buttons.check-answer')}
</Button>
<Spacer size={2} />
</Col>
<CompletionModal
block={block}
blockName={blockName}
certification={certification}
superBlock={superBlock}
/>
</Row>
</Grid>
</LearnLayout>
</Hotkeys>
);
}
}
ShowOdin.displayName = 'ShowOdin';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(ShowOdin));
export const query = graphql`
query TheOdinProject($slug: String!) {
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
challenge {
videoId
videoLocaleIds {
espanol
italian
portuguese
}
bilibiliIds {
aid
bvid
cid
}
title
description
challengeType
helpCategory
superBlock
certification
block
fields {
blockName
slug
}
question {
text
answers
solution
}
translationPending
assignments
}
}
}
`;

View File

@@ -0,0 +1,28 @@
input[type='checkbox'] {
display: grid;
place-content: center;
-webkit-appearance: none;
appearance: none;
background-color: #fff;
min-width: 1.15em;
min-height: 1.15em;
max-width: 1.15em;
max-height: 1.15em;
margin-right: 15px;
border: 2px solid black;
border-radius: 0.15em;
transform: translateY(-0.075em);
}
input[type='checkbox']::before {
content: '';
width: 0.65em;
height: 0.65em;
transform: scale(0);
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em black;
}
input[type='checkbox']:checked::before {
transform: scale(1);
}

View File

@@ -24,7 +24,7 @@ interface ToolPanelProps {
t: TFunction;
}
export function ToolPanel({
function ToolPanel({
guideUrl,
openHelpModal,
t

View File

@@ -99,9 +99,8 @@ const testJS = matchesProperty('ext', 'js');
const testJSX = matchesProperty('ext', 'jsx');
const testHTML = matchesProperty('ext', 'html');
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
export const testJS$JSX = overSome(testJS, testJSX);
export const replaceNBSP = cond([
const replaceNBSP = cond([
[
testHTML$JS$JSX,
partial(transformContents, contents => contents.replace(NBSPReg, ' '))

View File

@@ -14,7 +14,6 @@ export const actionTypes = createTypes(
'updateConsole',
'updateChallengeMeta',
'updateFile',
'updateJSEnabled',
'updateSolutionFormValues',
'updateSuccessMessage',
'updateTests',

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