diff --git a/.sass-lint.yml b/.sass-lint.yml index 79136f013..e60e787eb 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -1,6 +1,8 @@ rules: + class-name-format: 0 color-literals: 2 final-newline: 2 + force-element-nesting: 0 hex-notation: 2 indentation: - 2 @@ -13,6 +15,12 @@ rules: max-depth: 4 no-css-comments: 0 no-ids: 0 + no-mergeable-selectors: 0 + no-qualifying-elements: + - 1 + - + - allow-element-with-attribute + no-transition-all: 0 property-sort-order: - 2 - @@ -21,4 +29,5 @@ rules: - 2 - style: double + shorthand-values: 0 zero-unit: 2 diff --git a/.travis.yml b/.travis.yml index 26f9adf3e..9f3d8def9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js -node_js: '0.12' +node_js: +- '4.2' sudo: false cache: directories: @@ -11,12 +12,19 @@ env: global: - secure: A138rYuXDsOmpEwYxZ31WyXEeq5fgr9qyqsQh1nTFsjBKpFtNM+CN9e0QJQFT3PLs4wH/lWTRSyHxakxKQS1sxq828f9gHed+f15REKk/fRUplcCYIexT9xKVtU3D8CRNn/KBFWk75fZyZt20eyOVIv4h3pInKQz7y84J6PWzB1BCrAFvADrzS1X68Z3NJJLyxnz0YEurzz8mC2v4D0s/XifKTWvRtefD4QM6pE0C2iYyk+ThrLwg7i9FDHVfo0MrkgcdX7mz37SnTr7p7mHWnGXrGngi/NiDRQ+Uwwq/sr2UIww0rCwS1xsOcS//dC4NNqrrt1kUTsoC1Yt87Ny+gI0nUplsfEpdKajAkOYdANC5bJUGqPdSlOds1v9aJs9Hx48uGamWkm/3cFmoJ5uA2ZzUwbSGjTkWbnhwzT0YRvcLGhP1WE/EswaIyK5qMp522E79mP1yH6M750iUvi4N39+QW1BNX3ADkOwyAI67ArX5on5gWP83RXcJ15im7XsBpsmVn/KXi6AouWPb8jmSmKCj0QZCzfLY7ivM42IugYpK2NV7kFB38DpXQamJ5eskgwYa3elRmednIFUuwb1QDnONvJogVjk4CLmoSxssC2mJnnrUItM7l8G6As81GMI+6lTtl86hAuXBjUk60FMbgTAQDX9ll26LgpBy8jHSx8= - secure: EX1fyov+f6ytWN2ZSL4dLslwrVkp6Ho/uoSLO38/qNG3XdGmBN4VprxddcQiWfo+Mrg3GdWcfcM/VazhhStBi1uLfZiw3RHZaSGuWbiuD2EtzqtlC+OVvoajgy91QFajh9Zzuwa0rYbEPd/sw01R53NoWJYl0GSteWk7C8Wv6anl4FUJCqgvvTV2ZEcyTtGcVJgUhKi1MfNpTSM6JWBy0DWszcyxj7C8LSs1+l9ZjAtnlUBWY13HsrNu8G5d+FwqGHZLUAjdu2O602wxV897/xLARLduZ+01ALpVefNEEGMB1Wd+xMw4dm2B0Uk86a4TBRCeOgJZ1yoJoPpGPOHTo+dgNXcU8ReszGVoy7uOjFWwu82FQq8gzfcf75yzaRJgG8/BJ6BkJfa0EmFg3iO5CwixQyHR5+CqsedtoLAWVT8zlOfQ/Z6yx4Pm7jXQSOkyvo09YJ2QIn4IFGPvwOVS7Firzi+fLl8GYApeSV9G10e1IzA4pPrKdJMRA4qRMPt9zJGq7ZO1J/d9aW/5KIsJUDnodnl7yXJyDMOyNeljT9I82ciHZcURxRRY080vrW6dgNJE1V9jxBhWEvr2iCeWMMedWaGuC41I7K9L79eW8lmaE+cQ+OZrzpOJP4GbfmIiXrh+0M4ChL/xBpjtiFwpNdkCXXhzWMnjJ4wCrii4yuc= + - CXX=g++-4.8 - EB_REGION=us-east-1 - EB_APP=scratch-www - EB_AWS_BUCKET_NAME=elasticbeanstalk-us-east-1-307680192167 - SKIP_CLEANUP=true - ELASTIC_BEANSTALK_LABEL=${TRAVIS_COMMIT:0:5} - BUILD_ARCHIVE=$ELASTIC_BEANSTALK_LABEL.zip +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 before_deploy: - zip -qr $BUILD_ARCHIVE . deploy: @@ -29,7 +37,7 @@ deploy: skip_cleanup: $SKIP_CLEANUP region: $EB_REGION app: $EB_APP - env: scratch-www-staging + env: scratch-www-stage-v208 on: repo: LLK/scratch-www branch: diff --git a/Makefile b/Makefile index 2f9111b8e..5fa6c41eb 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,8 @@ test: @echo "" @make functional @echo "" + @make localization + @echo "" lint: $(ESLINT) ./*.js @@ -80,6 +82,9 @@ functional: integration: $(TAP) ./test/integration/*.js +localization: + $(TAP) ./test/localization/*.js + # ------------------------------------ .PHONY: build clean deploy static tag translations webpack watch stop start test lint diff --git a/bin/build-locales b/bin/build-locales index 136ea385b..059c1fc06 100755 --- a/bin/build-locales +++ b/bin/build-locales @@ -39,9 +39,10 @@ var fs = require('fs'); var glob = require('glob'); var merge = require('lodash.merge'); var path = require('path'); -var po2icu = require('po2icu'); +var languages = require('../languages.json'); var localeCompare = require('./lib/locale-compare'); +var localizedUrls = require('./lib/localized-urls'); // ----------------------------------------------------------------------------- // Main script @@ -54,12 +55,7 @@ if (!args.length) { process.stdout.write('A destination directory must be specified.'); process.exit(1); } -var verbose = false; -if (args.length > 1) { - verbose = (args[1] === '-v') ? true : false; -} -var poUiDir = path.resolve(__dirname, '../node_modules/scratchr2_translations/ui'); var outputDir = path.resolve(__dirname, '../', args[0]); try { fs.accessSync(outputDir, fs.F_OK); @@ -68,27 +64,19 @@ try { fs.mkdirSync(outputDir); } -// get global locale strings first. -var globalTemplateFile = path.resolve(__dirname, '../src/l10n.json'); // message key with english string values (i.e. default values) -var generalIds = JSON.parse(fs.readFileSync(globalTemplateFile, 'utf8')); var viewLocales = {}; -var generalLocales = { - en: generalIds -}; - // FormattedMessage id with english string as value. Use for default values in translations // Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' } var idsWithICU = {}; - // reverse (i.e. english string with message key as the value) object for searching po files. // Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' } var icuWithIds = {}; -for (var id in generalIds) { - idsWithICU['general-' + id] = generalIds[id]; - icuWithIds[generalIds[id]] = 'general-' + id; -} +// get global locale strings first. +var globalTemplateFile = path.resolve(__dirname, '../src/l10n.json'); +localeCompare.getIdsForView('general', globalTemplateFile, viewLocales, idsWithICU, icuWithIds); + // start with all views, and remove localized ones as they are iterated over var views = glob.sync(path.resolve(__dirname, '../src/views/*')); @@ -102,14 +90,28 @@ files.forEach(function (file) { var dirPath = file.split('/'); dirPath.pop(); var view = dirPath.pop(); + localeCompare.getIdsForView(view, file, viewLocales, idsWithICU, icuWithIds); +}); - var viewIds = JSON.parse(fs.readFileSync(file, 'utf8')); - viewLocales[view] = { - en: viewIds - }; - for (var id in viewIds) { - idsWithICU[view + '-' + id] = viewIds[id]; - icuWithIds[viewIds[id]] = view + '-' + id; // add viewName to identifier for later +// get asset url translations +var localizedAssetUrls = {}; +files = glob.sync(path.resolve(__dirname, '../src/views/**/l10n-static.json')); +files.forEach(function (file) { + var dirPath = file.split('/'); + dirPath.pop(); + var view = dirPath.pop(); + localizedAssetUrls[view] = {}; + + var assetUrls = JSON.parse(fs.readFileSync(file, 'utf8')); + for (var lang in localizedUrls) { + localizedAssetUrls[view][lang] = {}; + for (var key in assetUrls) { + if (localizedUrls[lang].hasOwnProperty(key)) { + localizedAssetUrls[view][lang][key] = localizedUrls[lang][key]; + } else { + localizedAssetUrls[view][lang][key] = assetUrls[key]; + } + } } }); @@ -118,53 +120,21 @@ files.forEach(function (file) { var md5WithIds = localeCompare.getMD5Map(icuWithIds); // Get ui localization strings first -glob(poUiDir + '/*', function (err, files) { - if (err) throw new Error(err); - - files.forEach(function (file) { - var lang = file.split('/').pop(); - var jsFile = path.resolve(file, 'LC_MESSAGES/djangojs.po'); - var pyFile = path.resolve(file, 'LC_MESSAGES/django.po'); - - var translations = {}; - - try { - var jsTranslations = po2icu.poFileToICUSync(lang, jsFile); - translations = localeCompare.mergeNewTranslations(translations, jsTranslations, idsWithICU, md5WithIds); - } catch (err) { - if (verbose) process.stdout.write(lang + ': ' + err + '\n'); - } - - try { - var pyTranslations = po2icu.poFileToICUSync(lang, pyFile); - translations = localeCompare.mergeNewTranslations(translations, pyTranslations, idsWithICU, md5WithIds); - } catch (err) { - if (verbose) process.stdout.write(lang + ': ' + err + '\n'); - } - - // add new translations to locale object - for (var id in translations) { - var ids = id.split('-'); // [viewName, stringId] - var viewName = ids[0]; - var stringId = ids[1]; - if (viewLocales.hasOwnProperty(viewName)) { - if (!viewLocales[viewName].hasOwnProperty(lang)) viewLocales[viewName][lang] = {}; - viewLocales[viewName][lang][stringId] = translations[id]; - } else { - // default to general - if (!generalLocales.hasOwnProperty(lang)) generalLocales[lang] = {}; - generalLocales[lang][stringId] = translations[id]; - } - } - }); - - for (var i in views) { - var viewTranslations = generalLocales; - if (views[i] in viewLocales) { - viewTranslations = merge(viewLocales[views[i]], viewTranslations); - } - var objectString = JSON.stringify(viewTranslations); - var fileString = 'window._messages = ' + objectString + ';'; - fs.writeFileSync(outputDir + '/' + views[i] + '.intl.js', fileString); +var isoCodes = Object.keys(languages); +for (i in isoCodes) { + var translations = localeCompare.getTranslationsForLanguage(isoCodes[i], idsWithICU, md5WithIds); + for (var key in translations) { + viewLocales[key] = merge(viewLocales[key], translations[key]); } -}); +} + +for (i in views) { + var viewTranslations = viewLocales['general']; + if (views[i] in viewLocales) { + viewTranslations = merge(viewLocales[views[i]], viewTranslations); + } + if (views[i] in localizedAssetUrls) { + viewTranslations = merge(viewTranslations, localizedAssetUrls[[views[i]]]); + } + localeCompare.writeTranslationsToJS(outputDir, views[i], viewTranslations); +} diff --git a/bin/lib/locale-compare.js b/bin/lib/locale-compare.js index 25ac94345..96f9f24df 100644 --- a/bin/lib/locale-compare.js +++ b/bin/lib/locale-compare.js @@ -3,6 +3,9 @@ // ----------------------------------------------------------------------------- var crypto = require('crypto'); +var fs = require('fs'); +var path = require('path'); +var po2icu = require('po2icu'); var Helpers = {}; @@ -62,4 +65,75 @@ Helpers.getMD5Map = function (ICUIdMap) { return md5Map; }; +/** + * Grabs the translated strings from the po files for the given language and strings + * @param {str} lang iso code of the language to use + * @param {object} idsWithICU key: '-'. + * value: english strings for translation + * @param {object} md5WithIds key: md5 hash of the english strings for translation. + * value: '-' + * @return {object} translations – sub-objects by view containing: + * key: '' + * value: translated version of string + */ +Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) { + var poUiDir = path.resolve(__dirname, '../../node_modules/scratchr2_translations/ui'); + var jsFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/djangojs.po'); + var pyFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/django.po'); + + var translations = {}; + try { + fs.accessSync(jsFile, fs.R_OK); + var jsTranslations = po2icu.poFileToICUSync(lang, jsFile); + translations = Helpers.mergeNewTranslations(translations, jsTranslations, idsWithICU, md5WithIds); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + try { + fs.accessSync(pyFile, fs.R_OK); + var pyTranslations = po2icu.poFileToICUSync(lang, pyFile); + translations = Helpers.mergeNewTranslations(translations, pyTranslations, idsWithICU, md5WithIds); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + var translationsByView = {}; + for (var id in translations) { + var ids = id.split('-'); // [viewName, stringId] + var viewName = ids[0]; + var stringId = ids[1]; + + if (!translationsByView.hasOwnProperty(viewName)) { + translationsByView[viewName] = {}; + } + if (!translationsByView[viewName].hasOwnProperty(lang)) { + translationsByView[viewName][lang] = {}; + } + translationsByView[viewName][lang][stringId] = translations[id]; + } + return translationsByView; +}; + +Helpers.writeTranslationsToJS = function (outputDir, viewName, translationObject) { + var objectString = JSON.stringify(translationObject); + var fileString = 'window._messages = ' + objectString + ';'; + fs.writeFileSync(outputDir + '/' + viewName + '.intl.js', fileString); +}; + +Helpers.getIdsForView = function (viewName, viewFile, localeObject, idsWithICU, icuWithIds) { + var ids = JSON.parse(fs.readFileSync(viewFile, 'utf8')); + localeObject[viewName] = { + en: ids + }; + for (var id in ids) { + idsWithICU[viewName + '-' + id] = ids[id]; + icuWithIds[ids[id]] = viewName + '-' + id; // add viewName to identifier for later + } +}; + module.exports = Helpers; diff --git a/bin/lib/localized-urls.json b/bin/lib/localized-urls.json new file mode 100644 index 000000000..5b85d12e0 --- /dev/null +++ b/bin/lib/localized-urls.json @@ -0,0 +1,52 @@ +{ + "en": { + "cards.starterLink": "//scratch.mit.edu/scratchr2/static/pdfs/help/Scratch2Cards.pdf", + "cards.nameLink": "//scratch.mit.edu/scratchr2/static/pdfs/help/AnimateYourNameCards.pdf", + "cards.pongLink": "//scratch.mit.edu/scratchr2/static/pdfs/help/PongCards.pdf", + "cards.storyLink": "//scratch.mit.edu/scratchr2/static/pdfs/help/StoryCards.pdf", + "cards.danceLink": "//scratch.mit.edu/scratchr2/static/pdfs/help/DanceCards.pdf", + "cards.hideLink": "//scratch.mit.edu/scratchr2/static/pdfs/help/Hide-and-Seek-Cards.pdf" + }, + "ar": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/ar/Scratch2Cards.pdf" + }, + "ca": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/ca/Scratch2Cards.pdf" + }, + "cs": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/cs/Scratch2Cards.pdf" + }, + "de": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/de/Scratch2Cards.pdf" + }, + "es": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/es/Scratch2Cards.pdf" + }, + "fr": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/fr/Scratch2Cards.pdf" + }, + "hr": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/hr/Scratch2Cards.pdf" + }, + "it": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/it/Scratch2Cards.pdf" + }, + "ja": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/ja/Scratch2Cards.pdf" + }, + "ja-hr": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/ja-hr/Scratch2Cards.pdf" + }, + "ko": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/ko/Scratch2Cards.pdf" + }, + "nl": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/nl/Scratch2Cards.pdf" + }, + "pt-br": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/pt-br/Scratch2Cards.pdf" + }, + "sl": { + "cards.starterLink": "//cdn.scratch.mit.edu/scratchr2/static/pdfs/help/sl/Scratch2Cards.pdf" + } +} \ No newline at end of file diff --git a/languages.json b/languages.json index 7839e91d7..bcabe056f 100644 --- a/languages.json +++ b/languages.json @@ -33,6 +33,7 @@ "cat": "Meow", "nl": "Nederlands", "nb": "Norsk Bokmål", + "nn": "Norsk Nynorsk", "uz": "Oʻzbekcha", "pl": "Polski", "pt": "Português", diff --git a/package.json b/package.json index d6b832f86..d81a60fbd 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,14 @@ }, "homepage": "https://github.com/llk/scratch-www#readme", "dependencies": { - "bunyan": "1.5.1", - "compression": "1.5.2", - "express": "4.13.3", + "bunyan": "1.7.1", + "compression": "1.6.1", + "express": "4.13.4", "express-http-proxy": "0.6.0", - "lodash.defaults": "3.1.2", - "mustache": "2.1.3", - "newrelic": "1.22.1", - "raven": "0.8.1" + "lodash.defaults": "4.0.1", + "mustache": "2.2.1", + "newrelic": "1.25.4", + "raven": "0.10.0" }, "devDependencies": { "autoprefixer-loader": "2.1.0", @@ -44,6 +44,7 @@ "json-loader": "0.5.2", "json2po-stream": "1.0.3", "jsx-loader": "0.13.2", + "keymirror": "0.1.1", "lodash.clone": "3.0.3", "lodash.defaultsdeep": "3.10.0", "lodash.merge": "3.3.2", @@ -51,27 +52,26 @@ "lodash.range": "3.0.1", "minilog": "2.0.8", "node-sass": "3.3.3", + "pako": "0.2.8", "po2icu": "git://github.com/LLK/po2icu.git#develop", - "react": "0.14.0", - "react-addons-test-utils": "0.14.0", - "react-dom": "0.14.0", - "react-intl": "2.0.0-beta-1", + "react-addons-test-utils": "0.14.7", "react-modal": "0.6.1", "react-onclickoutside": "4.1.1", + "react-redux": "4.4.0", "react-slick": "0.9.2", + "redux-thunk": "2.0.1", "routes-to-nginx-conf": "0.0.4", - "sass-lint": "1.3.2", + "sass-lint": "1.5.1", "sass-loader": "2.0.1", "scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master", "slick-carousel": "1.5.8", "source-map-support": "0.3.2", "style-loader": "0.12.3", "tap": "2.0.0", - "tape": "4.2.0", "url-loader": "0.5.6", "watch": "0.16.0", - "webpack": "1.12.2", + "webpack": "1.12.14", "webpack-dev-middleware": "1.2.0", - "xhr": "2.0.4" + "xhr": "2.2.0" } } diff --git a/server/config.js b/server/config.js index c2772d910..6f77b1101 100644 --- a/server/config.js +++ b/server/config.js @@ -22,5 +22,8 @@ module.exports = { sentry_dsn: process.env.CLIENT_SENTRY_DSN || '', // Use minified JS libraries - min: (process.env.NODE_ENV === 'production') ? '.min' : '' + min: (process.env.NODE_ENV === 'production') ? '.min' : '', + + // Redux likes to have this + NODE_ENV: process.env.NODE_ENV }; diff --git a/server/routes.json b/server/routes.json index f66ece9b8..7867f964a 100644 --- a/server/routes.json +++ b/server/routes.json @@ -24,6 +24,21 @@ "view": "credits", "title": "Credits" }, + { + "pattern": "/info/cards", + "view": "cards", + "title": "Cards" + }, + { + "pattern": "/info/communityblocks-interviews", + "view": "communityblocks-interviews", + "title": "Community Blocks Beta Tester Interviews" + }, + { + "pattern": "/jobs", + "view": "jobs", + "title": "Jobs" + }, { "pattern": "/wedo", "view": "wedo2", diff --git a/server/template.html b/server/template.html index 4be96455c..d68541291 100644 --- a/server/template.html +++ b/server/template.html @@ -41,13 +41,12 @@ - -
- +
+ @@ -56,7 +55,9 @@ diff --git a/src/components/adminpanel/adminpanel.jsx b/src/components/adminpanel/adminpanel.jsx index ebcb91751..ef2c1344d 100644 --- a/src/components/adminpanel/adminpanel.jsx +++ b/src/components/adminpanel/adminpanel.jsx @@ -1,15 +1,12 @@ var React = require('react'); +var connect = require('react-redux').connect; var Button = require('../../components/forms/button.jsx'); -var Session = require('../../mixins/session.jsx'); require('./adminpanel.scss'); var AdminPanel = React.createClass({ type: 'AdminPanel', - mixins: [ - Session - ], getInitialState: function () { return { showPanel: false @@ -22,8 +19,8 @@ var AdminPanel = React.createClass({ render: function () { // make sure user is present before checking if they're an admin. Don't show anything if user not an admin. var showAdmin = false; - if (this.state.session.user) { - showAdmin = this.state.session.permissions.admin; + if (this.props.session.user) { + showAdmin = this.props.session.permissions.admin; } if (!showAdmin) return false; @@ -78,4 +75,12 @@ var AdminPanel = React.createClass({ } }); -module.exports = AdminPanel; +var mapStateToProps = function (state) { + return { + session: state.session + }; +}; + +var ConnectedAdminPanel = connect(mapStateToProps)(AdminPanel); + +module.exports = ConnectedAdminPanel; diff --git a/src/components/adminpanel/adminpanel.scss b/src/components/adminpanel/adminpanel.scss index 64ef709a0..6b8c36907 100644 --- a/src/components/adminpanel/adminpanel.scss +++ b/src/components/adminpanel/adminpanel.scss @@ -38,7 +38,7 @@ font-size: large; font-weight: 700; } - + dd { margin-left: 0; } @@ -51,21 +51,22 @@ margin: 0; list-style: none; - .button-row { - display: flex; - font-size: small; - align-items: center; - justify-content: space-between; + } + } + } - .button { - padding: .5rem 1rem; + .button-row { + display: flex; + font-size: small; + align-items: center; + justify-content: space-between; - &.inprogress { - background-color: $ui-dark-gray; - color: $type-gray; - } - } - } + .button { + padding: .5rem 1rem; + + &.inprogress { + background-color: $ui-dark-gray; + color: $type-gray; } } } diff --git a/src/components/banner/banner.scss b/src/components/banner/banner.scss index 2596df6c8..64a69d638 100644 --- a/src/components/banner/banner.scss +++ b/src/components/banner/banner.scss @@ -13,7 +13,8 @@ $navigation-height: 50px; text-align: center; line-height: $navigation-height; - &, a { + &, + a { color: $ui-white; } @@ -23,14 +24,14 @@ $navigation-height: 50px; .close { float: right; - margin-top: $navigation-height/4; - border-radius: $navigation-height/4; + margin-top: $navigation-height / 4; + border-radius: $navigation-height / 4; background-color: $box-shadow-gray; - width: $navigation-height/2; - height: $navigation-height/2; + width: $navigation-height / 2; + height: $navigation-height / 2; text-decoration: none; text-shadow: none; - line-height: $navigation-height/2; + line-height: $navigation-height / 2; color: $ui-white; font-weight: normal; } diff --git a/src/components/box/box.scss b/src/components/box/box.scss index 632c31a07..01310720f 100644 --- a/src/components/box/box.scss +++ b/src/components/box/box.scss @@ -11,6 +11,7 @@ $base-bg: $ui-white; //4 columns @media only screen and (max-width: $mobile - 1) { width: $cols4; + .box-header { h4 { font-size: .9rem; @@ -21,9 +22,10 @@ $base-bg: $ui-white; //6 columns @media only screen and (min-width: $mobile) and (max-width: $tablet - 1) { width: $cols6; + .box-header { h4 { - font-size: 1.0rem; + font-size: 1rem; } } } @@ -31,6 +33,7 @@ $base-bg: $ui-white; //8 columns @media only screen and (min-width: $tablet) and (max-width: $desktop - 1) { width: $cols8; + .box-header { h4 { font-size: 1.1rem; @@ -41,6 +44,7 @@ $base-bg: $ui-white; //12 columns @media only screen and (min-width: $desktop) { width: $cols12; + .box-header { h4 { font-size: 1.1rem; diff --git a/src/components/carousel/carousel.scss b/src/components/carousel/carousel.scss index dd2bac747..b8fbcdbf4 100644 --- a/src/components/carousel/carousel.scss +++ b/src/components/carousel/carousel.scss @@ -14,7 +14,7 @@ .slick-next, .slick-prev { - margin-top: -$icon-size/2; + margin-top: -$icon-size / 2; width: $icon-size; height: $icon-size; diff --git a/src/components/intro/intro.jsx b/src/components/intro/intro.jsx index c7434278c..07e58f281 100644 --- a/src/components/intro/intro.jsx +++ b/src/components/intro/intro.jsx @@ -1,6 +1,9 @@ +var connect = require('react-redux').connect; var omit = require('lodash.omit'); var React = require('react'); +var actions = require('../../redux/actions.js'); + var Modal = require('../modal/modal.jsx'); var Registration = require('../registration/registration.jsx'); @@ -10,12 +13,8 @@ Modal.setAppElement(document.getElementById('view')); var Intro = React.createClass({ type: 'Intro', - propTypes: { - projectCount: React.PropTypes.number - }, getDefaultProps: function () { return { - projectCount: 10569070, messages: { 'intro.aboutScratch': 'ABOUT SCRATCH', 'intro.forEducators': 'FOR EDUCATORS', @@ -23,8 +22,11 @@ var Intro = React.createClass({ 'intro.joinScratch': 'JOIN SCRATCH', 'intro.seeExamples': 'SEE EXAMPLES', 'intro.tagLine': 'Create stories, games, and animations
Share with others around the world', - 'intro.tryItOut': 'TRY IT OUT' - } + 'intro.tryItOut': 'TRY IT OUT', + 'intro.description': 'A creative learning community with ' + + 'over 13 million projects shared' + }, + session: {} }; }, getInitialState: function () { @@ -46,7 +48,7 @@ var Intro = React.createClass({ this.setState({'registrationOpen': false}); }, completeRegistration: function () { - window.refreshSession(); + this.props.dispatch(actions.refreshSession()); this.closeRegistration(); }, render: function () { @@ -109,11 +111,8 @@ var Intro = React.createClass({ onRequestClose={this.closeRegistration} onRegistrationDone={this.completeRegistration} /> -
- A creative learning community with - {this.props.projectCount.toLocaleString()} - projects shared -
+
{this.props.messages['intro.aboutScratch']} @@ -145,4 +144,12 @@ var Intro = React.createClass({ } }); -module.exports = Intro; +var mapStateToProps = function (state) { + return { + session: state.session + }; +}; + +var ConnectedIntro = connect(mapStateToProps)(Intro); + +module.exports = ConnectedIntro; diff --git a/src/components/intro/intro.scss b/src/components/intro/intro.scss index 5406f8d54..d8fb1e43b 100644 --- a/src/components/intro/intro.scss +++ b/src/components/intro/intro.scss @@ -57,14 +57,6 @@ display: none; } - &:hover .costume-1 { - display: none; - } - - &:hover .costume-2 { - display: block; - } - .circle { display: block; top: 15px; @@ -98,52 +90,74 @@ } - &.sprite-1 .circle { - background-color: $splash-green; + &.sprite-1 { + .circle { + background-color: $splash-green; + } + + .text { + top: 60px; + left: 50px; + color: $splash-green; + } } - &.sprite-2 .circle { - background-color: $splash-pink; + &.sprite-2 { + .circle { + background-color: $splash-pink; + } + + .text { + top: 77px; + left: 50px; + color: $splash-pink; + } } - &.sprite-3 .circle { - background-color: $splash-blue; + &.sprite-3 { + .circle { + background-color: $splash-blue; + } + + .text { + top: 37px; + left: 45px; + color: $splash-blue; + } + + .subtext { + top: 63px; + left: 60px; + color: $ui-white; + } } - &:hover.sprite-1 .circle { - box-shadow: 0 0 10px 2px $splash-green; - } + &:hover { + .costume-1 { + display: none; + } - &:hover.sprite-2 .circle { - box-shadow: 0 0 10px 2px $splash-pink; - } + .costume-2 { + display: block; + } - &:hover.sprite-3 .circle { - box-shadow: 0 0 10px 2px $splash-blue; - } + &.sprite-1 { + .circle { + box-shadow: 0 0 10px 2px $splash-green; + } + } - &.sprite-1 .text { - top: 60px; - left: 50px; - color: $splash-green; - } + &.sprite-2 { + .circle { + box-shadow: 0 0 10px 2px $splash-pink; + } + } - &.sprite-2 .text { - top: 77px; - left: 50px; - color: $splash-pink; - } - - &.sprite-3 .text { - top: 37px; - left: 45px; - color: $splash-blue; - } - - &.sprite-3 .subtext { - top: 63px; - left: 60px; - color: $ui-white; + &.sprite-3 { + .circle { + box-shadow: 0 0 10px 2px $splash-blue; + } + } } } diff --git a/src/components/languagechooser/languagechooser.jsx b/src/components/languagechooser/languagechooser.jsx index 39d39887b..765f883ba 100644 --- a/src/components/languagechooser/languagechooser.jsx +++ b/src/components/languagechooser/languagechooser.jsx @@ -6,8 +6,6 @@ var jar = require('../../lib/jar.js'); var languages = require('../../../languages.json'); var Select = require('../forms/select.jsx'); -require('./languagechooser.scss'); - /** * Footer dropdown menu that allows one to change their language. */ diff --git a/src/components/languagechooser/languagechooser.scss b/src/components/languagechooser/languagechooser.scss deleted file mode 100644 index abe1a6d34..000000000 --- a/src/components/languagechooser/languagechooser.scss +++ /dev/null @@ -1,3 +0,0 @@ -.language-chooser { - -} diff --git a/src/components/login/login.jsx b/src/components/login/login.jsx index 4dac048db..d12ce2361 100644 --- a/src/components/login/login.jsx +++ b/src/components/login/login.jsx @@ -40,30 +40,30 @@ var Login = React.createClass({ return (
- + diff --git a/src/components/login/login.scss b/src/components/login/login.scss index 75709bc3e..5f70c0b08 100644 --- a/src/components/login/login.scss +++ b/src/components/login/login.scss @@ -20,11 +20,12 @@ a { margin-top: 15px; + + &:hover { + background-color: transparent; + } } - a:hover { - background-color: transparent; - } .error { border: 1px solid $active-dark-gray; diff --git a/src/components/microworld/microworld.jsx b/src/components/microworld/microworld.jsx new file mode 100644 index 000000000..695bfefad --- /dev/null +++ b/src/components/microworld/microworld.jsx @@ -0,0 +1,246 @@ +var React = require('react'); + +require('./microworld.scss'); + +var Box = require('../../components/box/box.jsx'); +var Carousel = require('../../components/carousel/carousel.jsx'); +var Modal = require('../../components/modal/modal.jsx'); +var NestedCarousel = require('../../components/nestedcarousel/nestedcarousel.jsx'); + +var Microworld = React.createClass({ + type: 'Microworld', + propTypes: { + microworldData: React.PropTypes.node.isRequired + }, + markVideoOpen: function (key) { + {/* When a video is clicked, mark it as an open video, so the video Modal will open. + Key is the number of the video, so distinguish between different videos on the page */} + + var videoOpenArr = this.state.videoOpen; + videoOpenArr[key] = true; + this.setState({videoOpen: videoOpenArr}); + }, + markVideoClosed: function (key) { + {/* When a video's x is clicked, mark it as closed, so the video Modal will disappear. + Key is the number of the video, so distinguish between different videos on the page */} + var videoOpenArr = this.state.videoOpen; + videoOpenArr[key] = false; + this.setState({videoOpen: videoOpenArr}); + }, + getInitialState: function () { + return { + videoOpen: {} + }; + }, + renderVideos: function () { + var videos = this.props.microworldData.videos; + if (!videos || videos.length <= 0) { + return null; + } + + return ( +
+

Get Inspired...

+
+
+ {videos.map(this.renderVideo)} +
+
+
+ ); + }, + renderVideo: function (video, key) { + var frameProps = { + width: 570, + height: 357, + padding: 15 + }; + return ( +
+
+
+
+ +
+ + + +
+ ); + }, + renderEditorWindow: function () { + var projectId = this.props.microworldData.microworld_project_id; + + if (!projectId) { + return null; + } + return ( +
+

Start Creating!

+
+ + + + {this.renderTips()} +
+ ); + }, + renderTips: function () { + var tips = this.props.microworldData.tips; + if (!tips || tips.length <= 0) { + return null; + } + + return ( +
+
+
+
+ +
+
+ ); + }, + renderStarterProject: function () { + var starterProjects = this.props.microworldData.starter_projects; + if (!starterProjects || starterProjects.length <= 0){ + return null; + } + + return ( +
+

Check out ideas for more projects

+ + + +
+ ); + }, + renderProjectIdeasBox: function () { + var communityProjects = this.props.microworldData.community_projects; + if (!communityProjects || communityProjects.size <= 0) { + return null; + } + + var featured = communityProjects.featured_projects; + var all = communityProjects.newest_projects; + + var rows = []; + if (featured && featured.length > 0){ + rows.push( + + + + ); + } + if (all && all.length > 0) { + rows.push( + + + + ); + } + if (rows.length <= 0) { + return null; + } + return ( +
+

Get inspiration from other projects

+ {rows} +
+ ); + }, + renderForum: function () { + if (!this.props.microworldData.show_forum) { + return null; + } + + return ( +
+

Chat with others!

+ +
+ ); + }, + renderDesignStudio: function () { + var designChallenge = this.props.microworldData.design_challenge; + if (!designChallenge) { + return null; + } + if (designChallenge.studio_id) { + var studioHref = 'https://scratch.mit.edu//studios/' + designChallenge.studio_id + '/'; + } + if (designChallenge.project_id) { + return ( +
+

Join our Design Challenge!

+
+ +
+
+ + {/* The two carousels are used to show two rows of projects, one above the + other. This should be probably be changed, to allow better scrolling. */} + + + +
+
+ ); + } else { + return ( +
+

Join our Design Challenge!

+ + + +
+ ); + } + }, + render: function () { + return ( +
+
+

{this.props.microworldData.title}

+

{this.props.microworldData.description.join(' ')}

+
+ {this.renderVideos()} + +
+ {this.renderEditorWindow()} + {this.renderStarterProject()} + {this.renderDesignStudio()} + {this.renderProjectIdeasBox()} + {this.renderForum()} +
+
+ + ); + } +}); + +module.exports = Microworld; diff --git a/src/components/microworld/microworld.scss b/src/components/microworld/microworld.scss new file mode 100644 index 000000000..b18adda58 --- /dev/null +++ b/src/components/microworld/microworld.scss @@ -0,0 +1,185 @@ +@import "../../colors"; +@import "../../frameless"; + +$base-bg: $ui-white; + +#view { + background-color: $base-bg; + padding: 0; + + // To be integrated into the Global Typography standards + h3, + p { + font-weight: 300; + + } + + p { + line-height: 2em; + } + + h1 { + margin: 0 auto; + padding: 5px 10%; + text-align: center; + color: $type-gray; + } + + .top-banner, + .videos-section, + .section { + padding: 30px 0; + width: 100%; + + h1, + p { + margin: 0 auto; + padding: 5px 10%; + text-align: center; + color: $type-gray; + } + } + + .videos-container { + display: flex; + margin: 0 auto; + + justify-content: center; + flex-wrap: wrap; + align-items: center; + + .videos { + display: inline-flex; + justify-content: center; + flex-wrap: wrap; + } + + .video { + position: relative; + margin: 10px; + border-radius: 7px; + background-color: $active-gray; + padding: 2px; + max-width: 290px; + } + + img { + margin: 10px 10px 5px; + border-radius: 5px; + width: calc(100% - 20px); + height: 179px; + } + + .play-button { + display: block; + top: calc(50% - 25px); + left: calc(50% - 35px); + opacity: .8; + border: 5px solid $ui-border; + border-radius: 20px; + background-color: $type-gray; + width: 70px; + height: 50px; + + &, + &:after { + position: absolute; + margin: 0; + cursor: pointer; + padding: 0; + } + + &:after { + $play-arrow: rgba(255, 255, 255, 0); + top: 37px; + left: 28px; + margin-top: -30px; + border: solid transparent; + border-width: 18px; + border-color: $play-arrow; + border-left-color: $ui-white; + width: 0; + height: 0; + content: " "; + pointer-events: none; + } + } + } + + .content { + img { + display: block; + margin-right: auto; + margin-left: auto; + } + + .box, + iframe { + display: block; + margin-right: auto; + margin-left: auto; + border: 0; + padding-top: 25px; + padding-bottom: 15px; + } + + iframe { + height: 600px; + } + + .editor { + position: relative; + + iframe { + width: 100%; + } + + .scratch-link { + position: absolute; + right: 3%; + width: 10%; + } + } + + .side-by-side { + margin-right: auto; + margin-left: auto; + + height: 520px; + + .design-studio-projects, + .design-studio { + display: inline-block; + width: 45%; + height: 500px; + } + + .design-studio-projects { + float: right; + } + + .design-studio { + float: left; + + iframe { + margin-top: 60px; + width: 200%; + -webkit-transform: scale(.5); + -webkit-transform-origin: top left; + -moz-transform: scale(.5); + } + } + } + } + + .box-content { + .nestedcarousel { + text-align: center; + + .thumbnail { + display: inline-block; + margin: 0 50px; + } + } + } +} diff --git a/src/components/modal/modal.scss b/src/components/modal/modal.scss index 49503eaf4..fae33030d 100644 --- a/src/components/modal/modal.scss +++ b/src/components/modal/modal.scss @@ -1,6 +1,6 @@ @import "../../colors"; -&.ReactModal__Content { +.ReactModal__Content { iframe { border: 0; } @@ -11,10 +11,10 @@ position: absolute; top: 0; right: 0; - margin-top: -$modal-close-size/2; - margin-right: -$modal-close-size/2; + margin-top: -$modal-close-size / 2; + margin-right: -$modal-close-size / 2; border: 2px solid $ui-border; - border-radius: $modal-close-size/2; + border-radius: $modal-close-size / 2; background-color: $active-dark-gray; cursor: pointer; width: $modal-close-size; diff --git a/src/components/navigation/dropdown.scss b/src/components/navigation/dropdown.scss index 5b60414f9..78ef27a81 100644 --- a/src/components/navigation/dropdown.scss +++ b/src/components/navigation/dropdown.scss @@ -64,7 +64,7 @@ &:before { display: block; position: absolute; - top: -$arrow-border-width/2; + top: -$arrow-border-width / 2; right: 10%; transform: rotate(45deg); diff --git a/src/components/navigation/navigation.jsx b/src/components/navigation/navigation.jsx index cf51ad8f1..fbcb401b0 100644 --- a/src/components/navigation/navigation.jsx +++ b/src/components/navigation/navigation.jsx @@ -1,10 +1,12 @@ var classNames = require('classnames'); +var connect = require('react-redux').connect; var React = require('react'); var ReactIntl = require('react-intl'); -var defineMessages = ReactIntl.defineMessages; var FormattedMessage = ReactIntl.FormattedMessage; var injectIntl = ReactIntl.injectIntl; +var actions = require('../../redux/actions.js'); + var Api = require('../../mixins/api.jsx'); var Avatar = require('../avatar/avatar.jsx'); var Dropdown = require('./dropdown.jsx'); @@ -13,32 +15,15 @@ var log = require('../../lib/log.js'); var Login = require('../login/login.jsx'); var Modal = require('../modal/modal.jsx'); var Registration = require('../registration/registration.jsx'); -var Session = require('../../mixins/session.jsx'); require('./navigation.scss'); Modal.setAppElement(document.getElementById('view')); -var defaultMessages = defineMessages({ - messages: { - id: 'general.messages', - defaultMessage: 'Messages' - }, - myStuff: { - id: 'general.myStuff', - defaultMessage: 'My Stuff' - }, - search: { - id: 'general.search', - defaultMessage: 'Search' - } -}); - var Navigation = React.createClass({ type: 'Navigation', mixins: [ - Api, - Session + Api ], getInitialState: function () { return { @@ -51,20 +36,25 @@ var Navigation = React.createClass({ messageCountIntervalId: -1 // javascript method interval id for getting messsage count. }; }, + getDefaultProps: function () { + return { + session: {} + }; + }, componentDidMount: function () { - if (this.state.session.user) { + if (this.props.session.user) { this.getMessageCount(); var intervalId = setInterval(this.getMessageCount, 120000); // check for new messages every 2 mins. this.setState({'messageCountIntervalId': intervalId}); } }, - componentDidUpdate: function (prevProps, prevState) { - if (prevState.session.user != this.state.session.user) { + componentDidUpdate: function (prevProps) { + if (prevProps.session.user != this.props.session.user) { this.setState({ 'loginOpen': false, 'accountNavOpen': false }); - if (this.state.session.user) { + if (this.props.session.user) { this.getMessageCount(); var intervalId = setInterval(this.getMessageCount, 120000); this.setState({'messageCountIntervalId': intervalId}); @@ -89,13 +79,13 @@ var Navigation = React.createClass({ } }, getProfileUrl: function () { - if (!this.state.session.user) return; - return '/users/' + this.state.session.user.username + '/'; + if (!this.props.session.user) return; + return '/users/' + this.props.session.user.username + '/'; }, getMessageCount: function () { this.api({ method: 'get', - uri: '/users/' + this.state.session.user.username + '/messages/count' + uri: '/users/' + this.props.session.user.username + '/messages/count' }, function (err, body) { if (err) return this.setState({'unreadMessageCount': 0}); if (body) { @@ -141,7 +131,7 @@ var Navigation = React.createClass({ this.showCanceledDeletion(); } }.bind(this)); - window.refreshSession(); + this.props.dispatch(actions.refreshSession()); } } // JS error already logged by api mixin @@ -158,7 +148,7 @@ var Navigation = React.createClass({ }, function (err) { if (err) log.error(err); this.closeLogin(); - window.refreshSession(); + this.props.dispatch(actions.refreshSession()); }.bind(this)); }, handleAccountNavClick: function (e) { @@ -178,57 +168,48 @@ var Navigation = React.createClass({ this.setState({'registrationOpen': false}); }, completeRegistration: function () { - window.refreshSession(); + this.props.dispatch(actions.refreshSession()); this.closeRegistration(); }, render: function () { var classes = classNames({ 'inner': true, - 'logged-in': this.state.session.user + 'logged-in': this.props.session.user }); var messageClasses = classNames({ - 'messageCount': true, + 'message-count': true, 'show': this.state.unreadMessageCount > 0 }); var formatMessage = this.props.intl.formatMessage; + var createLink = this.props.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home'; return (
  • - - + +
  • - +
  • - +
  • - +
  • - +
  • @@ -236,35 +217,35 @@ var Navigation = React.createClass({ - {this.state.session.user ? [ + {this.props.session.user ? [
  • + title={formatMessage({id: 'general.messages'})}> {this.state.unreadMessageCount} - +
  • ,
  • + title={formatMessage({id: 'general.myStuff'})}> - +
  • ,
  • - - - {this.state.session.user.username} + + + {this.props.session.user.username}
  • - +
  • - +
  • - {this.state.session.permissions.educator ? [ + {this.props.session.permissions.educator ? [
  • - + + +
  • + ] : []} + {this.props.session.permissions.student ? [ +
  • + +
  • ] : []}
  • - +
  • - +
  • @@ -310,9 +290,7 @@ var Navigation = React.createClass({ ] : [
  • - +
  • , - - + className="ignore-react-onclickoutside" + key="login-link"> + + + onRequestClose={this.closeLogin} + key="login-dropdown"> @@ -356,4 +334,12 @@ var Navigation = React.createClass({ } }); -module.exports = injectIntl(Navigation); +var mapStateToProps = function (state) { + return { + session: state.session + }; +}; + +var ConnectedNavigation = connect(mapStateToProps)(Navigation); + +module.exports = injectIntl(ConnectedNavigation); diff --git a/src/components/navigation/navigation.scss b/src/components/navigation/navigation.scss index b2c11a71b..373e377e2 100644 --- a/src/components/navigation/navigation.scss +++ b/src/components/navigation/navigation.scss @@ -51,72 +51,73 @@ vertical-align: bottom; } } + } - .logo { - margin-right: 10px; + .logo { + margin-right: 10px; - a { - display: block; - transition: .15s ease all; - margin: 0 6px 0 0; - border: 0; + a { + display: block; + transition: .15s ease all; + margin: 0 6px 0 0; + border: 0; - background-image: url("/images/logo_sm.png"); - background-repeat: no-repeat; - background-position: center center; - background-size: 95%; - width: 81px; - height: 50px; + background-image: url("/images/logo_sm.png"); + background-repeat: no-repeat; + background-position: center center; + background-size: 95%; + width: 81px; + height: 50px; - &:hover { - transition: .15s ease all; - background-size: 100%; - } + &:hover { + transition: .15s ease all; + background-size: 100%; } } + } - .link { - > a { - display: block; - padding: 17px 15px 0 15px; - height: 33px; - - text-decoration: none; - white-space: nowrap; - color: $type-white; - font-size: .85rem; - font-weight: bold; - } - - > a:hover { - background-color: $active-gray; - } - } - - .search { - margin: 0 20px; - border-right: 0; + .link { + > a { + display: block; + padding: 17px 15px 0 15px; + height: 33px; + + text-decoration: none; + white-space: nowrap; color: $type-white; - flex-grow: 3; + font-size: .85rem; + font-weight: bold; - .ie9 & { - width: 100%; - } - - form { - margin: 0; - } - - input { - display: inline-block; - margin-top: 5px; - outline: none; - border: 0; + &:hover { background-color: $active-gray; - height: 14px; } + } - input[type=submit] { + } + + .search { + margin: 0 20px; + border-right: 0; + color: $type-white; + flex-grow: 3; + + .ie9 & { + width: 100%; + } + + form { + margin: 0; + } + + input { + display: inline-block; + margin-top: 5px; + outline: none; + border: 0; + background-color: $active-gray; + height: 14px; + + &[type=submit] { position: absolute; background-color: transparent; @@ -129,7 +130,7 @@ height: 40px; } - input[type=text] { + &[type=text] { transition: .15s ease background-color; padding: 0; padding-right: 10px; @@ -148,122 +149,125 @@ transition: .15s ease background-color; background-color: $active-dark-gray; } - } - .ie9 input[type=text] { - width: 70px; + .ie9 & { + width: 70px; + } } } + } - .right { - float: right; - margin-left: auto; - align-self: flex-end; + .right { + float: right; + margin-left: auto; + align-self: flex-end; - .ie9 & { - float: none; - } + .ie9 & { + float: none; + } - a:hover { + a { + &:hover { background-color: $active-gray; } } + } - .messages, - .mystuff { - > a { - background-repeat: no-repeat; - background-position: center center; - background-size: 45%; - padding-right: 10px; - padding-left: 10px; - width: 30px; - overflow: hidden; - text-indent: 50px; - white-space: nowrap; - } + .messages, + .mystuff { + > a { + background-repeat: no-repeat; + background-position: center center; + background-size: 45%; + padding-right: 10px; + padding-left: 10px; + width: 30px; + overflow: hidden; + text-indent: 50px; + white-space: nowrap; - > a:hover { + &:hover { background-size: 50%; } } - .messages { - > a { - background-image: url("/images/nav-notifications.png"); - } + } - .messageCount { - display: none; - - &.show { - display: block; - position: absolute; - top: .5rem; - right: .25rem; - border-radius: 1rem; - background-color: $ui-orange; - padding: 0 .25rem; - text-indent: 0; - line-height: 1rem; - color: $type-white; - font-size: .7rem; - font-weight: bold; - } - } + .messages { + > a { + background-image: url("/images/nav-notifications.png"); } - .mystuff { - > a { - background-image: url("/images/mystuff.png"); - } - } + .message-count { + display: none; - .login-dropdown { - width: 200px; - } - - .account-nav { - .userInfo { - padding-top: 14px; - max-width: 260px; - } - - > a { - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - font-size: .8125rem; - font-weight: normal; - - .avatar { - margin-right: 10px; - border-radius: 3px; - width: 24px; - height: 24px; - vertical-align: middle; - } - - &:after { - display: inline-block; - margin-left: 8px; - - background-image: url("/images/dropdown.png"); - background-repeat: no-repeat; - background-position: center center; - background-size: 50%; - width: 20px; - height: 20px; - vertical-align: middle; - content: " "; - } - } - - .dropdown { - padding: 0; - padding-top: 5px; - width: 100%; + &.show { + display: block; + position: absolute; + top: .5rem; + right: .25rem; + border-radius: 1rem; + background-color: $ui-orange; + padding: 0 .25rem; + text-indent: 0; + line-height: 1rem; + color: $type-white; + font-size: .7rem; + font-weight: bold; } } } + + .mystuff { + > a { + background-image: url("/images/mystuff.png"); + } + } + + .login-dropdown { + width: 200px; + } + + .account-nav { + .user-info { + padding-top: 14px; + max-width: 260px; + } + + > a { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + font-size: .8125rem; + font-weight: normal; + + .avatar { + margin-right: 10px; + border-radius: 3px; + width: 24px; + height: 24px; + vertical-align: middle; + } + + &:after { + display: inline-block; + margin-left: 8px; + + background-image: url("/images/dropdown.png"); + background-repeat: no-repeat; + background-position: center center; + background-size: 50%; + width: 20px; + height: 20px; + vertical-align: middle; + content: " "; + } + } + + .dropdown { + padding: 0; + padding-top: 5px; + width: 100%; + } + } } diff --git a/src/components/nestedcarousel/nestedcarousel.json b/src/components/nestedcarousel/nestedcarousel.json new file mode 100644 index 000000000..6dda5e6d8 --- /dev/null +++ b/src/components/nestedcarousel/nestedcarousel.json @@ -0,0 +1,42 @@ +[ + { + "title":"Start Dancing", + "thumbnails":[ + { + "type":"tip", + "title":"First, select a sprite", + "thumbnailUrl":"/images/microworlds/hiphop/dancer-sprite.png" + }, + { + "type":"tip", + "title":"Then, try this script", + "thumbnailUrl":"/images/microworlds/hiphop/switch-wait.gif" + }, + { + "type":"tip", + "title":"Start it!", + "thumbnailUrl":"/images/microworlds/hiphop/green-flag.gif" + } + ] + }, + { + "title":"Repeat the Dance", + "thumbnails":[ + { + "type":"tip", + "title":"Add another \"wait\"", + "thumbnailUrl":"/images/microworlds/hiphop/add-wait.gif" + }, + { + "type":"tip", + "title":"Drag this block over", + "thumbnailUrl":"/images/microworlds/hiphop/add-repeat.gif" + }, + { + "type":"tip", + "title":"Start it", + "thumbnailUrl":"/images/microworlds/hiphop/green-flag.gif" + } + ] + } +] \ No newline at end of file diff --git a/src/components/nestedcarousel/nestedcarousel.jsx b/src/components/nestedcarousel/nestedcarousel.jsx new file mode 100644 index 000000000..4600032e3 --- /dev/null +++ b/src/components/nestedcarousel/nestedcarousel.jsx @@ -0,0 +1,75 @@ +var classNames = require('classnames'); +var defaults = require('lodash.defaults'); +var React = require('react'); +var Slider = require('react-slick'); + +var Thumbnail = require('../thumbnail/thumbnail.jsx'); + +require('slick-carousel/slick/slick.scss'); +require('slick-carousel/slick/slick-theme.scss'); +require('./nestedcarousel.scss'); + + +{/* + NestedCarousel is used to show a carousel, where each slide is composed of a few + thumbnails (for example, to show step-by-syep tips, where each stage has a few steps). + It creates the thumbnails without links. + + Each slide has a title, and then a list of thumbnails, that will be shown together. +*/} +var NestedCarousel = React.createClass({ + type: 'NestedCarousel', + propTypes: { + items: React.PropTypes.array + }, + getDefaultProps: function () { + return { + items: require('./nestedcarousel.json') + }; + }, + render: function () { + var settings = this.props.settings || {}; + defaults(settings, { + dots: true, + infinite: false, + lazyLoad: true, + slidesToShow: 1, + slidesToScroll: 1, + variableWidth: false + }); + + var arrows = this.props.items.length > settings.slidesToShow; + + var classes = classNames( + 'nestedcarousel', + 'carousel', + this.props.className + ); + + var stages = []; + for (var i=0; i < this.props.items.length; i++) { + var items = this.props.items[i].thumbnails; + var thumbnails = []; + for (var j=0; j < items.length; j++) { + var item = items[j]; + thumbnails.push( + ); + } + stages.push( +
    +

    {this.props.items[i].title}

    + {thumbnails} +
    ); + } + return ( + + {stages} + + ); + } +}); + +module.exports = NestedCarousel; diff --git a/src/components/nestedcarousel/nestedcarousel.scss b/src/components/nestedcarousel/nestedcarousel.scss new file mode 100644 index 000000000..138e42d6b --- /dev/null +++ b/src/components/nestedcarousel/nestedcarousel.scss @@ -0,0 +1,20 @@ +@import "../../colors"; +@import "../carousel/carousel.scss"; + +.nestedcarousel { + /* Overrides carousel's settings for extra padding */ + .slick-slide { + padding: 0; + + /* Add some padding under the title for each slide */ + h3 { + padding-bottom: 10px; + } + + /* Align to top. Important when one of the slides have + more than one line of text */ + .thumbnail.project { + vertical-align: top; + } + } +} diff --git a/src/components/news/news.scss b/src/components/news/news.scss index e58bee698..bb74aae6d 100644 --- a/src/components/news/news.scss +++ b/src/components/news/news.scss @@ -46,10 +46,10 @@ color: $type-gray; font-size: .85rem; } - } - li:nth-child(even) { - border-top: 1px solid $ui-border; - border-bottom: 1px solid $ui-border; + &:nth-child(even) { + border-top: 1px solid $ui-border; + border-bottom: 1px solid $ui-border; + } } } diff --git a/src/components/page/page.jsx b/src/components/page/page.jsx new file mode 100644 index 000000000..d5143ed1c --- /dev/null +++ b/src/components/page/page.jsx @@ -0,0 +1,25 @@ +var React = require('react'); + +var Navigation = require('../navigation/navigation.jsx'); +var Footer = require('../footer/footer.jsx'); + +var Page = React.createClass({ + type: 'Page', + render: function () { + return ( +
    + +
    + {this.props.children} +
    + +
    + ); + } +}); + +module.exports = Page; diff --git a/src/components/spinner/spinner.scss b/src/components/spinner/spinner.scss index f021901d6..e4fd45305 100644 --- a/src/components/spinner/spinner.scss +++ b/src/components/spinner/spinner.scss @@ -21,27 +21,33 @@ width: 15%; height: 15%; content: ""; - -webkit-animation: circleFadeDelay 1.2s infinite ease-in-out both; } } @for $i from 1 through 12 { $rotation: 30deg * ($i - 1); $delay: -1.3s + $i * .1; + .circle#{$i} { transform: rotate($rotation); - -ms-transform: rotate($rotation); - -webkit-transform: rotate($rotation); - } - .circle#{$i}:before { - animation-delay: $delay; - -webkit-animation-delay: $delay; + + &:before { + animation-delay: $delay; + } } + } } @keyframes circleFadeDelay { - 0%, 39%, 100% { opacity: 0; } - 40% { opacity: 1; } + 0%, + 39%, + 100% { + opacity: 0; + } + + 40% { + opacity: 1; + } } diff --git a/src/components/subnavigation/subnavigation.scss b/src/components/subnavigation/subnavigation.scss index 0f951cd4e..76248f1d6 100644 --- a/src/components/subnavigation/subnavigation.scss +++ b/src/components/subnavigation/subnavigation.scss @@ -37,7 +37,7 @@ &.description { /* clear styling for info element */ - border: none; + border: 0; border-radius: none; text-decoration: none; @@ -47,7 +47,7 @@ } &:active { - border: none; + border: 0; box-shadow: none; background-color: transparent; } diff --git a/src/components/thumbnail/thumbnail.jsx b/src/components/thumbnail/thumbnail.jsx index e1e483136..a93247b26 100644 --- a/src/components/thumbnail/thumbnail.jsx +++ b/src/components/thumbnail/thumbnail.jsx @@ -16,6 +16,7 @@ var Thumbnail = React.createClass({ type: 'project', showLoves: false, showRemixes: false, + linkTitle: true, alt: '' }; }, @@ -55,13 +56,23 @@ var Thumbnail = React.createClass({
); } + + var imgElement,titleElement; + if (this.props.linkTitle) { + imgElement = + {this.props.alt} + ; + titleElement = {this.props.title}; + } else { + imgElement = ; + titleElement = this.props.title; + } + return (
- - {this.props.alt} - + {imgElement}
- {this.props.title} + {titleElement}
{extra}
diff --git a/src/components/thumbnail/thumbnail.scss b/src/components/thumbnail/thumbnail.scss index ffaea2754..29498673d 100644 --- a/src/components/thumbnail/thumbnail.scss +++ b/src/components/thumbnail/thumbnail.scss @@ -3,11 +3,11 @@ .thumbnail { .thumbnail-image { display: block; - } - .thumbnail-image img { - margin-bottom: 2px; - border: 1px solid $ui-border; + img { + margin-bottom: 2px; + border: 1px solid $ui-border; + } } $extras: ".thumbnail-creator, .thumbnail-loves, .thumbnail-remixes"; @@ -26,7 +26,7 @@ .thumbnail-title { margin-bottom: 1px; - font-size: .9230em; + font-size: .923em; font-weight: 800; a { @@ -58,12 +58,16 @@ } } - .thumbnail-loves:before { - background-image: url("/svgs/love/love_type-gray.svg"); + .thumbnail-loves { + &:before { + background-image: url("/svgs/love/love_type-gray.svg"); + } } - .thumbnail-remixes:before { - background-image: url("/svgs/remix/remix_type-gray.svg"); + .thumbnail-remixes { + &:before { + background-image: url("/svgs/remix/remix_type-gray.svg"); + } } &.project { diff --git a/src/components/welcome/welcome.scss b/src/components/welcome/welcome.scss index a0e3437a5..a2404d7e7 100644 --- a/src/components/welcome/welcome.scss +++ b/src/components/welcome/welcome.scss @@ -4,6 +4,7 @@ .box-content { padding: 0; } + .welcome-col { display: inline-block; margin: 10px 15px; @@ -33,26 +34,32 @@ height: 10px; content: ""; } + &.blue { #{$color-bars} { background-color: $splash-blue; } + a { color: $splash-blue; } } + &.green { #{$color-bars} { background-color: $splash-green; } + a { color: $splash-green; } } + &.pink { #{$color-bars} { background-color: $splash-pink; } + a { color: $splash-pink; } diff --git a/src/init.js b/src/init.js index f44c24647..3700537f1 100644 --- a/src/init.js +++ b/src/init.js @@ -1,55 +1,5 @@ -var api = require('./mixins/api.jsx').api; var jar = require('./lib/jar'); -/** - * ----------------------------------------------------------------------------- - * Session - * ----------------------------------------------------------------------------- - */ - -(function () { - window._session = {}; - - /** - * Binds the object to private session variable and dispatches a global - * "session" event. - * - * @param {object} Session object - * - * @return {void} - */ - window.updateSession = function (session) { - window._session = session; - var sessionEvent = new CustomEvent('session', session); - window.dispatchEvent(sessionEvent); - }; - - /** - * Gets a session object from the local proxy method. Calls "updateSession" - * once session has been returned from the proxy. - * - * @return {void} - */ - window.refreshSession = function () { - api({ - host: '', - uri: '/session/' - }, function (err, body) { - if (err) return; - - if (typeof body !== 'undefined') { - if (body.banned) { - return window.location = body.redirectUrl; - } else { - window.updateSession(body); - } - } - }); - }; - - // Fetch session - window.refreshSession(); -})(); /** * ----------------------------------------------------------------------------- diff --git a/src/l10n.json b/src/l10n.json index 0a3bf407e..9f2763296 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -22,6 +22,7 @@ "general.legal": "Legal", "general.learnMore": "Learn More", "general.messages": "Messages", + "general.myClass": "My Class", "general.myClasses": "My Classes", "general.myStuff": "My Stuff", "general.offlineEditor": "Offline Editor", diff --git a/src/lib/jar.js b/src/lib/jar.js index 951c2f2ab..89accd5a5 100644 --- a/src/lib/jar.js +++ b/src/lib/jar.js @@ -1,5 +1,6 @@ var cookie = require('cookie'); var xhr = require('xhr'); +var pako = require('pako'); /** * Module that handles coookie interactions. @@ -9,41 +10,70 @@ var xhr = require('xhr'); * set(name, value) – synchronously sets the cookie * use(name, uri, callback) – can by sync or async, gets cookie from the uri if not there. */ -var Jar = {}; +var Jar = { + unsign: function (value, callback) { + // Return the usable content portion of a signed, compressed cookie generated by + // Django's signing module + // https://github.com/django/django/blob/stable/1.8.x/django/core/signing.py + if (!value) return callback('No value to unsign'); + try { + var b64Data = value.split(':')[0]; + var decompress = false; + if (b64Data[0] === '.') { + decompress = true; + b64Data = b64Data.substring(1); + } -Jar.get = function (name, callback) { - // Get cookie by name - var obj = cookie.parse(document.cookie) || {}; + // Django makes its base64 strings url safe by replacing + and / with - and _ respectively + // using base64.urlsafe_b64encode + // https://docs.python.org/2/library/base64.html#base64.b64encode + b64Data = b64Data.replace(/[-_]/g, function (c) {return {'-':'+', '_':'/'}[c]; }); + var strData = atob(b64Data); - // Handle optional callback - if (typeof callback === 'function') { - if (typeof obj === 'undefined') return callback('Cookie not found.'); - return callback(null, obj[name]); - } + if (decompress) { + var charData = strData.split('').map(function (c) { return c.charCodeAt(0); }); + var binData = new Uint8Array(charData); + var data = pako.inflate(binData); + strData = String.fromCharCode.apply(null, new Uint16Array(data)); + } - return obj[name]; -}; + return callback(null, strData); + } catch (e) { + return callback(e); + } + }, + get: function (name, callback) { + // Get cookie by name + var obj = cookie.parse(document.cookie) || {}; -Jar.use = function (name, uri, callback) { - // Attempt to get cookie - Jar.get(name, function (err, obj) { - if (typeof obj !== 'undefined') return callback(null, obj); + // Handle optional callback + if (typeof callback === 'function') { + if (typeof obj === 'undefined') return callback('Cookie not found.'); + return callback(null, obj[name]); + } - // Make XHR request to cookie setter uri - xhr({ - uri: uri - }, function (err) { - if (err) return callback(err); - Jar.get(name, callback); + return obj[name]; + }, + use: function (name, uri, callback) { + // Attempt to get cookie + Jar.get(name, function (err, obj) { + if (typeof obj !== 'undefined') return callback(null, obj); + + // Make XHR request to cookie setter uri + xhr({ + uri: uri + }, function (err) { + if (err) return callback(err); + Jar.get(name, callback); + }); }); - }); -}; - -Jar.set = function (name, value) { - var obj = cookie.serialize(name, value); - var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString(); - var path = '; path=/'; - document.cookie = obj + expires + path; + }, + set: function (name, value) { + var obj = cookie.serialize(name, value); + var expires = '; expires=' + new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString(); + var path = '; path=/'; + document.cookie = obj + expires + path; + } }; module.exports = Jar; diff --git a/src/lib/render.jsx b/src/lib/render.jsx index 04791ab72..548447ff4 100644 --- a/src/lib/render.jsx +++ b/src/lib/render.jsx @@ -1,12 +1,18 @@ +var redux = require('redux'); +var thunk = require('redux-thunk').default; var ReactDOM = require('react-dom'); +var StoreProvider = require('react-redux').Provider; -var ReactIntl = require('./intl.jsx'); -var IntlProvider = ReactIntl.IntlProvider; +var IntlProvider = require('./intl.jsx').IntlProvider; +var actions = require('../redux/actions.js'); +var reducer = require('../redux/reducer.js'); require('../main.scss'); -var Navigation = require('../components/navigation/navigation.jsx'); -var Footer = require('../components/footer/footer.jsx'); +var store = redux.createStore( + reducer, + redux.applyMiddleware(thunk) +); var render = function (jsx, element) { // Get locale and messages from global namespace (see "init.js") @@ -21,37 +27,18 @@ var render = function (jsx, element) { } var messages = window._messages[locale]; - - // Render nav and footer for page. - var nav = ReactDOM.render( - - - , - document.getElementById('navigation') - ); - - var footer = ReactDOM.render( - -