├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config ├── defaults.env └── production.env ├── intl-config.json ├── npm_tasks ├── bin │ └── build-html.js ├── build-config.js ├── build-html.js ├── get-pages.js └── tests │ ├── build-html.test.js │ └── get-pages.test.js ├── package.json ├── src ├── colors.less ├── components │ ├── action-menu │ │ ├── action-menu.jsx │ │ └── action-menu.less │ ├── alert │ │ ├── alert.jsx │ │ └── alert.less │ ├── basic-element │ │ ├── basic-element.jsx │ │ ├── basic-element.less │ │ └── types │ │ │ ├── image.jsx │ │ │ ├── link.jsx │ │ │ ├── page.jsx │ │ │ ├── text.jsx │ │ │ └── textedit.js │ ├── button │ │ └── button.less │ ├── card │ │ ├── card.jsx │ │ └── card.less │ ├── color-group │ │ ├── color-group.jsx │ │ └── color-group.less │ ├── color-spectrum │ │ ├── color-spectrum.jsx │ │ └── color-spectrum.less │ ├── d-pad │ │ ├── d-pad.jsx │ │ └── d-pad.less │ ├── element-group │ │ ├── element-group.jsx │ │ └── element-group.less │ ├── featured-tag-card │ │ ├── featured-tag-card.jsx │ │ └── featured-tag-card.less │ ├── ftu │ │ ├── ftu.jsx │ │ └── ftu.less │ ├── get-media │ │ └── get-media.jsx │ ├── link │ │ ├── link.jsx │ │ └── link.less │ ├── loading │ │ ├── loading.jsx │ │ └── loading.less │ ├── modal-confirm │ │ ├── modal-confirm.jsx │ │ └── modal-confirm.less │ ├── modal-switch │ │ ├── modal-switch.jsx │ │ └── modal-switch.less │ ├── option-panel │ │ ├── option-panel.jsx │ │ └── option-panel.less │ ├── project-list │ │ ├── project-list.jsx │ │ └── project-list.less │ ├── range │ │ ├── range.jsx │ │ └── range.less │ ├── select │ │ └── select.less │ ├── shim │ │ ├── shim.jsx │ │ └── shim.less │ ├── snackbar │ │ ├── snackbar.jsx │ │ └── snackbar.less │ ├── spindicator │ │ ├── spindicator.jsx │ │ └── spindicator.less │ ├── tabs │ │ ├── tabs.jsx │ │ └── tabs.less │ └── text-input │ │ ├── text-input.jsx │ │ └── text-input.less ├── fonts.less ├── html │ └── index.html ├── lib │ ├── api.js │ ├── cartesian.js │ ├── color.js │ ├── dispatcher.js │ ├── errors.js │ ├── i18n.js │ ├── jsonUtils.js │ ├── keyboard.js │ ├── messages.js │ ├── platform.js │ ├── render.jsx │ ├── router.js │ ├── spec.js │ ├── swipe.js │ ├── touchhandler.js │ ├── uuid.js │ └── validators.js ├── locales │ ├── bn-BD.yaml │ ├── bn-IN.yaml │ ├── en-GB.yaml │ ├── en-US.yaml │ ├── es-MX.yaml │ ├── id-ID.yaml │ └── pt-BR.yaml ├── main.less ├── normalize.less ├── pages │ ├── discover │ │ ├── discover.jsx │ │ └── mock.json │ ├── element │ │ ├── element.jsx │ │ ├── element.less │ │ ├── font-selector.js │ │ ├── image-editor.jsx │ │ ├── link-editor.jsx │ │ ├── page-editor.jsx │ │ ├── text-editor.jsx │ │ └── witheditable.js │ ├── login │ │ ├── form-input.jsx │ │ ├── login.jsx │ │ ├── login.less │ │ ├── sign-in.jsx │ │ └── sign-up.jsx │ ├── make │ │ ├── make.jsx │ │ └── make.less │ ├── page │ │ ├── flattening.js │ │ ├── loader.js │ │ ├── page-controls.jsx │ │ ├── page.jsx │ │ └── page.less │ ├── project-settings │ │ ├── project-settings.jsx │ │ └── project-settings.less │ ├── project │ │ ├── cartzoom.js │ │ ├── dpad-logic.js │ │ ├── form-pages.js │ │ ├── loader.js │ │ ├── pageadmin.js │ │ ├── pageblock.jsx │ │ ├── project.jsx │ │ ├── project.less │ │ ├── remix.js │ │ ├── renderhelpers.js │ │ ├── setdestination.js │ │ └── transforms.js │ ├── style-guide │ │ ├── style-guide.jsx │ │ └── style-guide.less │ ├── tag-list │ │ ├── tag-list.jsx │ │ └── tag-list.less │ ├── tinker │ │ ├── tinker.jsx │ │ └── tinker.less │ ├── user-projects │ │ ├── user-projects.jsx │ │ └── user-projects.less │ └── web-link │ │ ├── web-link.jsx │ │ └── web-link.less ├── static │ ├── fonts │ │ ├── FiraSans-Medium.woff │ │ ├── FiraSans-Regular.woff │ │ └── lamson-sharpie-webfont.woff │ ├── img │ │ ├── B-white.svg │ │ ├── B.svg │ │ ├── I-white.svg │ │ ├── I.svg │ │ ├── U-white.svg │ │ ├── U.svg │ │ ├── add.png │ │ ├── align-center.svg │ │ ├── align-left.svg │ │ ├── align-right.svg │ │ ├── avatar-icon.svg │ │ ├── back-arrow.png │ │ ├── brush.svg │ │ ├── camera-gallery.svg │ │ ├── camera.svg │ │ ├── caret-down.svg │ │ ├── cc.svg │ │ ├── change-image.svg │ │ ├── curved-arrow-wide.svg │ │ ├── curved-arrow.svg │ │ ├── default.svg │ │ ├── demo.png │ │ ├── external-url.svg │ │ ├── flag.svg │ │ ├── gray_and_white_checkers.gif │ │ ├── link.svg │ │ ├── material-check-on.png │ │ ├── material-check.png │ │ ├── more-dots.svg │ │ ├── nub-white.svg │ │ ├── nub.svg │ │ ├── oval-droplet.svg │ │ ├── oval-flag.svg │ │ ├── oval-source.svg │ │ ├── page-screenshot.png │ │ ├── palette-icon.svg │ │ ├── pencil-blue.svg │ │ ├── pencil.svg │ │ ├── plus.svg │ │ ├── scrubber-white.svg │ │ ├── scrubber.svg │ │ ├── settings.svg │ │ ├── slider.png │ │ ├── take-photo.svg │ │ ├── text.svg │ │ ├── tinker.png │ │ ├── toucan.svg │ │ ├── trash.svg │ │ ├── web-search.svg │ │ ├── webmaker-icon.svg │ │ ├── x.svg │ │ ├── zoom-in.svg │ │ ├── zoom-out-blue.svg │ │ └── zoom-out.svg │ └── js │ │ └── Intl.js ├── tests │ ├── api.test.js │ ├── color-spectrum.test.js │ ├── jsonUtils.test.js │ ├── router.test.js │ └── spec.test.js └── variables.less └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig : http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/tests/**/*.js 2 | src/static/js/Intl.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [2, 2], 4 | "no-empty": [2], 5 | "no-console": [0], 6 | "comma-dangle": [2], 7 | "no-unused-vars": [0], 8 | "linebreak-style": [2, "unix"], 9 | "semi": [2, "always"] 10 | }, 11 | "env": { 12 | "commonjs": true, 13 | "es6": true, 14 | "browser": true 15 | }, 16 | "extends": "eslint:recommended", 17 | "ecmaFeatures": { 18 | "es6": true, 19 | "jsx": true, 20 | "experimentalObjectRestSpread": true 21 | }, 22 | "plugins": [ 23 | "react" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | config.js 4 | npm-debug.log* 5 | TODO.txt 6 | dest/ 7 | .env 8 | src/locales/*.json 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.1.0' 4 | sudo: false 5 | cache: 6 | directories: 7 | - node_modules 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ## Reporting issues 4 | 5 | - **Search for existing issues.** Please check to see if someone else has reported the same issue. 6 | - **Share as much information as possible.** Please include your device model and OS version. Also, include steps to reproduce the bug. 7 | 8 | ## Code Style for Patches 9 | 10 | For basic file formatting rules, we use [EditorConfig](http://editorconfig.org/). There's an `.editorconfig` in the root of the project. 11 | 12 | Please ensure your editor either supports EditorConfig by default or has a plugin installed for it. 13 | 14 | ### JavaScript 15 | 16 | JS files must pass ESLint using the provided [.eslintrc](https://github.com/mozilla/webmaker-core/blob/develop/.eslintrc) settings. 17 | 18 | Run `npm test` before pushing a commit. It will validate your JS. 19 | 20 | #### Variable Naming 21 | 22 | - `lowerCamelCase` General variables 23 | - `UPPER_CASE` Constants 24 | 25 | ### HTML / JSX 26 | 27 | - 2 space indentation 28 | - Class names use hypenated case (e.g, `my-class-name`) 29 | 30 | ### LESS / CSS 31 | 32 | - 2 space indentation 33 | - Always a space after a property's colon (e.g, `display: block;` and not `display:block;`) 34 | - End all lines with a semi-colon 35 | - For multiple, comma-separated selectors, place each selector on it's own line 36 | 37 | ## Testing 38 | 39 | Please test your patch on an Android device if possible. If you don't have access to a physical device you can test using an Android emulator instead. 40 | 41 | ## Pull Requests 42 | 43 | - After opening your PR, add the `PR Needs Review` tag. One of our core contributors will then review your patch and either merge it or request further changes (also indicated by the `PR Needs Work` tag). If you are a core contributor, you may merge your own patch once it has passed peer review. 44 | - If possible, [squash your commits](http://davidwalsh.name/squash-commits-git). 45 | - Try to share which devices your code has been tested on when submitting a pull request. 46 | -------------------------------------------------------------------------------- /config/defaults.env: -------------------------------------------------------------------------------- 1 | # Defaults 2 | 3 | CLIENT_ID='wm_id_zIPGbkEDB5Cv9dCzo7nS' 4 | API_URI='https://webmaker-api.herokuapp.com' 5 | LOGIN_URI='https://id.mofostaging.net' 6 | -------------------------------------------------------------------------------- /config/production.env: -------------------------------------------------------------------------------- 1 | # Production 2 | # DO NOT COMMIT SECRETS 3 | 4 | API_URI='https://api.webmaker.org' 5 | LOGIN_URI='https://id.webmaker.org' 6 | -------------------------------------------------------------------------------- /intl-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportedLocales": ["en-US", "en-GB", "pt-BR", "bn-BD", "id-ID","bn-IN"], 3 | "dest": "src/locales", 4 | "src": "src/locales", 5 | "type": "json" 6 | } 7 | -------------------------------------------------------------------------------- /npm_tasks/bin/build-html.js: -------------------------------------------------------------------------------- 1 | var argv = require('minimist')(process.argv.slice(2)); 2 | require('../build-html')(argv); 3 | -------------------------------------------------------------------------------- /npm_tasks/build-config.js: -------------------------------------------------------------------------------- 1 | var habitat = require('habitat'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | 5 | var defaultPath = path.join(__dirname, '../config/defaults.env'); 6 | var prodPath = path.join(__dirname, '../config/production.env'); 7 | 8 | function fileErrors() { 9 | process.stderr.write( 10 | 'Looks like there is a problem with your config paths:\n' + 11 | '> ' + defaultPath + '\n' + 12 | '> ' + prodPath + '\n' + 13 | 'See npm_tasks/build-config.js\n' 14 | ); 15 | process.exit(1); 16 | } 17 | 18 | // Check paths to make sure they exist 19 | try { 20 | if (!fs.statSync(defaultPath).isFile() || !fs.statSync(defaultPath)) { 21 | fileErrors(); 22 | } 23 | } catch (e) { 24 | fileErrors(); 25 | } 26 | 27 | // Local environment in .env overwrites everything else 28 | habitat.load('.env'); 29 | 30 | var environment = habitat.get('NODE_ENV', '').toLowerCase(); 31 | 32 | 33 | if (environment === 'production') { 34 | habitat.load(prodPath); 35 | } 36 | 37 | habitat.load(defaultPath); 38 | 39 | var config = { 40 | CLIENT_ID: habitat.get('CLIENT_ID'), 41 | API_URI: habitat.get('API_URI'), 42 | LOGIN_URI: habitat.get('LOGIN_URI') 43 | }; 44 | 45 | process.stdout.write( 46 | '// THIS IS A GENERATED FILE. EDIT npm_tasks/build-config.js INSTEAD\n' + 47 | 'module.exports = ' + JSON.stringify(config) + ';\n' 48 | ); 49 | -------------------------------------------------------------------------------- /npm_tasks/build-html.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var path = require('path'); 3 | var getPages = require('./get-pages'); 4 | require('colors'); 5 | 6 | // For the command line utility, look in npm_tasks/bin 7 | module.exports = function (options) { 8 | options = options || {}; 9 | var baseDir = options.baseDir || './dest/pages/'; 10 | var template = options.template || fs.readFileSync('./src/html/index.html', {encoding: 'utf-8'}); 11 | var pages = getPages(); 12 | pages.forEach(function (page) { 13 | var html = template.replace('{{ js_src }}', '../../js/' + page + '.bundle.js'); 14 | fs.outputFileSync(path.join(baseDir, page, '/index.html'), html); 15 | }); 16 | console.log(('Built html for pages: ' + pages.join(', ') + '\n').green); 17 | }; 18 | -------------------------------------------------------------------------------- /npm_tasks/get-pages.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var PAGES_DIR = './src/pages/'; 3 | var path = require('path'); 4 | 5 | module.exports = function (pagesDir) { 6 | pagesDir = pagesDir || PAGES_DIR; 7 | var pages = fs.readdirSync(pagesDir); 8 | return pages.filter(function (page) { 9 | var dirPath = path.join(pagesDir, page); 10 | if (!fs.statSync(dirPath).isDirectory()) return false; 11 | if (fs.readdirSync(dirPath).indexOf(page + '.jsx') === -1) return false; 12 | return true; 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /npm_tasks/tests/build-html.test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var should = require('should'); 3 | var path = require('path'); 4 | var tmp = require('tmp'); 5 | var proxyquire = require('proxyquire'); 6 | 7 | var fakePages= ['bar', 'baz', 'foo']; 8 | var fakeTemplate = '\n'; 9 | 10 | var buildHtml = proxyquire('../build-html', { 11 | './get-pages': function () { 12 | return fakePages; 13 | } 14 | }); 15 | 16 | tmp.setGracefulCleanup(); 17 | 18 | describe('build-html (js)', function () { 19 | var fakeDir; 20 | beforeEach(function () { 21 | fakeDir = tmp.dirSync().name; 22 | buildHtml({ 23 | baseDir: fakeDir, 24 | template: fakeTemplate 25 | }); 26 | }); 27 | it('should create directories in the base dir', function () { 28 | should.deepEqual(fs.readdirSync(fakeDir), fakePages); 29 | }); 30 | 31 | fakePages.forEach(function (page) { 32 | it('should create an index.html for ' + page, function () { 33 | var filePath = path.join(fakeDir, page, 'index.html'); 34 | should(fs.existsSync(filePath)).be.equal(true); 35 | }); 36 | it('should contain the text ' + page + '.bundle.js', function () { 37 | var filePath = path.join(fakeDir, page, 'index.html'); 38 | var output = fs.readFileSync(filePath, {encoding: 'utf-8'}); 39 | should(output.match(page + '.bundle.js')).be.ok; 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /npm_tasks/tests/get-pages.test.js: -------------------------------------------------------------------------------- 1 | var getPages = require('../get-pages'); 2 | var fs = require('fs-extra'); 3 | var path = require('path'); 4 | var should = require('should'); 5 | var tmp = require('tmp'); 6 | tmp.setGracefulCleanup(); 7 | 8 | function outputFakeFile(baseDir, name) { 9 | var f = fs.outputFileSync(path.join(baseDir, name), 'fake stuff 123\n'); 10 | } 11 | 12 | describe('getPages', function () { 13 | var fakeDir; 14 | beforeEach(function () { 15 | fakeDir = tmp.dirSync().name; 16 | }); 17 | it('should read the path directory in alpha order', function () { 18 | outputFakeFile(fakeDir, 'foo/foo.jsx'); 19 | outputFakeFile(fakeDir, 'bar/bar.jsx'); 20 | outputFakeFile(fakeDir, 'baz/baz.jsx'); 21 | should.deepEqual(getPages(fakeDir), ['bar', 'baz', 'foo']); 22 | }); 23 | it('should skip directories without properly formatted jsx files', function () { 24 | outputFakeFile(fakeDir, 'foo/foo.jsx'); 25 | outputFakeFile(fakeDir, 'bar/bar.js'); 26 | outputFakeFile(fakeDir, 'baz/index.jsx'); 27 | fs.mkdir(path.join(fakeDir, 'qux')); 28 | should.deepEqual(getPages(fakeDir), ['foo']); 29 | }); 30 | it('should return an empty array for an empty dir', function () { 31 | should.deepEqual(getPages(fakeDir), []); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmaker-core", 3 | "version": "1.6.1", 4 | "description": "Web core for Webmaker App", 5 | "scripts": { 6 | "start": "npm run build && npm-run-all --parallel webserver watch:**", 7 | "config": "node ./npm_tasks/build-config.js > src/config.js", 8 | "build": "npm-run-all build:**", 9 | "build:clean": "rimraf dest/ && mkdirp dest/", 10 | "build:static": "ncp src/static/ dest/", 11 | "build:l10njson": "yamlconvert intl-config.json", 12 | "build:js": "npm run config && webpack", 13 | "build:css": "npm run watch:css -- --no-watch", 14 | "build:html": "node ./npm_tasks/bin/build-html", 15 | "test": "npm run config && npm-run-all test:**", 16 | "test:mocha": "mocha -R spec --compilers js:babel/register \"./{src,npm_tasks}/**/*.test.js\"", 17 | "test:lint": "eslint --ext .js,.jsx src/", 18 | "watch:static": "watch \"npm run build:static\" src/static", 19 | "watch:js": "npm run build:js -- -d --watch", 20 | "watch:css": "autoless --source-map --autoprefix \"last 2 versions, android >= 4.2\" src dest", 21 | "watch:html": "watch \"npm run build:html\" src/html", 22 | "webserver": "live-server ./dest --port=4242" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/mozilla/webmaker-core.git" 27 | }, 28 | "author": "", 29 | "license": "MPL-2.0", 30 | "bugs": { 31 | "url": "https://github.com/mozilla/webmaker-core/issues" 32 | }, 33 | "homepage": "https://github.com/mozilla/webmaker-core", 34 | "engines": { 35 | "node": "^4.1.0", 36 | "npm": "^2.7.4" 37 | }, 38 | "devDependencies": { 39 | "autoless": "^0.1.7", 40 | "babel": "^5.3.3", 41 | "babel-core": "^5.8.25", 42 | "babel-loader": "^5.3.2", 43 | "colors": "^1.1.0", 44 | "eslint": "^1.3.1", 45 | "eslint-plugin-react": "^3.3.1", 46 | "fs-extra": "^0.24.0", 47 | "git-rev-sync": "^1.1.0", 48 | "habitat": "^3.1.2", 49 | "intl-locales-supported": "^1.0.0", 50 | "json-loader": "^0.5.1", 51 | "jsx-loader": "^0.13.2", 52 | "live-server": "^0.9.0", 53 | "minimist": "^1.1.1", 54 | "mkdirp": "^0.5.0", 55 | "mocha": "^2.2.1", 56 | "ncp": "^2.0.0", 57 | "npm-run-all": "^1.2.2", 58 | "proxyquire": "^1.4.0", 59 | "rimraf": "^2.3.2", 60 | "should": "^8.0.2", 61 | "tmp": "^0.0.28", 62 | "watch": "^0.16.0", 63 | "webpack": "^1.7.3", 64 | "yaml-intl-xml-json-converter": "0.0.7" 65 | }, 66 | "dependencies": { 67 | "classnames": "2.2.1", 68 | "color": "0.10.1", 69 | "intl": "1.0.0", 70 | "lodash.defaults": "3.1.2", 71 | "lru-cache": "2.7.0", 72 | "owasp-password-strength-test": "1.3.0", 73 | "panzoom": "https://github.com/k88hudson/panzoom.git#b3b698d44ed12129c723f76efbd431678a34d96a", 74 | "react": "0.14.0", 75 | "react-addons-linked-state-mixin": "0.14.0", 76 | "react-addons-update": "0.14.0", 77 | "react-dom": "0.14.0", 78 | "react-hammerjs": "0.3.0", 79 | "react-intl": "1.2.0", 80 | "xhr": "2.2.0" 81 | }, 82 | "jshintConfig": { 83 | "esnext": true 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/colors.less: -------------------------------------------------------------------------------- 1 | @blue: #669eff; 2 | @shadowBlue: #5793f2; 3 | @sapphire: #315d8a; 4 | @teal: #61d3c9; 5 | @slate: #6d7886; 6 | @darkSlate: #484B50; 7 | @heatherGrey: #cdd2dd; 8 | @lightGrey: #e9ebf0; 9 | @softGrey: #f2f6fc; 10 | @plum: #404467; 11 | @shadowPlum: #31344f; 12 | @brick: #EB5D52; 13 | @aqua: #9FD0E0; 14 | @green: #98CC3B; 15 | @yellow: #F0C337; 16 | @orange: #E26A1F; 17 | @purple: #8173E4; 18 | @black: #000; 19 | @white: #fff; 20 | @overlay: rgba(26, 34, 55, 70%); 21 | -------------------------------------------------------------------------------- /src/components/action-menu/action-menu.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classNames = require('classnames'); 3 | var assign = require('react/lib/Object.assign'); 4 | var Link = require('../link/link.jsx'); 5 | 6 | var Menu = React.createClass({ 7 | render: function () { 8 | return (
9 | {this.props.children} 10 |
); 11 | } 12 | }); 13 | 14 | function makeButtonType(type) { 15 | return React.createClass({ 16 | render: function () { 17 | var className = classNames(type, this.props.side, this.props.className, { 18 | off: this.props.off 19 | }); 20 | var Tag = this.props.url ? Link : 'button'; 21 | var props = assign({}, this.props, {className}); 22 | return ( 23 | 24 | {this.props.children} 25 | ); 26 | } 27 | }); 28 | } 29 | 30 | module.exports = { 31 | Menu, 32 | PrimaryButton: makeButtonType('primary-btn'), 33 | SecondaryButton: makeButtonType('secondary-btn'), 34 | FullWidthButton: makeButtonType('full-width-btn btn btn-teal') 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/action-menu/action-menu.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Action Menu 3 | */ 4 | .action-menu { 5 | @_bottomGutter: 10px; 6 | @_primarySize: 56px; 7 | @_fullWidthSize: 42px; 8 | @_secondarySize: 40px; 9 | @_spacing: 56px; 10 | 11 | position: fixed; 12 | left: 50%; 13 | bottom: @_bottomGutter; 14 | z-index: @ZINDEX_ACTION_OVERLAY; 15 | width: @_primarySize; 16 | margin-left: -(@_primarySize/2); 17 | 18 | &.full-width { 19 | left: 20px; 20 | right: 20px; 21 | width: auto; 22 | margin-left: 0; 23 | } 24 | 25 | &.off { 26 | transform: translateY(@_bottomGutter + @_primarySize); 27 | } 28 | 29 | .primary-btn, 30 | .secondary-btn, 31 | .full-width-btn { 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | transition: transform 0.2s ease-in-out; 36 | 37 | .icon { 38 | display: block; 39 | } 40 | } 41 | 42 | .primary-btn, 43 | .secondary-btn { 44 | border-radius: 50%; 45 | border: none; 46 | } 47 | 48 | .primary-btn { 49 | width: @_primarySize; 50 | height: @_primarySize; 51 | background: @blue; 52 | color: @white; 53 | box-shadow: 0 4px 3px -2px rgba(0,0,0,0.2); 54 | position: absolute; 55 | bottom: 0; 56 | left: 0; 57 | 58 | .icon { 59 | height: 22px; 60 | } 61 | 62 | &.off { 63 | transform: translateY(@_bottomGutter + @_primarySize); 64 | } 65 | } 66 | .secondary-btn { 67 | position: absolute; 68 | left: 50%; 69 | margin-left: -(@_secondarySize/2); 70 | bottom: ((@_primarySize - @_secondarySize)/2); 71 | width: @_secondarySize; 72 | height: @_secondarySize; 73 | background: @white; 74 | color: @slate; 75 | box-shadow: 0 4px 3px -2px rgba(0,0,0,0.2); 76 | &.left { 77 | margin-left: -(@_secondarySize/2 + @_spacing); 78 | } 79 | &.right { 80 | margin-left: -(@_secondarySize/2 - @_spacing); 81 | } 82 | &.off { 83 | transform: translateY(@_bottomGutter + @_secondarySize * 2); 84 | } 85 | 86 | .icon { 87 | height: 15px; 88 | } 89 | } 90 | .btn.full-width-btn { 91 | height: @_fullWidthSize; 92 | padding: 0; 93 | position: absolute; 94 | bottom: 0; 95 | margin: 0; 96 | width: 100%; 97 | 98 | &.active, 99 | &:active { 100 | top: auto; 101 | bottom: -2px; 102 | } 103 | 104 | &.off { 105 | transform: translateY(@_bottomGutter + @_fullWidthSize); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/components/alert/alert.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Alert = React.createClass({ 4 | getInitialState: function () { 5 | return { 6 | isVisible: false 7 | }; 8 | }, 9 | show: function () { 10 | this.setState({ 11 | isVisible: true 12 | }); 13 | }, 14 | hide: function () { 15 | this.setState({ 16 | isVisible: false 17 | }); 18 | }, 19 | render: function () { 20 | return ( 21 |
22 |
23 | {this.props.children} 24 |
25 |
26 | ); 27 | } 28 | }); 29 | 30 | module.exports = Alert; 31 | -------------------------------------------------------------------------------- /src/components/alert/alert.less: -------------------------------------------------------------------------------- 1 | .alert { 2 | align-items: center; 3 | background: @blue; 4 | border-radius: 5px; 5 | border: 1px solid rgba(0,67,104,0.36); 6 | color: @white; 7 | display: flex; 8 | margin: 10px 0; 9 | padding: 10px 20px; 10 | 11 | .text { 12 | flex-grow: 1; 13 | text-align: center; 14 | padding: 0 10px; 15 | } 16 | 17 | .dismiss { 18 | background: none; 19 | border: none; 20 | font-weight: 100; 21 | flex-shrink: 0; 22 | width: 10px; 23 | height: 15px; 24 | background: url(../img/x.svg) center center / contain no-repeat; 25 | } 26 | 27 | &.hidden { 28 | display: none; 29 | } 30 | 31 | .hidden { 32 | visibility: hidden; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/basic-element/basic-element.less: -------------------------------------------------------------------------------- 1 | .el-container { 2 | position: absolute; 3 | height: 1px; 4 | width: 100%; 5 | left: 50%; 6 | top: 50%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | margin-left: -50%; 11 | 12 | .icon { 13 | position: relative; 14 | top: 2px; 15 | height: 1em; 16 | margin-right: 0.5em; 17 | } 18 | } 19 | 20 | .touch-overlay { 21 | position: fixed; 22 | top: 0; 23 | right: 0; 24 | bottom: 0; 25 | left: 0; 26 | z-index: @ZINDEX_MAX; 27 | } 28 | 29 | .el { 30 | flex-shrink: 0; 31 | 32 | user-select: none; 33 | 34 | position: relative; 35 | z-index: 0; 36 | 37 | &.current { 38 | margin: -1px; 39 | border: 1px dashed @white; 40 | 41 | &:after { 42 | z-index: -1; 43 | content: ''; 44 | display: block; 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | width: 100%; 49 | height: 100%; 50 | border: 1px dashed @blue; 51 | } 52 | } 53 | } 54 | 55 | /********************************************************* 56 | * Types 57 | */ 58 | 59 | .el { 60 | box-sizing: content-box; 61 | // Text 62 | &.el-text { 63 | max-width: 320px; 64 | p { 65 | margin: 0; 66 | } 67 | } 68 | 69 | // Link 70 | &.el-link { 71 | .btn { 72 | margin: 0; 73 | //This line can probably be removed eventually, as box-shadow is being applied in the component properties now. 74 | //But removing it might affect existing projects, so hesitant to do so quite yet. 75 | box-shadow: inset 0px 2px 0px rgba(255,255,255, 0.3); 76 | display: flex; 77 | align-items: center; 78 | 79 | img { 80 | top: 1px; 81 | opacity: 0.8; 82 | height: 100%; 83 | margin: 0 0 0 8px; 84 | } 85 | } 86 | } 87 | 88 | // Image 89 | &.el-image { 90 | img { 91 | display: block; 92 | max-width: 300px; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/components/basic-element/types/image.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var assign = require('react/lib/Object.assign'); 3 | var Spec = require('../../../lib/spec'); 4 | 5 | var BASE_DEFAULT_URL = 'https://stuff.webmaker.org/webmaker-android/default-images/'; 6 | var TOTAL_PNGS = 10; 7 | var TOTAL_SVGS = 5; 8 | 9 | var spec = new Spec('image', assign({ 10 | src: { 11 | category: 'attributes', 12 | validation: React.PropTypes.string, 13 | default: function () { 14 | var max = TOTAL_PNGS + TOTAL_SVGS; 15 | var randomInt = Math.floor(Math.random() * max); 16 | var ext; 17 | if (randomInt >= TOTAL_PNGS) { 18 | randomInt -= TOTAL_PNGS; 19 | ext = '.svg'; 20 | } else { 21 | ext = '.jpg'; 22 | } 23 | return BASE_DEFAULT_URL + randomInt + ext; 24 | } 25 | }, 26 | alt: { 27 | category: 'attributes', 28 | validation: React.PropTypes.string, 29 | default: '' 30 | }, 31 | opacity: { 32 | category: 'styles', 33 | validation: React.PropTypes.number, 34 | default: 1 35 | }, 36 | borderWidth: { 37 | category: 'styles', 38 | validation: React.PropTypes.number, 39 | default: 0 40 | }, 41 | borderColor: { 42 | category: 'styles', 43 | validation: React.PropTypes.string, 44 | editor: 'color', 45 | default: '' 46 | }, 47 | borderRadius: { 48 | category: 'styles', 49 | validation: React.PropTypes.number, 50 | default: 0 51 | } 52 | }, Spec.getPositionProps())); 53 | 54 | module.exports = React.createClass({ 55 | 56 | statics: {spec}, 57 | 58 | propTypes: spec.getPropTypes(), 59 | 60 | getDefaultProps: function () { 61 | return spec.getDefaultProps(); 62 | }, 63 | 64 | render: function() { 65 | var props = this.props; 66 | var style = { 67 | opacity: props.opacity, 68 | borderStyle: 'solid', 69 | borderWidth: props.borderWidth, 70 | borderColor: props.borderColor, 71 | borderRadius: props.borderRadius 72 | }; 73 | return {this.props.alt}; 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/components/basic-element/types/link.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var assign = require('react/lib/Object.assign'); 3 | var {getContrastingColor, darken} = require('../../../lib/color'); 4 | var Spec = require('../../../lib/spec'); 5 | var dispatcher = require('../../../lib/dispatcher'); 6 | var Color = require("color"); 7 | 8 | var spec = new Spec('link', assign({ 9 | innerHTML: { 10 | category: 'attributes', 11 | validation: React.PropTypes.string, 12 | default: 'Button link' 13 | }, 14 | targetPageId: { 15 | category: 'attributes', 16 | validation: React.PropTypes.string, 17 | default: '' 18 | }, 19 | targetProjectId: { 20 | category: 'attributes', 21 | validation: React.PropTypes.string, 22 | default: '' 23 | }, 24 | targetUserId: { 25 | category: 'attributes', 26 | validation: React.PropTypes.string, 27 | default: '' 28 | }, 29 | targetWebURL: { 30 | category: 'attributes', 31 | validation: React.PropTypes.string, 32 | default: '' 33 | }, 34 | href: { 35 | category: 'attributes', 36 | validation: React.PropTypes.string, 37 | default: '' 38 | }, 39 | fontFamily: { 40 | category: 'styles', 41 | validation: React.PropTypes.string, 42 | default: 'sans-serif' 43 | }, 44 | backgroundColor: { 45 | category: 'styles', 46 | validation: React.PropTypes.string, 47 | default: '#69A0FC', 48 | editor: 'color' 49 | }, 50 | borderRadius: { 51 | category: 'styles', 52 | validation: React.PropTypes.number, 53 | default: 3 54 | }, 55 | color: { 56 | category: 'styles', 57 | validation: React.PropTypes.string, 58 | editor: 'color' 59 | }, 60 | boxShadow: { 61 | category: 'styles', 62 | validation: React.PropTypes.string, 63 | default: 'inset 0px 2px 0px rgba(255, 255, 255, 0.3)' 64 | } 65 | }, Spec.getPositionProps())); 66 | 67 | var Link = React.createClass({ 68 | 69 | mixins: [ 70 | require('./textedit') 71 | ], 72 | 73 | statics: {spec}, 74 | 75 | propTypes: spec.getPropTypes(), 76 | 77 | getDefaultProps: function () { 78 | return assign(spec.getDefaultProps(), { 79 | active: false 80 | }); 81 | }, 82 | 83 | onClick: function (event) { 84 | if (this.state.editing) { 85 | this.activate(); 86 | } else { 87 | dispatcher.fire('linkClicked', { 88 | props: this.props, 89 | originalEvent: event 90 | }); 91 | } 92 | }, 93 | 94 | render: function() { 95 | var props = this.props; 96 | 97 | var shadowOpacity = new Color(props.backgroundColor).alpha(); 98 | var style = { 99 | borderRadius: props.borderRadius, 100 | backgroundColor: props.backgroundColor, 101 | border: `1px solid ${darken(props.backgroundColor, 0.4)}`, 102 | color: props.color || getContrastingColor(props.backgroundColor), 103 | fontFamily: props.fontFamily, 104 | whiteSpace: props.whiteSpace, 105 | boxShadow: `inset 0px 2px 0px rgba(255, 255, 255, ${0.3*shadowOpacity})` 106 | }; 107 | 108 | var Element = this.props.activelink ? 'a' : 'span'; 109 | var content = this.makeEditable(props.innerHTML, style); 110 | 111 | return ( 112 | 113 | { content } 114 | 115 | 116 | 117 | 118 | ); 119 | } 120 | }); 121 | 122 | module.exports = Link; 123 | -------------------------------------------------------------------------------- /src/components/basic-element/types/page.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Spec = require('../../../lib/spec'); 3 | var assign = require('react/lib/Object.assign'); 4 | 5 | var spec = new Spec('page', { 6 | backgroundColor: { 7 | category: 'styles', 8 | validation: React.PropTypes.string, 9 | default: '#f2f6fc' 10 | } 11 | }); 12 | 13 | var Page = React.createClass({ 14 | 15 | statics: {spec}, 16 | 17 | propTypes: spec.getPropTypes(), 18 | 19 | getDefaultProps: function () { 20 | return assign(spec.getDefaultProps(), { 21 | active: false 22 | }); 23 | }, 24 | 25 | render: function() { 26 | var style = { 27 | backgroundColor: this.props.backgroundColor, 28 | width: "100%", 29 | height: "100%" 30 | }; 31 | 32 | return ( 33 |
34 | ); 35 | } 36 | }); 37 | 38 | module.exports = Page; 39 | -------------------------------------------------------------------------------- /src/components/basic-element/types/text.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var assign = require('react/lib/Object.assign'); 3 | var Spec = require('../../../lib/spec'); 4 | var HELLO_STRINGS = ['Hello', 'Bonjour', 'Halló', 'Hej', 'Olá', '¡Hola!', 'Ciao', 'Halo']; 5 | 6 | var spec = new Spec('text', assign({ 7 | innerHTML: { 8 | category: 'attributes', 9 | validation: React.PropTypes.string, 10 | default: function () { 11 | var randomIndex = Math.floor(Math.random() * HELLO_STRINGS.length); 12 | return HELLO_STRINGS[randomIndex]; 13 | } 14 | }, 15 | fontFamily: { 16 | category: 'styles', 17 | validation: React.PropTypes.string, 18 | default: 'Roboto' 19 | }, 20 | color: { 21 | category: 'styles', 22 | validation: React.PropTypes.string, 23 | default: '#E06A2C', 24 | editor: 'color' 25 | }, 26 | backgroundColor: { 27 | category: 'styles', 28 | validation: React.PropTypes.string, 29 | default: 'transparent', 30 | editor: 'color' 31 | }, 32 | fontSize: { 33 | category: 'styles', 34 | validation: React.PropTypes.number, 35 | default: 35 36 | }, 37 | fontStyle: { 38 | category: 'styles', 39 | validation: React.PropTypes.string, 40 | default: 'normal' 41 | }, 42 | fontWeight: { 43 | category: 'styles', 44 | validation: React.PropTypes.string, 45 | default: 'bold' 46 | }, 47 | textDecoration: { 48 | category: 'styles', 49 | validation: React.PropTypes.string, 50 | default: 'none' 51 | }, 52 | textAlign: { 53 | category: 'styles', 54 | validation: React.PropTypes.string, 55 | default: 'center' 56 | }, 57 | padding: { 58 | category: 'styles', 59 | default: '0 10px' 60 | } 61 | }, Spec.getPositionProps())); 62 | 63 | module.exports = React.createClass({ 64 | mixins: [ 65 | require('./textedit') 66 | ], 67 | 68 | statics: {spec}, 69 | 70 | propTypes: spec.getPropTypes(), 71 | 72 | getDefaultProps: function () { 73 | return spec.getDefaultProps(); 74 | }, 75 | 76 | render: function() { 77 | var props = this.props; 78 | var style = {}; 79 | 80 | [ 81 | 'fontFamily', 82 | 'color', 83 | 'fontWeight', 84 | 'fontSize', 85 | 'fontStyle', 86 | 'textDecoration', 87 | 'textAlign', 88 | 'backgroundColor', 89 | 'padding' 90 | ].forEach(function (prop) { 91 | style[prop] = props[prop]; 92 | }); 93 | 94 | var content = this.makeEditable(props.innerHTML, style); 95 | var onPClick = this.activate; 96 | if (this.state.editing) { 97 | onPClick = false; 98 | } 99 | return

{content}

; 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /src/components/button/button.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Button 3 | */ 4 | .btn { 5 | text-decoration: none; 6 | position: relative; 7 | padding: 12px 24px; 8 | margin-bottom: 10px; 9 | color: @slate; 10 | background: @white; 11 | border: none; 12 | border-radius: 3px; 13 | box-shadow: 0 2px 0 0 @heatherGrey; 14 | display: inline-flex; 15 | align-items: center; 16 | justify-content: center; 17 | .active { 18 | box-shadow: none; 19 | top: 2px; 20 | } 21 | &.btn-block { 22 | display: flex; 23 | width: 100%; 24 | } 25 | &:active { 26 | .active; 27 | } 28 | .icon { 29 | height: 1.2em; 30 | &:first-child { 31 | margin-right: 0.3em; 32 | } 33 | &:last-child { 34 | margin-left: 0.3em; 35 | } 36 | } 37 | &.btn-teal { 38 | background: @teal; 39 | color: @white; 40 | border-radius: 30px; 41 | box-shadow: 0 2px 0 0 darken(@teal, 20%); 42 | &:active { 43 | .active; 44 | } 45 | } 46 | &.btn-blue { 47 | background: @shadowBlue; 48 | color: @white; 49 | box-shadow: 0 2px 0 0 darken(@shadowBlue, 20%); 50 | &:active { 51 | .active; 52 | } 53 | } 54 | &.btn-rounded { 55 | background: #FDFDFE; 56 | border-radius: 40px; 57 | font-family: FiraSans-Medium; 58 | color: #71A5FF; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/card/card.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Link = require('../link/link.jsx'); 3 | 4 | var Card = React.createClass({ 5 | statics: { 6 | DEFAULT_THUMBNAIL: '../../img/default.svg' 7 | }, 8 | getDefaultProps: function(){ 9 | return { 10 | showAuthor: true, 11 | showActions: false 12 | }; 13 | }, 14 | actionsClicked: function (e) { 15 | e.preventDefault(); 16 | e.stopPropagation(); 17 | this.props.onActionsClick.call(this, this.props); 18 | }, 19 | onImageError: function() { 20 | var imageEl = this.refs.imageEl; 21 | imageEl.src = Card.DEFAULT_THUMBNAIL; 22 | imageEl.onerror = null; 23 | }, 24 | componentDidMount: function () { 25 | var imageEl = this.refs.imageEl; 26 | imageEl.onerror = this.onImageError; 27 | imageEl.src = this.props.thumbnail || Card.DEFAULT_THUMBNAIL; 28 | }, 29 | render: function () { 30 | function tagifyDescription (description) { 31 | // 32 | // ༼ つ ◕_◕ ༽つ <( HEY, YOU!!! ) 33 | // 34 | // If this regex changes, be sure to update the 35 | // corresponding regex in api.webmaker.org's tagging code! 36 | var hashFinder = /(#[A-Za-z\d]+)/g; 37 | 38 | var words = description.split(hashFinder); 39 | 40 | if (words.length) { 41 | words.forEach((word, index) => { 42 | if (word.length) 43 | if (word.match(hashFinder)) { 44 | words[index] = React.createElement(Link, {key: index, href: `/pages/tag-list`, url: `/tags/${word.slice(1)}`}, word); 45 | } else { 46 | words[index] = React.createElement(`span`, {key: index}, word); 47 | } 48 | }); 49 | } 50 | 51 | return words; 52 | } 53 | 54 | return ( 55 |
56 | 57 |
58 | 59 |
60 | 61 | 62 |
63 |
64 | 65 |
66 |
{this.props.title}
67 | {this.props.author.username} 68 |
69 | 74 |
75 | 78 |
79 | 80 |
81 | ); 82 | } 83 | }); 84 | 85 | module.exports = Card; 86 | -------------------------------------------------------------------------------- /src/components/card/card.less: -------------------------------------------------------------------------------- 1 | .card { 2 | display: block; 3 | position: relative; 4 | 5 | width: calc(100% - 10px); 6 | margin: 0 auto 20px auto; 7 | 8 | color: @slate; 9 | text-decoration: none; 10 | background-color: white; 11 | box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.3); 12 | 13 | .avatar{ 14 | margin-right: 10px; 15 | img{ 16 | display: block; 17 | } 18 | } 19 | 20 | .thumbnail { 21 | padding: 0; 22 | height: 0; 23 | padding-bottom: 61.875%; 24 | img { 25 | display: block; 26 | position: relative; 27 | 28 | width: 100%; 29 | margin: 0 auto; 30 | background: #efefef; 31 | } 32 | } 33 | 34 | .meta { 35 | width: 100%; 36 | margin: 0 auto; 37 | padding: 10px; 38 | font-size: 1.2rem; 39 | 40 | .wrapper { 41 | display: flex; 42 | align-items: center; 43 | align-content: center; 44 | } 45 | 46 | .description { 47 | margin-top: 10px; 48 | font-size: 15px; 49 | 50 | a { 51 | color: @blue; 52 | text-decoration: none; 53 | } 54 | } 55 | 56 | .text { 57 | flex: 1; 58 | word-wrap: break-word; 59 | padding: 5px 0 0 0; 60 | } 61 | 62 | .title { 63 | line-height: 1.2rem; 64 | } 65 | 66 | .action { 67 | button { 68 | margin: 5px 0 0 0; 69 | padding: 5px 5px 5px 20px; // ~40x40px tap size 70 | border: 0; 71 | background: none; 72 | line-height: 0; 73 | img { 74 | width: 5px; 75 | } 76 | } 77 | } 78 | } 79 | 80 | 81 | .author { 82 | color: @slate; 83 | opacity: 0.6; 84 | font-size: 1rem; 85 | text-decoration: none; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/color-group/color-group.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var LinkedStateMixin = require('react-addons-linked-state-mixin'); 3 | var classNames = require('classnames'); 4 | 5 | var ColorGroup = React.createClass({ 6 | statics: { 7 | defaultColors: ['transparent', '#FFF', '#99CA47', '#EFC246', '#E06A2C', '#69A0FC'] 8 | }, 9 | mixins: [LinkedStateMixin], 10 | getDefaultProps: function () { 11 | return { 12 | colors: this.defaultColors.slice(), 13 | id: 'value' 14 | }; 15 | }, 16 | getInitialState: function () { 17 | return { 18 | value: this.props.colors[0] 19 | }; 20 | }, 21 | getTinkerUrl: function () { 22 | var params = this.props.params; 23 | if (!params) { 24 | return; 25 | } 26 | return `/users/${params.user}/projects/${params.project}/pages/${params.page}/elements/${params.element}/propertyName/${this.props.id}`; 27 | }, 28 | onChange: function (e) { 29 | if (this.valueLink) { 30 | this.valueLink.requestChange(e.target.value); 31 | } 32 | if (typeof this.props.onChange === 'function') { 33 | this.props.onChange(e.target.value); 34 | } 35 | }, 36 | 37 | // Terrible hack to allow us to save before going to 38 | // tinker mode 39 | launchTinker: function (e) { 40 | 41 | // Call on change so that we update the border colour if needed 42 | if (typeof this.props.onChange === 'function') { 43 | this.props.onChange(this.valueLink.value); 44 | } 45 | 46 | if (!window.Platform) { 47 | return; 48 | } 49 | 50 | e.preventDefault(); 51 | 52 | var launch = () => { 53 | window.Platform.setView(this.getTinkerUrl()); 54 | }; 55 | 56 | if (this.props.onLaunchTinker) { 57 | this.props.onLaunchTinker(launch); 58 | } else { 59 | launch(); 60 | } 61 | }, 62 | 63 | render: function () { 64 | var colors = this.props.colors; 65 | var linkState = this.props.linkState || this.linkState; 66 | this.valueLink = linkState(this.props.id); 67 | 68 | // If current color is custom, add it to the list 69 | if (this.valueLink.value && colors.indexOf(this.valueLink.value) === -1) { 70 | colors = colors.concat([this.valueLink.value]); 71 | } 72 | 73 | return (
74 | 75 | {colors.map(color => { 76 | var className = { 77 | 'color-swatch': true, 78 | checked: this.valueLink.value === color 79 | }; 80 | var innerClassName = { 81 | 'color-swatch-inner': true, 82 | transparent: color === 'transparent', 83 | white: color === '#FFF' 84 | }; 85 | return (); 89 | })} 90 | 95 |
); 96 | } 97 | }); 98 | 99 | module.exports = ColorGroup; 100 | -------------------------------------------------------------------------------- /src/components/color-group/color-group.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Color 3 | */ 4 | .color-group { 5 | @_swatchSize: 35px; 6 | @_swatchMargin: 15px; 7 | @_selectedSize: 12px; 8 | 9 | display: flex; 10 | align-items: center; 11 | .color-swatch { 12 | margin: 0; 13 | display: block; 14 | padding: 0; 15 | width: @_swatchSize; 16 | margin-right: @_swatchMargin; 17 | border: none; 18 | position: relative; 19 | background: none; 20 | &.checked::after { 21 | content: ''; 22 | display: block; 23 | background: white; 24 | width: @_selectedSize; 25 | height: 0; 26 | padding-bottom: @_selectedSize; 27 | border-radius: 50%; 28 | position: absolute; 29 | top: 50%; 30 | left: 50%; 31 | margin-top: -(@_selectedSize/2); 32 | margin-left: -(@_selectedSize/2); 33 | box-shadow: 0 1px 1px 1px fade(#000, 10%); 34 | } 35 | .color-swatch-inner { 36 | cursor: pointer; 37 | display: block; 38 | position: relative; 39 | border-radius: 50%; 40 | width: 100%; 41 | height: 0; 42 | padding-bottom: 100%; 43 | background: white; 44 | box-shadow: inset 0 1px 1px 1px fade(#000, 10%); 45 | &.transparent, &.white { 46 | box-shadow: inset 0 0 1px 1px darken(#E9EBF0, 10%); 47 | } 48 | &.transparent::after { 49 | // This is the strikethrough 50 | content: ''; 51 | display: block; 52 | width: 100%; 53 | height: 2px; 54 | background-color: @brick; 55 | position: absolute; 56 | top: 50%; 57 | left: 0; 58 | transform: rotate(-45deg); 59 | } 60 | } 61 | } 62 | .tinker-container { 63 | flex-grow: 1; 64 | justify-content: flex-end; 65 | .tinker { 66 | width: @_swatchSize; 67 | display: block; 68 | img { 69 | width: 100%; 70 | display: block; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/color-spectrum/color-spectrum.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Spectrum color picker 3 | */ 4 | //Default height of the picker, but gets overridden in tinker page for its layout 5 | .spectrum-container { 6 | @_spectrum-pickerHeight: 140px; 7 | @_rangeWidth: 40px; 8 | @_margin: 20px; 9 | position: relative; 10 | display: flex; 11 | .spectrum-left { 12 | position: relative; 13 | flex-grow: 1; 14 | } 15 | .saturation { 16 | position: relative; 17 | width: 100%; 18 | height: @_spectrum-pickerHeight; 19 | } 20 | .saturation-white, 21 | .saturation-black { 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | width: 100%; 26 | height: 100%; 27 | } 28 | .saturation-white { 29 | background: linear-gradient(to right, #fff, rgba(204,154,129,0)); 30 | } 31 | .saturation-black { 32 | background: linear-gradient(to top, #000, rgba(204,154,129,0)); 33 | } 34 | .sl-marker { 35 | width: 20px; 36 | height: 20px; 37 | border-radius: 20px; 38 | border: 2px solid white; 39 | box-shadow: 0 0 0 2px fade(black, 10%); 40 | position: absolute; 41 | margin-left: -10px; 42 | margin-top: -10px; 43 | } 44 | .spectrum-right { 45 | position: relative; 46 | margin-left: @_margin; 47 | width: @_rangeWidth; 48 | background: linear-gradient(to bottom, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); 49 | cursor: pointer; 50 | } 51 | .hue { 52 | -webkit-appearance: none; 53 | transform: rotate(90deg); 54 | transform-origin: 0 0; 55 | position: absolute; 56 | top: @_margin; 57 | width: @_spectrum-pickerHeight; 58 | height: @_rangeWidth; 59 | margin-top: -(@_rangeWidth/2); 60 | z-index: 1; 61 | background: transparent; 62 | } 63 | .hue::-webkit-slider-thumb { 64 | -webkit-appearance: none; 65 | border: none; 66 | width: 1px; 67 | height: 46px; 68 | cursor: pointer; 69 | background: transparent; 70 | &::after { 71 | position: absolute; 72 | content: ''; 73 | width: 20px; 74 | height: 46px; 75 | margin-left: -10px; 76 | background: url(./img/slider.png) no-repeat; 77 | background-size: 20px 46px; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/d-pad/d-pad.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | propTypes: { 5 | onDirectionClick: React.PropTypes.func, // External handler for button clicks 6 | isVisible: React.PropTypes.bool 7 | }, 8 | getInitialState: function () { 9 | return { 10 | showUp: true, 11 | showDown: true, 12 | showLeft: true, 13 | showRight: true 14 | }; 15 | }, 16 | onButtonClick: function (direction, event) { 17 | this.props.onDirectionClick.call(this, direction); 18 | }, 19 | bulkSetVisibility: function (newState) { 20 | this.setState(newState); 21 | }, 22 | positionAroundContainer: function (boundingRect) { 23 | if (!boundingRect) { 24 | return; 25 | } 26 | 27 | this.refs.btnUp.style.bottom = boundingRect.bottom - 3 + 'px'; 28 | this.refs.btnDown.style.top = boundingRect.bottom - 3 + 'px'; 29 | this.refs.btnLeft.style.right = boundingRect.right - 3 + 'px'; 30 | this.refs.btnRight.style.left = boundingRect.right - 3 + 'px'; 31 | }, 32 | render: function () { 33 | return ( 34 |
35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/d-pad/d-pad.less: -------------------------------------------------------------------------------- 1 | .dPad { 2 | button.moon { 3 | @moon-width: 33px; 4 | 5 | border-bottom-left-radius: 50%; 6 | border-bottom-right-radius: 50%; 7 | background: rgba(109, 120, 135, 0.9) url(./img/nub-white.svg) no-repeat; 8 | background-position: center 1.225 * @moon-width; 9 | background-size: 0.525 * @moon-width; 10 | width: @moon-width * 2; 11 | height: @moon-width * 2; 12 | 13 | &.up { 14 | top: -@moon-width - 2; 15 | margin-left: -@moon-width; 16 | } 17 | 18 | &.down { 19 | bottom: -@moon-width - 2; 20 | margin-left: -@moon-width; 21 | } 22 | 23 | &.left { 24 | left: -@moon-width - 2; 25 | margin-top: -@moon-width; 26 | } 27 | 28 | &.right { 29 | right: -@moon-width - 2; 30 | margin-top: -@moon-width; 31 | } 32 | } 33 | 34 | button { 35 | @nub-width: 30px; 36 | 37 | position: absolute; 38 | display: block; 39 | z-index: 9999; 40 | background: url(./img/nub.svg) no-repeat center center / 100%; 41 | border: 0; 42 | width: @nub-width; 43 | height: @nub-width; 44 | 45 | &.up { 46 | left: 50%; 47 | margin-left: -(@nub-width / 2); 48 | } 49 | 50 | &.down { 51 | left: 50%; 52 | margin-left: -(@nub-width / 2); 53 | transform: rotate(180deg); 54 | } 55 | 56 | &.left { 57 | top: 50%; 58 | margin-top: -(@nub-width / 2); 59 | transform: rotate(-90deg); 60 | } 61 | 62 | &.right { 63 | top: 50%; 64 | margin-top: -(@nub-width / 2); 65 | transform: rotate(90deg); 66 | } 67 | 68 | } 69 | 70 | &.hidden, .hidden { 71 | display: none; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/element-group/element-group.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var BasicElement = require('../../components/basic-element/basic-element.jsx'); 3 | var assign = require('react/lib/Object.assign'); 4 | 5 | var ElementGroup = React.createClass({ 6 | getDefaultProps: function () { 7 | return { 8 | elements: {}, 9 | currentElementId: -1, 10 | interactive: false 11 | }; 12 | }, 13 | 14 | /** 15 | * Generate the JSX for the element 16 | * @param {id} elementId the element's id in the this.props-passed `elements` dictionary 17 | * @param {object} properties The element property sheet from which to build the JSX representation 18 | * @return {JSX} an element's JSX representation 19 | */ 20 | formElement: function(elementId, properties) { 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }, 28 | 29 | /** 30 | * Process an element's property sheet, transforming it into 31 | * a JSX object for use by React, 32 | * @param {id} elementId the element's id in the this.props-passed `elements` dictionary 33 | * @return {JSX} an element's JSX representation 34 | */ 35 | processProperties: function(elementId) { 36 | var properties = this.props.elements[elementId]; 37 | 38 | if (!properties || !properties.type) { 39 | return false; 40 | } 41 | 42 | properties = assign({}, properties, { 43 | isCurrent: +this.props.currentElementId === +elementId, 44 | interactive: this.props.interactive 45 | }); 46 | 47 | // Add callbacks for interactive mode 48 | if (this.props.onTouchEnd) { 49 | properties.onTouchEnd = this.props.onTouchEnd(elementId); 50 | } 51 | 52 | if (this.props.onTap) { 53 | properties.onTap = this.props.onTap(elementId); 54 | } 55 | 56 | if (this.props.onUpdate) { 57 | properties.onUpdate = this.props.onUpdate(elementId); 58 | } 59 | 60 | return this.formElement(elementId, properties); 61 | }, 62 | 63 | /** 64 | * Convert all passed element property sheets into JSX elemenets 65 | * @return {JSX[]} And array of JSX representations of each element in this.props.elemeents 66 | */ 67 | formElements: function () { 68 | return Object.keys(this.props.elements).map(this.processProperties); 69 | }, 70 | 71 | render: function () { 72 | return ( 73 |
74 |
75 | { this.formElements() } 76 |
77 | ); 78 | } 79 | }); 80 | 81 | module.exports = ElementGroup; 82 | -------------------------------------------------------------------------------- /src/components/element-group/element-group.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Positionables 3 | */ 4 | .element-group { 5 | position: relative; 6 | overflow: visible; 7 | height: 100%; 8 | width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/featured-tag-card/featured-tag-card.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Link = require('../link/link.jsx'); 3 | 4 | var FeaturedTagCard = React.createClass({ 5 | mixins: [ 6 | require('react-intl').IntlMixin 7 | ], 8 | render: function () { 9 | return ( 10 | 11 |
{this.getIntlMessage('featured')}
12 |
#{this.props.tag}
13 | 14 | ); 15 | } 16 | }); 17 | 18 | module.exports = FeaturedTagCard; 19 | -------------------------------------------------------------------------------- /src/components/featured-tag-card/featured-tag-card.less: -------------------------------------------------------------------------------- 1 | .featured-tag-card{ 2 | background-color: @teal; 3 | color: white; 4 | padding: 10px 10px 20px; 5 | 6 | .featured{ 7 | text-transform: uppercase; 8 | font-size: .75rem; 9 | } 10 | .tag{ 11 | text-align: center; 12 | font-size: 1.7rem; 13 | line-height: 1; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ftu/ftu.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var FTU = React.createClass({ 4 | mixins: [require('react-intl').IntlMixin], 5 | propTypes: { 6 | singleMessage: React.PropTypes.string, 7 | rightMessage: React.PropTypes.string, 8 | leftMessage: React.PropTypes.string 9 | }, 10 | getInitialState: function () { 11 | return { 12 | isVisible: false 13 | }; 14 | }, 15 | hide: function () { 16 | this.setState({ 17 | isVisible: false 18 | }); 19 | }, 20 | show: function () { 21 | this.setState({ 22 | isVisible: true 23 | }); 24 | }, 25 | onGotItClicked: function () { 26 | this.hide(); 27 | this.props.onViewed(); 28 | }, 29 | render: function () { 30 | return ( 31 |
32 |
33 | 34 |

{ this.props.singleMessage }

35 | 36 |
37 | 38 |
39 |
40 | 41 |

{ this.props.leftMessage }

42 |
43 |
44 | 45 |

{ this.props.rightMessage }

46 |
47 |
48 | 49 | 50 |
51 | ); 52 | } 53 | }); 54 | 55 | module.exports = FTU; 56 | -------------------------------------------------------------------------------- /src/components/ftu/ftu.less: -------------------------------------------------------------------------------- 1 | .ftu { 2 | background-image: linear-gradient(-180deg, rgba(76,136,239,0.94) 25%, rgba(76,136,239,0.50) 93%); 3 | color: @white; 4 | margin: 0; 5 | padding: 0; 6 | z-index: @ZINDEX_MAX; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | height: 100%; 12 | position: fixed; 13 | text-align: center; 14 | 15 | &.hidden { 16 | display: none; 17 | } 18 | 19 | .helpers { 20 | display: none; 21 | justify-content: center; 22 | font-family: "Sharpie", "Chalkboard", "Comic Sans", serif, sans-serif; 23 | padding: 10px; 24 | 25 | &.active { 26 | display: flex; 27 | } 28 | 29 | &.helpers-single { 30 | align-items: flex-start; 31 | 32 | .invisible { 33 | visibility: hidden; 34 | } 35 | 36 | p { 37 | padding-top: 10px; 38 | } 39 | } 40 | 41 | div { 42 | text-align: center; 43 | padding: 0 20px 10px 20px; 44 | flex: 1; 45 | 46 | &:nth-child(1) img { 47 | transform: scaleX(-1); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/get-media/get-media.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classNames = require('classnames'); 3 | 4 | var GetMedia = React.createClass({ 5 | propTypes: { 6 | // Show or hide the component 7 | show: React.PropTypes.bool, 8 | 9 | // Runs when either "Gallery" or "Camera" is clicked 10 | onStartMedia: React.PropTypes.func, 11 | 12 | // Runs if window.Platform is not available (i.e. in a browser) 13 | onNoCamera: React.PropTypes.func, 14 | 15 | // Runs run an image URI is ready 16 | onImageReady: React.PropTypes.func, 17 | 18 | // Runs if the user exits out of the 19 | // camera or the menu 20 | onCancel: React.PropTypes.func 21 | }, 22 | componentDidMount: function () { 23 | // Expose image handler to Android 24 | window.imageReady = this.imageReady; 25 | window.onMediaCancel = this.onMediaCancel; 26 | }, 27 | imageReady: function (uri) { 28 | if (this.props.onImageReady) { 29 | this.props.onImageReady(uri); 30 | } 31 | }, 32 | onMediaCancel: function () { 33 | if (this.props.onCancel) { 34 | this.props.onCancel(); 35 | } 36 | }, 37 | openMedia: function (type) { 38 | if (window.Platform) { 39 | if (type === 'gallery') { 40 | window.Platform.getFromMedia(); 41 | } else { 42 | window.Platform.getFromCamera(); 43 | } 44 | } else if (this.props.onNoCamera) { 45 | this.props.onNoCamera(); 46 | } else if (this.props.onCancel) { 47 | this.props.onCancel(); 48 | } 49 | }, 50 | onCameraClick: function () { 51 | if (this.props.onStartMedia) this.props.onStartMedia(); 52 | this.openMedia('camera'); 53 | }, 54 | onGalleryClick: function () { 55 | if (this.props.onStartMedia) this.props.onStartMedia(); 56 | this.openMedia('gallery'); 57 | }, 58 | render: function () { 59 | return (
60 |
61 |
62 | 66 | 70 |
71 |
); 72 | } 73 | }); 74 | 75 | module.exports = GetMedia; 76 | -------------------------------------------------------------------------------- /src/components/link/link.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var assign = require('react/lib/Object.assign'); 3 | 4 | var Link = React.createClass({ 5 | getDefaultProps: function () { 6 | return { 7 | tagName: 'a' 8 | }; 9 | }, 10 | render: function () { 11 | var className = this.props.className ? (this.props.className + ' link') : 'link'; 12 | var props = assign({}, this.props, { 13 | className, 14 | onClick: (e) => { 15 | if (window.Platform) { 16 | e.preventDefault(); 17 | 18 | // if there is a pre-navigation handling hook, call that before continuing 19 | if (this.props.preNavigation && typeof this.props.preNavigation === 'function') { 20 | this.props.preNavigation(); 21 | } 22 | 23 | if (this.props.external) { 24 | window.Platform.openExternalUrl(this.props.external); 25 | } else if (this.props.url) { 26 | window.Platform.setView(this.props.url); 27 | } 28 | } 29 | } 30 | }); 31 | if (this.props.external) { 32 | props.target = '_blank'; 33 | props.href = this.props.href || this.props.external; 34 | } 35 | return React.createElement(this.props.tagName, props); 36 | } 37 | }); 38 | 39 | module.exports = Link; 40 | -------------------------------------------------------------------------------- /src/components/link/link.less: -------------------------------------------------------------------------------- 1 | .link { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/loading/loading.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classNames = require('classnames'); 3 | 4 | var Loading = React.createClass({ 5 | mixins: [require('react-intl').IntlMixin], 6 | getDefaultProps: function () { 7 | return { 8 | on: false 9 | }; 10 | }, 11 | render: function () { 12 | return (
13 |
14 |
15 |
16 |
17 |
{this.getIntlMessage('loading')}
18 |
19 |
); 20 | } 21 | }); 22 | 23 | module.exports = Loading; 24 | -------------------------------------------------------------------------------- /src/components/loading/loading.less: -------------------------------------------------------------------------------- 1 | .loading { 2 | position: fixed; 3 | z-index: @ZINDEX_LOADING; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background: transparent; 9 | opacity: 0; 10 | visibility: hidden; 11 | transition: opacity 0.2s ease; 12 | &.on { 13 | opacity: 1; 14 | visibility: visible; 15 | } 16 | } 17 | 18 | .loading-bar { 19 | position: relative; 20 | width: 100%; 21 | height: 6px; 22 | background-color: lighten(@blue, 20%); 23 | border-bottom: 1px solid fade(@white, 10%); 24 | } 25 | .bar { 26 | content: ""; 27 | display: inline; 28 | position: absolute; 29 | width: 0; 30 | height: 100%; 31 | left: 50%; 32 | text-align: center; 33 | } 34 | .bar:nth-child(1) { 35 | background-color: lighten(@blue, 10%); 36 | animation: loading 3s linear infinite; 37 | } 38 | .bar:nth-child(2) { 39 | background-color: lighten(@blue, 5%); 40 | animation: loading 3s linear 1s infinite; 41 | } 42 | .bar:nth-child(3) { 43 | background-color: @blue; 44 | animation: loading 3s linear 2s infinite; 45 | } 46 | 47 | @keyframes loading { 48 | from { 49 | left: 50%; 50 | width: 0; 51 | z-index: 100; 52 | } 53 | 33.3333% { 54 | left: 0; 55 | width: 100%; 56 | z-index: 10; 57 | } 58 | to { 59 | left: 0; 60 | width: 100%; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/modal-confirm/modal-confirm.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Shim = require('../shim/shim.jsx'); 3 | var dispatcher = require('../../lib/dispatcher'); 4 | 5 | var ModalConfirm = React.createClass({ 6 | getInitialState: function () { 7 | return { 8 | header: '', 9 | body: '', 10 | attribution: undefined, 11 | icon: '', 12 | buttons: [], 13 | callback: null 14 | }; 15 | }, 16 | show: function () { 17 | this.refs.shim.show(); 18 | }, 19 | hide: function () { 20 | this.refs.shim.hide(); 21 | }, 22 | onConfirmClick: function (button) { 23 | this.hide(); 24 | 25 | if (button.callback) { 26 | button.callback.call(); 27 | } 28 | }, 29 | componentDidMount: function () { 30 | dispatcher.on('modal-confirm:show', (event) => { 31 | if (event.config.icon) { 32 | event.config.icon = '../../img/' + event.config.icon; 33 | } 34 | 35 | this.setState(React.__spread(this.getInitialState(), event.config)); 36 | this.show(); 37 | }); 38 | 39 | dispatcher.on('modal-confirm:hide', (event) => { 40 | this.hide(); 41 | }); 42 | }, 43 | render: function () { 44 | var buttons = this.state.buttons.map((button, i) => { 45 | return ( 46 | ); 52 | }); 53 | 54 | return ( 55 | 56 |
57 |
58 |
{this.state.header}
59 |
60 | 61 |
62 |
63 |
64 |

{this.state.body}

65 | {buttons} 66 | 70 |
71 |
72 |
73 | ); 74 | } 75 | }); 76 | 77 | module.exports = ModalConfirm; 78 | -------------------------------------------------------------------------------- /src/components/modal-confirm/modal-confirm.less: -------------------------------------------------------------------------------- 1 | .modal-confirm { 2 | .window { 3 | box-shadow: 0px 0px 30px rgba(0,0,0,0.3); 4 | border-radius: 5px; 5 | 6 | header { 7 | padding: 5px 20px 0 20px; 8 | background: @shadowPlum; 9 | border-top-left-radius: 5px; 10 | border-top-right-radius: 5px; 11 | display: flex; 12 | align-items: center; 13 | 14 | div { 15 | font-size: 18px; 16 | font-weight: 400; 17 | color: @white; 18 | padding: 5px 0; 19 | } 20 | 21 | .text { 22 | flex: 1; 23 | } 24 | 25 | .icon { 26 | img { 27 | width: 40px; 28 | } 29 | } 30 | } 31 | 32 | .content { 33 | background: @white; 34 | padding: 20px; 35 | border-bottom-right-radius: 5px; 36 | border-bottom-left-radius: 5px; 37 | 38 | p { 39 | font-size: 16px; 40 | margin-top: 0; 41 | color: #333; 42 | } 43 | 44 | button { 45 | background: @lightGrey; 46 | } 47 | 48 | .attribution { 49 | img { 50 | width: 18px; 51 | margin-right: 5px; 52 | opacity: 0.3; 53 | position: relative; 54 | top: 3px; 55 | } 56 | color: @heatherGrey; 57 | text-align: center; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/modal-switch/modal-switch.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Shim = require('../shim/shim.jsx'); 3 | var dispatcher = require('../../lib/dispatcher'); 4 | 5 | var ModalConfirm = React.createClass({ 6 | getInitialState: function () { 7 | return { 8 | actions: [], 9 | callback: null 10 | }; 11 | }, 12 | show: function () { 13 | this.refs.shim.show(); 14 | }, 15 | hide: function () { 16 | this.refs.shim.hide(); 17 | }, 18 | onButtonClick: function (event) { 19 | if (this.state.callback) { 20 | this.state.callback.call(this, { 21 | label: event 22 | }); 23 | } 24 | 25 | this.hide(); 26 | }, 27 | componentDidMount: function () { 28 | dispatcher.on('modal-switch:show', (event) => { 29 | this.setState(React.__spread(this.getInitialState(), event.config)); 30 | this.show(); 31 | }); 32 | 33 | dispatcher.on('modal-switch:hide', (event) => { 34 | this.hide(); 35 | }); 36 | }, 37 | render: function () { 38 | var buttons = this.state.actions.map((action, index) => { 39 | return ( 40 | 41 | ); 42 | }); 43 | 44 | return ( 45 | 46 |
47 | {buttons} 48 |
49 |
50 | ); 51 | } 52 | }); 53 | 54 | module.exports = ModalConfirm; 55 | -------------------------------------------------------------------------------- /src/components/modal-switch/modal-switch.less: -------------------------------------------------------------------------------- 1 | .modal-switch { 2 | .window { 3 | box-shadow: 0px 0px 30px rgba(0,0,0,0.3); 4 | border-radius: 5px; 5 | background: @white; 6 | border-radius: 5px; 7 | width: 100%; 8 | 9 | button { 10 | display: block; 11 | background: none; 12 | width: 100%; 13 | border: 0; 14 | border-bottom: 1px solid @heatherGrey; 15 | text-align: left; 16 | line-height: 22px; 17 | padding: 20px; 18 | color: darken(@slate, 20%); 19 | 20 | &:last-child { 21 | border-bottom: 0; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/option-panel/option-panel.less: -------------------------------------------------------------------------------- 1 | .option-panel { 2 | display: flex; 3 | .label { 4 | margin: 0; 5 | position: relative; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | flex: 1; 10 | background: #FFF; 11 | height: 44px; 12 | box-shadow: 0 2px 0 0 #CDCFD3; 13 | border-left: 1px solid fade(#CDCFD3, 50%); 14 | &.selected { 15 | background: #69A0FC; 16 | box-shadow: 0 2px 0 0 #4881DF; 17 | border-left: 1px solid fade(#4881DF, 50%); 18 | } 19 | &:first-child { 20 | border-radius: 3px 0 0 3px; 21 | } 22 | &:last-child { 23 | border-radius: 0 3px 3px 0; 24 | } 25 | .icon { 26 | height: 15px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/project-list/project-list.less: -------------------------------------------------------------------------------- 1 | .projectList { 2 | display: block; 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | margin: 20px auto 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/range/range.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var LinkedStateMixin = require('react-addons-linked-state-mixin'); 3 | 4 | module.exports = React.createClass({ 5 | mixins: [LinkedStateMixin], 6 | getDefaultProps: function () { 7 | return { 8 | id: 'value', 9 | max: 100, 10 | min: 0, 11 | step: 1, 12 | unit: '', 13 | percentage: false 14 | }; 15 | }, 16 | getInitialState: function () { 17 | return { 18 | value: typeof this.props.value !== 'undefined' ? this.props.value : 100 19 | }; 20 | }, 21 | onChange: function (e) { 22 | var value = parseFloat(e.target.value); 23 | this.valueLink.requestChange(value); 24 | if (this.props.onChange) { 25 | this.props.onChange(value); 26 | } 27 | }, 28 | getDisplayValue: function(){ 29 | if (this.props.percentage) { 30 | return `${Math.round(this.props.value/this.props.max * 100)}%`; 31 | } else { 32 | return `${this.props.value}${this.props.unit}`; 33 | } 34 | }, 35 | render: function () { 36 | var linkState = this.props.linkState || this.linkState; 37 | var valueLink = this.valueLink = linkState(this.props.id); 38 | 39 | var currentValue = parseFloat(valueLink.value); 40 | 41 | return ( 42 |
43 | 44 |
{this.getDisplayValue()}
45 |
46 | ); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/range/range.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Range 3 | */ 4 | @range_height: 2px; 5 | @range_vertical-margin: 20px; 6 | @range_total-height: @range_height + @range_vertical-margin*2; 7 | .range { 8 | display: flex; 9 | align-items: center; 10 | input[type=range] { 11 | -webkit-appearance: none; 12 | padding: 0; 13 | height: @range_height; 14 | background: @heatherGrey; 15 | margin: @range_vertical-margin 0; 16 | &::before { 17 | content: ''; 18 | } 19 | } 20 | 21 | input[type=range]::-webkit-slider-thumb { 22 | -webkit-appearance: none; 23 | border: none; 24 | height: 30px; 25 | width: 30px; 26 | border-radius: 36px; 27 | background: #FFF; 28 | cursor: pointer; 29 | // Generally, it's a good idea to avoid transparency 30 | // in moving parts where possible to improve animation performance 31 | box-shadow: 0px 1px 0 1px darken(@lightGrey, 4%); 32 | } 33 | 34 | .range-summary { 35 | width: 3.5em; 36 | text-align: right; 37 | &.min { 38 | opacity: 0.3; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/select/select.less: -------------------------------------------------------------------------------- 1 | .select { 2 | -webkit-appearance: none; 3 | -moz-appearance: none; 4 | appearance: none; 5 | &:extend(.btn); 6 | &:active { 7 | &:extend(.btn:active); 8 | } 9 | width: 100%; 10 | background: center right 12px #FFF url(./img/caret-down.svg) no-repeat; 11 | background-size: 11px 7px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/shim/shim.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Shim = React.createClass({ 4 | getInitialState: function () { 5 | return { 6 | isActive: false 7 | }; 8 | }, 9 | show: function () { 10 | this.setState({ 11 | isActive: true 12 | }); 13 | }, 14 | hide: function () { 15 | this.setState({ 16 | isActive: false 17 | }); 18 | }, 19 | onShimClick: function () { 20 | if (this.props.onClick) { 21 | this.props.onClick.call(); 22 | } 23 | }, 24 | render: function () { 25 | return ( 26 | 29 | ); 30 | } 31 | }); 32 | 33 | module.exports = Shim; 34 | -------------------------------------------------------------------------------- /src/components/shim/shim.less: -------------------------------------------------------------------------------- 1 | .shim { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: @overlay; 8 | z-index: @ZINDEX_MAX; 9 | display: flex; 10 | padding: 30px; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/snackbar/snackbar.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var dispatcher = require('../../lib/dispatcher'); 3 | 4 | var Snackbar = React.createClass({ 5 | getInitialState: function() { 6 | return { 7 | message: this.props.message || "", 8 | hidden: true 9 | }; 10 | }, 11 | 12 | componentDidMount: function () { 13 | dispatcher.on('reportError', (event) => { 14 | this.setState({ 15 | message: event.message, 16 | additionals: event.additionals, 17 | hidden: false 18 | },function() { 19 | // TODO: it may be possible to rewrite this so that it 20 | // hooks into a CSS transition end, although the 21 | // transition would be a little wonky 22 | setTimeout(_ => this.hide(), 3000); 23 | }); 24 | }); 25 | }, 26 | 27 | render: function () { 28 | var className = "snackbar"; 29 | if (this.state.hidden) { 30 | className = "hidden " + className; 31 | } 32 | return (
{ this.state.message }
); 33 | }, 34 | 35 | /** 36 | * Hide the snackbar from view 37 | */ 38 | hide: function() { 39 | this.setState({ 40 | hidden: true 41 | }); 42 | } 43 | }); 44 | 45 | module.exports = Snackbar; 46 | -------------------------------------------------------------------------------- /src/components/snackbar/snackbar.less: -------------------------------------------------------------------------------- 1 | .container div.snackbar { 2 | @barheight: 5em; 3 | width: 100%; 4 | position: fixed; 5 | z-index: 2147483647; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | background: #444; 10 | line-height: @barheight; 11 | text-align: center; 12 | color: #EEE; 13 | height: @barheight; 14 | min-height: 0; 15 | transition: height 0.25s linear; 16 | 17 | &.hidden { 18 | height: 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/spindicator/spindicator.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var dispatcher = require('../../lib/dispatcher'); 3 | 4 | var Spindicator = React.createClass({ 5 | getInitialState: function () { 6 | return { 7 | isVisible: false 8 | }; 9 | }, 10 | show: function () { 11 | this.setState({ 12 | isVisible: true 13 | }); 14 | }, 15 | hide: function () { 16 | this.setState({ 17 | isVisible: false 18 | }); 19 | }, 20 | componentDidMount: function () { 21 | dispatcher.on('apiLagging', (event) => { 22 | this.show(); 23 | }); 24 | 25 | dispatcher.on('apiCallFinished', (event) => { 26 | this.hide(); 27 | }); 28 | }, 29 | render: function () { 30 | return ( 31 | 34 | ); 35 | } 36 | }); 37 | 38 | module.exports = Spindicator; 39 | -------------------------------------------------------------------------------- /src/components/spindicator/spindicator.less: -------------------------------------------------------------------------------- 1 | #spindicator { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: rgba(24, 22, 30, 0.75); 8 | transition: all 0.3s cubic-bezier(0.79, 0.14, 0.15, 0.86) 0s; 9 | z-index: @ZINDEX_MAX; 10 | 11 | .loader { 12 | position: relative; 13 | top: 43%; 14 | left: 41%; 15 | display: inline-block; 16 | width: 80px; 17 | height: 80px; 18 | border: 5px solid white; 19 | border-radius: 50%; 20 | animation: spin 0.5s infinite linear; 21 | } 22 | 23 | .loader::before, 24 | .loader::after { 25 | left: -5px; 26 | top: -5px; 27 | display: none; 28 | position: absolute; 29 | content: ''; 30 | width: inherit; 31 | height: inherit; 32 | border: inherit; 33 | border-radius: inherit; 34 | } 35 | 36 | 37 | .spinner { 38 | border-color: transparent; 39 | border-top-color: rgba(0,0,0,0.3); 40 | animation-duration: 3s; 41 | } 42 | 43 | .spinner::after { 44 | display: block; 45 | border-color: transparent; 46 | border-top-color: #E9EBF0; 47 | animation: spin 1.5s infinite ease-in-out; 48 | } 49 | 50 | @keyframes spin { 51 | 0% { 52 | transform: rotate(0deg); 53 | } 54 | 100% { 55 | transform: rotate(360deg); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/tabs/tabs.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classNames = require('classnames'); 3 | 4 | var Tabs = React.createClass({ 5 | getInitialState: function () { 6 | return { 7 | selectedIndex: 0 8 | }; 9 | }, 10 | setTab: function (i) { 11 | return () => { 12 | this.setState({selectedIndex: i}); 13 | }; 14 | }, 15 | render: function () { 16 | var className = classNames('tabs', this.props.className); 17 | var tabs = this.props.tabs; 18 | return (
19 |
    20 | {tabs.map((tab, i) => { 21 | var className = classNames({ 22 | 'tab-btn': true, 23 | 'selected': this.state.selectedIndex === i 24 | }); 25 | return (
  • ); 26 | })} 27 |
28 |
29 | {tabs.map((tab, i) => { 30 | var className = classNames({ 31 | 'tab-body': true, 32 | 'selected': this.state.selectedIndex === i 33 | }); 34 | return (
{tab.body}
); 35 | })} 36 |
37 |
); 38 | } 39 | }); 40 | 41 | module.exports = Tabs; 42 | -------------------------------------------------------------------------------- /src/components/tabs/tabs.less: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | * Tabs 3 | */ 4 | @tabs_iconHeight: 24px; 5 | @tabs_tab-btn-padding: 10px; 6 | @tabs_tab-menu-bottom-margin: 20px; 7 | @tabs_total-height: @tabs_iconHeight + @tabs_tab-btn-padding * 2 + @tabs_tab-menu-bottom-margin; 8 | .tabs { 9 | .tab-menu { 10 | margin: 0 0 @tabs_tab-menu-bottom-margin -20px; 11 | padding: 0; 12 | list-style: none; 13 | display: flex; 14 | background: #31344f; 15 | position: fixed; 16 | width: 100%; 17 | li { 18 | margin: 0; 19 | padding: 0; 20 | flex: 1; 21 | text-align: center; 22 | box-shadow: inset -2px 0 0 @plum; 23 | &:last-child { 24 | box-shadow: none; 25 | } 26 | } 27 | .tab-btn { 28 | background: none; 29 | border: none; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | padding: @tabs_tab-btn-padding; 34 | width: 100%; 35 | box-shadow: 0 2px 0 #404467; 36 | &.selected { 37 | box-shadow: 0 2px 0 @white; 38 | .icon { 39 | opacity: 1; 40 | } 41 | } 42 | .icon { 43 | opacity: 0.3; 44 | display: block; 45 | height: @tabs_iconHeight; 46 | } 47 | } 48 | } 49 | .tab-body { 50 | display: none; 51 | &.selected { 52 | display: block; 53 | } 54 | .editor-scroll{ 55 | padding-top: 0; 56 | padding-bottom: 50px; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/text-input/text-input.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var LinkedStateMixin = require('react-addons-linked-state-mixin'); 3 | 4 | var TextInput = React.createClass({ 5 | mixins: [LinkedStateMixin], 6 | propTypes: { 7 | maxlength: React.PropTypes.number.isRequired, 8 | minlength: React.PropTypes.number, 9 | onEnterPressed: React.PropTypes.func, 10 | placeholder: React.PropTypes.string, 11 | type: React.PropTypes.string 12 | }, 13 | getInitialState: function () { 14 | return { 15 | text: '', 16 | isTooLong: false 17 | }; 18 | }, 19 | onChange: function (e) { 20 | this.valueLink.requestChange(e.target.innerHTML); 21 | 22 | this.setState({ 23 | inputLength: e.target.innerText.length, 24 | isTooLong: e.target.innerText.length > this.props.maxlength ? true : false 25 | }); 26 | }, 27 | validate: function () { 28 | if ((!this.props.minlength || this.valueLink.value.length > this.props.minlength) && 29 | this.valueLink.value.length <= this.props.maxlength) { 30 | return true; 31 | } else { 32 | return false; 33 | } 34 | }, 35 | onKeyDown: function (event) { 36 | // Fire callback (if defined) if enter is pressed 37 | if (event.keyCode === 13) { 38 | // Prevent newlines 39 | event.preventDefault(); 40 | 41 | if (this.props.onEnterPressed) { 42 | this.props.onEnterPressed.call(this, { 43 | value: event.target.value 44 | }); 45 | } 46 | } 47 | }, 48 | blur: function () { 49 | this.refs.input.blur(); 50 | }, 51 | render: function () { 52 | var linkState = this.props.linkState || this.linkState; 53 | this.valueLink = linkState(this.props.id); 54 | 55 | return ( 56 |
57 | 58 | 59 |
66 | 67 |
{this.valueLink.value.length} / {this.props.maxlength}
68 |
69 | ); 70 | } 71 | }); 72 | 73 | module.exports = TextInput; 74 | -------------------------------------------------------------------------------- /src/components/text-input/text-input.less: -------------------------------------------------------------------------------- 1 | .text-input { 2 | label { 3 | font-size: 16px; 4 | color: @slate; 5 | margin-bottom: 1/3em; 6 | display: block; 7 | } 8 | 9 | .input { 10 | display: block; 11 | border: 0; 12 | border-bottom: 2px solid @heatherGrey; 13 | font-size: 22px; 14 | width: 100%; 15 | background: none; 16 | padding: 0.2em 0; 17 | -webkit-text-fill-color: @black; 18 | color: @blue; // Caret color 19 | margin-bottom: 1/3em; 20 | 21 | &:focus { 22 | outline: 0; 23 | border-bottom: 2px solid @blue; 24 | } 25 | 26 | &::-webkit-input-placeholder { 27 | -webkit-text-fill-color: darken(@heatherGrey, 10%); 28 | } 29 | } 30 | 31 | .indicator { 32 | text-align: right; 33 | font-size: 14px; 34 | color: @slate; 35 | } 36 | 37 | &.maxed { 38 | .indicator { 39 | color: @brick; 40 | } 41 | 42 | input { 43 | border-bottom: 2px solid @brick; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/fonts.less: -------------------------------------------------------------------------------- 1 | @fonts-path: './fonts'; 2 | 3 | @font-face { 4 | font-family: 'Fira Sans'; 5 | src: local('Fira Sans Regular'), 6 | url('@{fonts-path}/FiraSans-Regular.woff') format('woff'); 7 | font-weight: 400; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Fira Sans'; 13 | src: local('Fira Sans Medium'), 14 | url('@{fonts-path}/FiraSans-Medium.woff') format('woff'); 15 | font-weight: 500; 16 | font-style: normal; 17 | } 18 | 19 | @font-face { 20 | font-family: 'Sharpie'; 21 | src: local('Sharpie'), 22 | url('@{fonts-path}/lamson-sharpie-webfont.woff') format('woff'); 23 | font-weight: 400; 24 | font-style: normal; 25 | } 26 | -------------------------------------------------------------------------------- /src/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Webmaker 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/color.js: -------------------------------------------------------------------------------- 1 | var Color = require('color'); 2 | var minimumContrast = 2; 3 | 4 | module.exports = { 5 | getContrastingColor: (function() { 6 | var white = Color().rgb(255, 255, 255), 7 | whiteCSS = white.rgbString(), 8 | black = Color().rgb(0, 0, 0), 9 | blackCSS = black.rgbString(); 10 | 11 | return function(input) { 12 | var c = Color(input), 13 | wc = white.contrast(c), 14 | bc = black.contrast(c); 15 | 16 | return (wc < bc && wc >= minimumContrast) ? whiteCSS : blackCSS; 17 | }; 18 | }()), 19 | darken: function(baseColor, shade){ 20 | var color = Color(baseColor); 21 | color.darken(shade); 22 | 23 | return color.rgbString(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fire: function (eventName, eventData) { 3 | var customEvent = new CustomEvent(eventName, { 4 | detail: eventData 5 | }); 6 | 7 | window.dispatchEvent(customEvent); 8 | }, 9 | on: function (eventName, callback) { 10 | window.addEventListener(eventName, function(e) { 11 | callback.call(this, e.detail); 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/errors.js: -------------------------------------------------------------------------------- 1 | var dispatcher = require('./dispatcher'); 2 | var debug = false; 3 | 4 | var ErrorReporter = function() { 5 | var array = Array.prototype.slice.call(arguments); 6 | if (debug) { 7 | console.log.apply(console, arguments); 8 | } 9 | dispatcher.fire('reportError', { 10 | message: array[0], 11 | additionals: array.slice(1) 12 | }); 13 | }; 14 | 15 | module.exports = ErrorReporter; 16 | -------------------------------------------------------------------------------- /src/lib/i18n.js: -------------------------------------------------------------------------------- 1 | var assign = require('react/lib/Object.assign'); 2 | var messages = require('./messages'); 3 | var locale = navigator.language.split('-'); 4 | locale = locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : navigator.language; 5 | 6 | var strings = messages[locale] ? messages[locale] : messages['en-US']; 7 | 8 | module.exports = { 9 | intlData: { 10 | locales : ['en-US'], 11 | // Sometimes we will include a language with partial translation 12 | // and we need to make sure the object that we pass to `intlData` 13 | // contains all keys based on the `en-US` messages. 14 | messages: assign(messages['en-US'], strings) 15 | }, 16 | defaultLang: 'en-US', 17 | currentLanguage: locale, 18 | isSupportedLanguage: function(lang) { 19 | return !!messages[lang]; 20 | }, 21 | intlDataFor: function(lang) { 22 | // we need to make sure we transform the given locale to the right format first 23 | // so we can access the right locale in our dictionary for example: pt-br should be transformed to pt-BR 24 | var locale = lang.split('-'); 25 | locale = locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang; 26 | var strings = messages[locale] ? messages[locale] : messages['en-US']; 27 | 28 | return {locales: [locale], messages: assign(messages['en-US'], strings)}; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/jsonUtils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jsonToFormEncoded: function (json) { 3 | if (!json) { 4 | return; 5 | } 6 | return Object.keys(json).map(key => { 7 | return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]); 8 | }).join('&'); 9 | }, 10 | parseJSON: function (string) { 11 | var result = {}; 12 | if (string && typeof string === 'object') { 13 | return string; 14 | } 15 | if (string && typeof string === 'string') { 16 | try { result = JSON.parse(string); } catch (e) { console.error(e); } 17 | } 18 | return result; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/keyboard.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | focusNextInputByTabIndex: function () { 3 | var current = document.activeElement; 4 | var elements = document.getElementsByTagName('input'); 5 | var idx = parseInt(current.getAttribute('tabIndex'), 10) + 1; 6 | 7 | for (var i = 0; i < elements.length; i++) { 8 | var t = parseInt(elements[i].getAttribute('tabIndex'), 10); 9 | if (t === idx) { 10 | elements[i].focus(); 11 | } 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/messages.js: -------------------------------------------------------------------------------- 1 | // This is essentially bulk require 2 | var req = require.context('../locales', true, /\.json.*$/); 3 | var exports = {}; 4 | 5 | req.keys().forEach(function (file) { 6 | var locale = file.replace('./', '').replace('.json', ''); 7 | exports[locale] = req(file); 8 | }); 9 | 10 | module.exports = exports; 11 | -------------------------------------------------------------------------------- /src/lib/render.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactDOM = require('react-dom'); 3 | 4 | var Spindicator = require('../components/spindicator/spindicator.jsx'); 5 | var ModalConfirm = require('../components/modal-confirm/modal-confirm.jsx'); 6 | var ModalSwitch = require('../components/modal-switch/modal-switch.jsx'); 7 | var Snackbar = require('../components/snackbar/snackbar.jsx'); 8 | var FTU = require('../components/ftu/ftu.jsx'); 9 | 10 | var platform = require('./platform'); 11 | var intlData = require('./i18n').intlData; 12 | var dispatcher = require('./dispatcher'); 13 | 14 | var Base = React.createClass({ 15 | mixins: [ 16 | require('./router') 17 | ], 18 | componentDidMount: function () { 19 | var showFTU = (message) => { 20 | this.setState({ 21 | ftuSingleMessage: message, 22 | ftuRightMessage: null, 23 | ftuLeftMessage: null 24 | }); 25 | 26 | this.refs.ftu.show(); 27 | }; 28 | 29 | dispatcher.on('secondPageCreated', () => { 30 | showFTU(intlData.messages['ftu-share-project']); 31 | }); 32 | 33 | if (this.props.route.displayName === 'Project' && this.state.params.mode === 'play') { 34 | var projectsViewed = parseInt(platform.getSharedPreferences('ftu::projects-viewed', true) || 0, 10); 35 | 36 | projectsViewed++; 37 | 38 | // Trigger FTU 39 | if (projectsViewed === 2) { 40 | showFTU(intlData.messages['ftu-remix']); 41 | } 42 | 43 | platform.setSharedPreferences('ftu::projects-viewed', projectsViewed, true); 44 | } 45 | 46 | if( this.props.route.displayName === 'Discover' && !platform.getSharedPreferences('ftu::seen-primary-ftu', true) ) { 47 | this.refs.ftu.show(); 48 | } 49 | }, 50 | onResume: function () { 51 | this.setState({isVisible: true}); 52 | }, 53 | onPause: function () { 54 | this.setState({isVisible: false}); 55 | }, 56 | onBackPressed: function() { 57 | if (this.state.onBackPressed) { 58 | return this.state.onBackPressed(); 59 | } else { 60 | platform.goBack(); 61 | } 62 | }, 63 | jsComm: function (event) { 64 | if (this[event]) { 65 | this[event](); 66 | } 67 | }, 68 | getInitialState: function () { 69 | // Expose to android 70 | window.jsComm = this.jsComm; 71 | return { 72 | isVisible: true, 73 | onBackPressed: false, 74 | ftuSingleMessage: null, 75 | ftuRightMessage: intlData.messages['ftu-make-your-own'], 76 | ftuLeftMessage: intlData.messages['ftu-explore'] 77 | }; 78 | }, 79 | onFTUViewed: function () { 80 | platform.setSharedPreferences('ftu::seen-primary-ftu', 'true', true); 81 | }, 82 | update: function (state) { 83 | this.setState(state); 84 | }, 85 | render: function () { 86 | var Route = this.props.route; 87 | return (
88 | 94 | 95 | 96 | 97 | 98 | 99 |
); 100 | } 101 | }); 102 | 103 | module.exports = function (Route) { 104 | ReactDOM.render(, document.getElementById('app')); 105 | }; 106 | -------------------------------------------------------------------------------- /src/lib/router.js: -------------------------------------------------------------------------------- 1 | var {parseJSON} = require('./jsonUtils'); 2 | 3 | module.exports = { 4 | 5 | android: window.Platform, 6 | 7 | getRouteParams: function () { 8 | var params = {}; 9 | 10 | if (this.android) { 11 | // Check to see if route params exist & create cache key 12 | var routeParams = this.android.getRouteParams(); 13 | var key = 'route::params'; 14 | 15 | // If they do, save them. If not, fetch from storage 16 | if (routeParams !== '{}') { 17 | params = parseJSON(routeParams); 18 | this.android.setMemStorage(key, routeParams); 19 | } else { 20 | var hit = this.android.getMemStorage(key); 21 | params = parseJSON(hit); 22 | } 23 | } else { 24 | params = { 25 | mode: 'play', // 'edit', 'play', 'link' 26 | user: 1, 27 | project: 1, 28 | page: 1, 29 | element: 1, 30 | tag: 'test', 31 | propertyName: 'borderColor' 32 | }; 33 | } 34 | 35 | return params; 36 | }, 37 | 38 | getRouteData: function () { 39 | var data = {}; 40 | 41 | if (this.android) { 42 | data = parseJSON(this.android.getRouteData()); 43 | this.android.clearRouteData(); 44 | } 45 | 46 | return data; 47 | }, 48 | 49 | // #getUserSession () 50 | // 51 | // Returns user session from android if it exists, 52 | // or else an empty object. 53 | // if window.Platform doesn't exist, returns test data 54 | // e.g. 55 | // { 56 | // user: {id: 1, username: 'foo', ...}, 57 | // token: 'validToken' 58 | // } 59 | getUserSession: function () { 60 | var session = {}; 61 | 62 | if (this.android) { 63 | session = parseJSON(this.android.getUserSession()); 64 | } else { 65 | // For testing 66 | session = { 67 | user: { 68 | id: 1, 69 | username: 'jonatmozilla', 70 | avatar: 'https://secure.gravatar.com/avatar/c1b6f037ee7e440940d3c0c6ebeb8ba2?d=https%3A%2F%2Fstuff.webmaker.org%2Favatars%2Fwebmaker-avatar-200x200.png', 71 | prefLocale: 'en-US' 72 | }, 73 | token: 'validToken' 74 | }; 75 | } 76 | 77 | return session; 78 | }, 79 | 80 | getInitialState: function () { 81 | var session = this.getUserSession(); 82 | return { 83 | params: this.getRouteParams(), 84 | routeData: this.getRouteData(), 85 | user: session.user || {}, 86 | token: session.token || '' 87 | }; 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/lib/swipe.js: -------------------------------------------------------------------------------- 1 | module.exports = function calculateSwipe(startX, startY, endX, endY) { 2 | var THRESHOLD = 100; 3 | var MINIMUM_DISTANCE = 50; 4 | 5 | var distance = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2)); 6 | 7 | if (distance >= MINIMUM_DISTANCE) { 8 | if (startX > endX && Math.abs(startY - endY) < THRESHOLD) { 9 | return('LEFT'); 10 | } else if (startX < endX && Math.abs(startY - endY) < THRESHOLD) { 11 | return('RIGHT'); 12 | } else if (startY < endY && Math.abs(startX - endX) < THRESHOLD) { 13 | return('DOWN'); 14 | } else if (startY > endY && Math.abs(startX - endX) < THRESHOLD) { 15 | return('UP'); 16 | } 17 | } else { 18 | return false; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/uuid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mad science micro-sized UUID (v4) module. 3 | * 4 | * Adapted from: https://gist.github.com/jed/982883 5 | * 6 | * @package webmaker 7 | * @author Andrew Sliwinski 8 | */ 9 | 10 | /* jshint ignore:start */ 11 | // jscs:disable 12 | function b( 13 | a // placeholder 14 | ) { 15 | return a // if the placeholder was passed, return 16 | ? ( // a random number from 0 to 15 17 | a ^ // unless b is 8, 18 | Math.random() // in which case 19 | * 16 // a random number from 20 | >> a / 4 // 8 to 11 21 | ).toString(16) // in hexadecimal 22 | : ( // or otherwise a concatenated string: 23 | [1e7] + // 10000000 + 24 | -1e3 + // -1000 + 25 | -4e3 + // -4000 + 26 | -8e3 + // -80000000 + 27 | -1e11 // -100000000000, 28 | ).replace( // replacing 29 | /[018]/g, // zeroes, ones, and eights with 30 | b // random hex digits 31 | ); 32 | } 33 | 34 | module.exports = b; 35 | /* jshint ignore:end */ 36 | // jscs:enable 37 | -------------------------------------------------------------------------------- /src/lib/validators.js: -------------------------------------------------------------------------------- 1 | var owasp = require('owasp-password-strength-test'); 2 | 3 | // this removes the repeating character required test 4 | // https://github.com/cadecairos/PassTest/blob/master/index.js 5 | owasp.tests.required.splice(2, 1); 6 | 7 | //https://github.com/mozilla/id.webmaker.org/blob/develop/web/server.js#L13 8 | owasp.config({ 9 | minLength: 8, 10 | maxLength: 256, 11 | minPhraseLength: 20, 12 | minOptionalTestsToPass: 2, 13 | allowPassphrases: true 14 | }); 15 | 16 | module.exports = { 17 | 18 | validators: { 19 | email: { 20 | regex: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/i, 21 | message: 'Please use a valid email address.' 22 | }, 23 | password: { 24 | test: function (input) { 25 | input = input || ''; 26 | return owasp.test(input).strong; 27 | }, 28 | message: 'Your password must be at least 8 characters and contain at least 1 number and 1 letter' 29 | }, 30 | username: { 31 | regex: /^[a-zA-Z0-9\-]{1,20}$/, 32 | message: 'Must be 1-20 characters long and use only "-" and alphanumeric symbols.' 33 | } 34 | }, 35 | 36 | // Looks at this.fields, and returns an object of errors 37 | // keyed on field.name 38 | // 39 | // e.g. this.fields should look like: 40 | // [{ 41 | // name: 'username', 42 | // label: 'Username', 43 | // required: true, 44 | // validations: 'username' 45 | // }, 46 | // { 47 | // name: 'email', 48 | // label: 'Email', 49 | // type: 'email', 50 | // required: true, 51 | // validations: 'email' 52 | // }, 53 | // { 54 | // name: 'password', 55 | // label: 'Password', 56 | // type: 'password', 57 | // required: true, 58 | // validations: ['passwordLength', 'lowerCase', 'upperCase', 'numbers'] 59 | // }] 60 | 61 | getValidationErrors: function () { 62 | var errors = {}; 63 | var fields = this.fields; 64 | 65 | if (!fields) { 66 | return errors; 67 | } 68 | 69 | fields.forEach(fieldData => { 70 | 71 | var field = fieldData.name; 72 | var value = this.state[field]; 73 | var isRequired = fieldData.required; 74 | var isEmpty = !value && value !== 0; 75 | var validations = fieldData.validations; 76 | 77 | // Required 78 | if (isRequired && isEmpty) { 79 | errors.requiredFieldsMissing = true; 80 | } 81 | 82 | // Custom Validation 83 | if (validations) { 84 | if (typeof validations === 'string') { 85 | validations = [validations]; 86 | } 87 | validations.forEach(type => { 88 | var validation = this.validators[type]; 89 | var errorMessage = validation.message; 90 | var test = validation.test || ((input) => validation.regex.test(input)); 91 | 92 | // Test the value if it is non-emptynpm tun 93 | if (!isEmpty && !test(value)) { 94 | errors[field] = errors[field] || []; 95 | errors[field].push(errorMessage); 96 | } 97 | 98 | }); 99 | } 100 | 101 | }); 102 | 103 | return errors; 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /src/locales/bn-BD.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | bn-BD: 3 | username: ইউজারনেম 4 | password: পাসওয়ার্ড 5 | email: ইমেইল 6 | email_me_updates: ওয়েবমেকার সম্পর্কে খবরাখবর আমাকে ইমেইল করো 7 | join_webmaker: ওয়েবমেকারে যোগ দিন 8 | privacy_policy: প্রাইভেসি পলিসি 9 | signin: সাইন ইন 10 | errorUsernameOrPassword: মনে হচ্ছে আপনার ইউজারনেম বা পাসওয়ার্ডে কোন সমস্যা হয়েছে। 11 | Red: লাল 12 | Green: সবুজ 13 | Blue: নীল 14 | Opacity: সচ্ছতা 15 | log_out: লগ আউট 16 | title: শিরোনাম 17 | done: সম্পন্ন 18 | edit_label: লেবেল সম্পাদনা 19 | change_link_dest: লিঙ্ক গন্তব্যের পরিবর্তন 20 | set_link_dest: লিঙ্ক গন্তব্য নির্ধারণ করুন 21 | corner_radius: কোণের মাপ 22 | Font: ফন্ট 23 | background_color: ব্যাকগ্রাউন্ডের রঙ 24 | error_update_element: উপকরণ হালনাগাদে সমস্যা হয়েছে 25 | error_element: উপকরণ লোডিং এ সমস্যা হয়েছে 26 | error_element_404: কোন উপকরণ পাওয়া যায়নি 27 | error_delete_element: উপকরণ মুছে ফেলতে সমস্যা হয়েছে 28 | -------------------------------------------------------------------------------- /src/locales/bn-IN.yaml: -------------------------------------------------------------------------------- 1 | bn-IN: 2 | username: ব্যবহারকারীর নাম 3 | password: 'পাসওয়ার্ড ' 4 | email: ইমেল 5 | helpText: পাসওয়ার্ড রিসেট 6 | email_me_updates: ওয়েবমেকার সম্পর্কে আপডেট আমায় ইমেইল করুন। 7 | join_webmaker: Webmaker-এ যোগ দিন 8 | by_joining: যুক্ত হয়ে, আমি মোজিলা ওয়েবমেকারের {linkTerms} এবং {linkPrivacyPolicy} এ সম্মতি প্রদান করছি। 9 | privacy_policy: গোপনীয়তা নীতি 10 | terms: শর্তাবলী 11 | already_join_signin: ইতিমধ্যে যোগ দিয়েছেন? {signinLink} 12 | signin: 'সাইন ইন ' 13 | dont_have_account: কোন অ্যাকাউন্ট নেই? {joinWebmakerLink} 14 | errorUsernameOrPassword: আপনার ব্যবহারকারী নাম বা পাসওয়ার্ড দিয়ে একটি সমস্যা হতে পারে বলে মনে হচ্ছে। 15 | color_editor: 'রঙ: {rgbaValue};' 16 | Red: লাল 17 | Green: সবুজ 18 | Blue: নীল 19 | Opacity: অস্বচ্ছতা 20 | my_project: আমার প্রকল্প 21 | log_out: প্রস্থান 22 | create_a_project: + প্রকল্প তৈরি করুন 23 | open_style_guide: উন্মুক্ত স্টাইল গাইড 24 | reset_share_preferences: শেয়ার করার পছন্দ রিসেট করুন 25 | loading: লোডিং! 26 | title: শিরোনাম 27 | done: সম্পূর্ণ 28 | edit_label: ট্যাগ সম্পাদনা করুন। 29 | change_link_dest: লিঙ্ক গন্তব্য পরিবর্তন করুন। 30 | set_link_dest: লিঙ্ক গন্তব্য সেট করুন। 31 | corner_radius: দৃষ্টিকোন ব্যাসার্ধ 32 | Font: ফন্ট 33 | background_color: পটভূমির রঙ 34 | no_project_found: দুঃখিত, কোন প্রকল্প পাওয়া যায়নি। 35 | error_featured: প্রকল্প উদ্ঘাটনে সমস্যা হয়েছে 36 | error_featured_404: কোন উদ্ঘাটিত প্রকল্প পাওয়া যায়নি 37 | error_update_element: উপাদান আপডেট করার সময় একটি ত্রুটি ছিল। 38 | error_element: উপাদান লোড করার সময় ত্রুটি। 39 | error_element_404: উপাদান খুঁজে পাওয়া যায়নি। 40 | error_create_make: প্রকল্প তৈরিতে চেষ্টা করা হয়েছে যখন কোন সেশন পাওয়া যায়নি 41 | error_create_element: উপকরণ তৈরিতে সমস্যা হয়েছে 42 | error_delete_element: উপাদান মুছে ফেলার সময় সমস্যা হয়েছে। 43 | error_page: পাতা লোড হতে কোন সমস্যা হয়েছে 44 | error_page_404: পাতা লোড করার জন্য পাওয়া যায়নি 45 | error_404_element_save: উপকরণ সংরক্ষণের জন্য পাওয়া যায়নি 46 | cc: ক্রিয়েটিভ কমন্স 47 | ccLicenseSection: কন্টেন্ট {creativecommonsLink} নামের একটি ক্রিয়েটিভ কমন্স লাইসেন্সের আওতায় প্রকাশিত হয়েছে। 48 | ccLicenseSectionSub: এর অর্থ হচ্ছে অন্য কেউ আপনার কৃতিত্ব স্বীকার করে আপনার কন্টেন্ট শেয়ার, অভিযোজন, এবং রিমিক্স করতে পারবে এবং একইভাবে তাদের সৃষ্টিকর্মও শেয়ার করতে পারবে, যেভাবে এই লাইসেন্সে বর্ণনা করা হয়েছে। 49 | ccAttribution: Attribution-ShareAlike 3.0 Unported 50 | -------------------------------------------------------------------------------- /src/locales/en-GB.yaml: -------------------------------------------------------------------------------- 1 | en-GB: 2 | username: Username 3 | password: Password 4 | email: Email 5 | helpText: Reset Password 6 | email_me_updates: Email me updates about Webmaker 7 | join_webmaker: Join Webmaker 8 | by_joining: By joining, I agree to Mozilla Webmaker’s {linkTerms} and {linkPrivacyPolicy} 9 | privacy_policy: Privacy Policy 10 | terms: Terms 11 | already_join_signin: Already joined? {signinLink} 12 | signin: Sign in 13 | dont_have_account: Don’t have an account? {joinWebmakerLink} 14 | errorUsernameOrPassword: Looks like there might be a problem with your username or password. 15 | color_editor: 'colour: {rgbaValue};' 16 | Red: Red 17 | Green: Green 18 | Blue: Blue 19 | Opacity: Opacity 20 | my_project: My project 21 | log_out: Log Out 22 | create_a_project: + Create a Project 23 | open_style_guide: Open Style Guide 24 | reset_share_preferences: Reset Shared Preferences 25 | loading: Loading! 26 | title: Title 27 | done: Done 28 | edit_label: Edit Label 29 | change_link_dest: Change Link Destination 30 | set_link_dest: Set Link Destination 31 | corner_radius: Corner Radius 32 | Font: Text 33 | background_color: Background Colour 34 | no_project_found: Sorry, no projects found. 35 | error_featured: Error getting discovery projects 36 | error_featured_404: No discovery projects found 37 | error_update_element: There was an error updating the element 38 | error_element: Error loading element 39 | error_element_404: No element found 40 | error_create_make: Tried to create project when no session was found 41 | error_create_element: There was an error creating an element 42 | error_delete_element: There was a problem deleting the element 43 | error_page: There was an error getting the page to load 44 | error_page_404: Could not find the page to load 45 | error_404_element_save: Could not find the element to save 46 | cc: Creative Commons 47 | ccLicenseSection: Content published under a Creative Commons license called {creativecommonsLink}. 48 | ccLicenseSectionSub: This means that other people can share, adapt, and remix your content if they give you credit and share their work in the same way, as all of this is described in the license. 49 | ccAttribution: Attribution-ShareAlike 3.0 Unported 50 | -------------------------------------------------------------------------------- /src/locales/en-US.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | en-US: 3 | create_account: Create a Webmaker Account 4 | pick_username: Pick a username 5 | username: Username 6 | username_email: Username or Email 7 | set_password: Set password 8 | password: Password 9 | email: Email 10 | helpText: Reset Password 11 | email_me_updates: Email me updates about Webmaker 12 | join_webmaker: Join Webmaker 13 | by_joining: By joining, I agree to Mozilla Webmaker’s {linkTerms} and {linkPrivacyPolicy} 14 | privacy_policy: Privacy Policy 15 | terms: Terms 16 | already_join_signin: Already joined? {signinLink} 17 | signin: Sign in 18 | signin_webmaker: Sign in to Webmaker 19 | dont_have_account: Don’t have an account? {joinWebmakerLink} 20 | errorUsernameOrPassword: Looks like there might be a problem with your username or password. 21 | color_editor: 'color: {rgbaValue};' 22 | text_color: Text Color 23 | Red: Red 24 | Green: Green 25 | Blue: Blue 26 | Opacity: Opacity 27 | my_project: My project 28 | log_out: Log out 29 | create_a_project: + Create a Project 30 | open_style_guide: Open Style Guide 31 | reset_share_preferences: Reset Shared Preferences 32 | loading: Loading! 33 | title: Title 34 | done: Done 35 | edit_label: Edit Label 36 | change_link_dest: Change Link Destination 37 | set_link_dest: Set Link Destination 38 | corner_radius: Corner Radius 39 | Font: Font 40 | background_color: Background Color 41 | no_project_found: Sorry, no projects found. 42 | error_featured: Error getting discovery projects 43 | error_featured_404: No discovery projects found 44 | error_update_element: There was an error updating the element 45 | error_element: Error loading element 46 | error_element_404: No element found 47 | error_create_make: Tried to create project when no session was found 48 | error_create_element: There was an error creating an element 49 | error_delete_element: There was a problem deleting the element 50 | error_page: There was an error getting the page to load 51 | error_page_404: Could not find the page to load 52 | error_404_element_save: Could not find the element to save 53 | error_featured_tags: Could not fetch featured tags 54 | cc: Creative Commons 55 | ccLicenseSection: Your Webmaker content is published under the Creative Commons {creativecommonsLink} as described in the {termsofuse}. 56 | ccLicenseSectionSub: This means that others may remix, tweak, and build upon your work even for commercial purposes, as long as they credit you and license their new creations under the identical terms. 57 | ccAttribution: Attribution-ShareAlike 3.0 Unported license 58 | ccTermsOfUse: terms of use 59 | error_discovery_get: Error getting discovery projects 60 | badTitle: Title must be between {minLength} and {maxLength} characters 61 | badDescription: Description must be less than {maxLength} characters 62 | ftu-explore: Explore stories around the world 63 | ftu-make-your-own: ...or make your own! 64 | ftu-share-project: Share this project with anyone! 65 | ftu-remix: Make your own version of this – hit 'Remix!' 66 | need_help: Need Help? 67 | link_editor_help: You can link your button to another page or to a destination on the web 68 | link_header: Link Destination 69 | dest_btn_page: A page in this project 70 | dest_btn_web: Web URL 71 | got_it: OK, got it! 72 | web_link_intro: A link can take us to any page or image on the web with an "address" or URL. 73 | web_link_label: URL 74 | invalid_url: Please enter a valid URL. 75 | follow_link: Follow Link 76 | hashtag: Hashtag 77 | featured: Featured 78 | -------------------------------------------------------------------------------- /src/locales/es-MX.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | es-MX: 3 | username: Usuario 4 | password: Contraseña 5 | email: Correo electrónico 6 | helpText: Reiniciar contraseña 7 | email_me_updates: Enviarme actualizaciones acerca de Webmaker 8 | join_webmaker: Únete a Webmaker 9 | by_joining: Al unirte, aceptas los {linkTerms} y {linkPrivacyPolicy} 10 | privacy_policy: Política de privacidad 11 | terms: Términos 12 | already_join_signin: ¿Ya te uniste? {signinLink} 13 | signin: Entrar 14 | dont_have_account: ¿No tienes una cuenta? {joinWebmakerLink} 15 | errorUsernameOrPassword: Parece que podría haber un probelam con tu usuario o contraseña. 16 | color_editor: 'color: {rgbaValue};' 17 | Red: Rojo 18 | Green: Verde 19 | Blue: Azul 20 | Opacity: Opacidad 21 | my_project: Mi proyecto 22 | log_out: Salir 23 | create_a_project: + Crear un proyecto 24 | open_style_guide: Abrir la guía de estilo 25 | reset_share_preferences: Reiniciar preferencias compartidas 26 | loading: ¡Cargando! 27 | title: Título 28 | done: Hecho 29 | edit_label: Editar etiqueta 30 | change_link_dest: Cambiar destino del link 31 | set_link_dest: Definir destino del link 32 | corner_radius: Radio de las esquinas 33 | Font: Fuente 34 | background_color: Color de fondo 35 | no_project_found: Lo sentimos, no encontramos ningún proyecto. 36 | error_featured: Error descubriendo proyectos 37 | error_featured_404: No se encontraron proyectos en descubre 38 | error_update_element: Hubo un error actualizando el elementp 39 | error_element: Error cargando el elemento 40 | error_element_404: Elemento no encontrado 41 | error_create_make: Intentamos crear un proyecto cuando no encotramos una sesión 42 | error_create_element: Hubo un error creando un elemento 43 | error_delete_element: Hubo un problema eliminando el elemento 44 | error_page: Hubo un error al obtener la página para cargar 45 | error_page_404: No se pudo encontrar la página para leer 46 | error_404_element_save: No se pudo encontrar el elemento para guardar 47 | cc: Creative Commons 48 | ccLicenseSection: Contenido publicado bajo una licencia llamada Creative Commons {creativecommonsLink}. 49 | ccLicenseSectionSub: Esto significa que otra gente puede compartir, adaptar, y mezclar tu 50 | contenido si ellos te dan crédito y comparten su trabajo del mismo modo, todo esto 51 | se describe en la licencia. 52 | ccAttribution: Reconocimiento-CompartirIgual 3.0 Unported 53 | error_discovery_get: Error obteniendo proyectos en descubre 54 | -------------------------------------------------------------------------------- /src/locales/id-ID.yaml: -------------------------------------------------------------------------------- 1 | id-ID: 2 | username: Nama pengguna 3 | password: Kata kunci 4 | email: Surel 5 | helpText: Tetapkan ulang Kata kunci 6 | email_me_updates: Kirim saya surel info terbaru tentang Webmaker 7 | join_webmaker: Bergabung dengan Webmaker 8 | by_joining: Dengan bergabung, saya menyetujui {linkTerms} dan {linkPrivacyPolicy} dari Mozilla Webmaker 9 | privacy_policy: Kebijakan Privasi 10 | terms: Syarat 11 | already_join_signin: Sudah bergabung? {signinLink} 12 | signin: Masuk 13 | dont_have_account: Tidak punya akun? {joinWebmakerLink} 14 | errorUsernameOrPassword: Sepertinya ada kesalahan pada nama pengguna atau kata sandi Anda. 15 | color_editor: 'warna: {rgbaValue};' 16 | Red: Merah 17 | Green: Hijau 18 | Blue: Biru 19 | Opacity: Tingkat pengaburan 20 | my_project: Proyek saya 21 | log_out: Keluar 22 | create_a_project: + Buat Proyek 23 | open_style_guide: Buka Panduan Gaya 24 | reset_share_preferences: Tetapkan ulang Preferensi Bersama 25 | loading: Memuat! 26 | title: Judul 27 | done: Selesai 28 | edit_label: Atur Label 29 | change_link_dest: Ubah Tujuan Tautan 30 | set_link_dest: Atur Tujuan Tautan 31 | corner_radius: Radius Sudut 32 | Font: Jenis Huruf 33 | background_color: Warna Latar 34 | no_project_found: Maaf, proyek tidak ditemukan. 35 | error_featured: Ada kesalahan dalam mendapatkan proyek temuan 36 | error_featured_404: Tidak ada proyek temuan ditemukan 37 | error_update_element: Ada kesalahan dalam memperbarui elemen 38 | error_element: Kesalahan dalam memuat elemen 39 | error_element_404: Elemen tidak ditemukan 40 | error_create_make: Telah coba membuat proyek ketika tiada sesi ditemukan 41 | error_create_element: Ada kesalahan dalam membuat elemen 42 | error_delete_element: Ada masalah dalam menghapus elemen 43 | error_page: Ada kesalahan dalam memuat halaman 44 | error_page_404: Tidak dapat menemukan halaman untuk dimuat 45 | error_404_element_save: Tidak dapat menemukan elemen untuk disimpan 46 | cc: Creative Commons 47 | ccLicenseSection: Konten diterbitkan dibawah lisensi Creative Commons bernama {creativecommonsLink}. 48 | ccLicenseSectionSub: Ini berarti orang lain dapat membagi, mengadaptasi dan mengubah-suai konten Anda jika mereka memberi kredit kepada Anda dan membagikan pekerjaan mereka dengan cara yang sama, sebagaimana hal tersebut dijelaskan di dalam lisensi. 49 | ccAttribution: Atribusi-BerbagiSerupa 3.0 Tanpa Adaptasi 50 | -------------------------------------------------------------------------------- /src/locales/pt-BR.yaml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | username: Nome de usuário 3 | password: Senha 4 | email: E-mail 5 | helpText: Resetar senha 6 | email_me_updates: Me envie atualizações por e-mail sobre o Webmaker 7 | join_webmaker: Faça parte do Webmaker 8 | by_joining: Ao entrar, Eu concordo com os {linkTerms} e {linkPrivacyPolicy} do Mozilla Webmaker 9 | privacy_policy: Política de privacidade 10 | terms: Condições 11 | already_join_signin: Já faz parte? {signinLink} 12 | signin: Entrar 13 | dont_have_account: Não tem uma conta? {joinWebmakerLink} 14 | errorUsernameOrPassword: Parece que existe um problema com o seu nome de usuário ou senha. 15 | color_editor: 'Cor: {rgbaValue};' 16 | Red: Vermelho 17 | Green: Verde 18 | Blue: Azul 19 | Opacity: Opacidade 20 | my_project: Meu projeto 21 | log_out: Sair 22 | create_a_project: +Criar um Projeto 23 | open_style_guide: Guia Aberta de Estilo 24 | reset_share_preferences: Reinicializar preferências compartilhadas 25 | loading: Carregando! 26 | title: Título 27 | done: Concluído 28 | edit_label: Editar rótulo 29 | change_link_dest: Trocar destino do link 30 | set_link_dest: Definir destino do link 31 | corner_radius: Raio do canto 32 | Font: Fonte 33 | background_color: Cor de fundo 34 | no_project_found: Desculpa, não foram encontrados projetos. 35 | error_featured: Erro ao procurar projetos em destaques 36 | error_featured_404: Não há projetos em destaques encontrados 37 | error_update_element: Houve um erro ao atualizar o elemento 38 | error_element: Erro ao carregar o elemento 39 | error_element_404: Nenhum elemento encontrado 40 | error_create_make: Tentou criar um projeto quando estava em um sessão não encontrada 41 | error_create_element: Houve algum erro ao criar um elemento 42 | error_delete_element: Houve um problema ao remover o elemento 43 | error_page: Houve um erro ao carregar a página 44 | error_page_404: Não conseguimos encontrar a página carregada 45 | error_404_element_save: Não conseguimos encontrar o elemento salvo 46 | cc: Creative Commons 47 | ccLicenseSection: Conteúdo publicado sob uma licença chamada Creative Commons {creativecommonsLink}. 48 | ccLicenseSectionSub: Isso significa que outras pessoas podem compartilhar, adaptar e remixar o seu conteúdo, com isso devem dar os créditos e compartilhar o seu trabalho,do mesmo modo, como tudo isto está descrito na licença. 49 | ccAttribution: Compartilhada pela mesma licença atrubuída 3.0 não adaptada 50 | -------------------------------------------------------------------------------- /src/main.less: -------------------------------------------------------------------------------- 1 | @import "normalize"; 2 | @import "variables"; 3 | @import "colors"; 4 | @import "fonts"; 5 | 6 | // Components ----------------------------------------------------------------- 7 | 8 | @import "components/action-menu/action-menu"; 9 | @import "components/alert/alert"; 10 | @import "components/basic-element/basic-element"; 11 | @import "components/button/button"; 12 | @import "components/card/card"; 13 | @import "components/color-group/color-group"; 14 | @import "components/color-spectrum/color-spectrum"; 15 | @import "components/element-group/element-group"; 16 | @import "components/link/link"; 17 | @import "components/loading/loading"; 18 | @import "components/modal-confirm/modal-confirm"; 19 | @import "components/modal-switch/modal-switch"; 20 | @import "components/option-panel/option-panel"; 21 | @import "components/range/range"; 22 | @import "components/select/select"; 23 | @import "components/shim/shim"; 24 | @import "components/snackbar/snackbar"; 25 | @import "components/spindicator/spindicator"; 26 | @import "components/tabs/tabs"; 27 | @import "components/text-input/text-input"; 28 | @import "components/d-pad/d-pad"; 29 | @import "components/ftu/ftu"; 30 | @import "components/project-list/project-list"; 31 | @import "components/featured-tag-card/featured-tag-card"; 32 | 33 | // Pages ---------------------------------------------------------------------- 34 | 35 | @import "pages/user-projects/user-projects"; 36 | @import "pages/element/element"; 37 | @import "pages/login/login"; 38 | @import "pages/make/make"; 39 | @import "pages/page/page"; 40 | @import "pages/project/project"; 41 | @import "pages/project-settings/project-settings"; 42 | @import "pages/style-guide/style-guide"; 43 | @import "pages/tinker/tinker"; 44 | @import "pages/web-link/web-link"; 45 | @import "pages/tag-list/tag-list"; 46 | 47 | *, 48 | *:after, 49 | *:before { 50 | box-sizing: border-box; 51 | -webkit-backface-visibility: hidden; 52 | -webkit-tap-highlight-color: rgba(0,0,0,0); 53 | -webkit-tap-highlight-color: transparent; 54 | } 55 | 56 | html { 57 | height: 100%; 58 | } 59 | 60 | body { 61 | height: 100%; 62 | margin: 0; 63 | padding: 0; 64 | font-family: "FiraSans", "Fira Sans", "Fira Sans OT", sans-serif; 65 | } 66 | 67 | code, pre { 68 | font-family: menlo, monospace; 69 | font-size: inherit; 70 | } 71 | 72 | #app { 73 | height: 100%; 74 | } 75 | 76 | .container { 77 | height: 100%; 78 | user-select: none; 79 | 80 | > div { 81 | background: @lightGrey; 82 | min-height: 100%; 83 | } 84 | } 85 | 86 | // Screen readers only 87 | .sr-only { 88 | position: absolute; 89 | width: 1px; 90 | height: 1px; 91 | padding: 0; 92 | margin: -1px; 93 | overflow: hidden; 94 | clip: rect(0 0 0 0); 95 | border: 0; 96 | } 97 | 98 | .text-center { 99 | text-align: center; 100 | } 101 | .text-right { 102 | text-align: right; 103 | } 104 | -------------------------------------------------------------------------------- /src/pages/discover/discover.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var api = require('../../lib/api'); 3 | var reportError = require('../../lib/errors'); 4 | var i18n = require('../../lib/i18n'); 5 | 6 | var render = require('../../lib/render.jsx'); 7 | var Card = require('../../components/card/card.jsx'); 8 | var Loading = require('../../components/loading/loading.jsx'); 9 | var ProjectList = require ('../../components/project-list/project-list.jsx'); 10 | 11 | var lang = i18n.isSupportedLanguage(i18n.currentLanguage) ? i18n.currentLanguage : i18n.defaultLang; 12 | 13 | var Discover = React.createClass({ 14 | mixins: [require('react-intl').IntlMixin], 15 | render: function () { 16 | return ( 17 | 18 | ); 19 | } 20 | }); 21 | 22 | render(Discover); 23 | -------------------------------------------------------------------------------- /src/pages/element/element.less: -------------------------------------------------------------------------------- 1 | @element-label_margin-bottom: 10px; 2 | @editor-form-group_margin-bottom: 20px; 3 | 4 | #editor { 5 | color: @slate; 6 | height: 100%; 7 | position: relative; 8 | } 9 | .editor{ 10 | height: 100%; 11 | } 12 | 13 | .get-media { 14 | .get-media-overlay { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | height: 100%; 19 | width: 100%; 20 | z-index: 998; 21 | background: #1A2237; 22 | opacity: 0; 23 | visibility: hidden; 24 | transition: all 0.2s ease-in-out; 25 | &.active { 26 | visibility: visible; 27 | opacity: 0.7; 28 | } 29 | } 30 | 31 | .get-media-controls { 32 | position: fixed; 33 | top: 0; 34 | left: 0; 35 | bottom: 0; 36 | right: 0; 37 | width: 170px; 38 | height: 300px; 39 | z-index: 999; 40 | margin: auto; 41 | 42 | opacity: 0; 43 | visibility: hidden; 44 | transition: all 0.2s ease-in-out; 45 | 46 | &.active { 47 | visibility: visible; 48 | opacity: 1.0; 49 | } 50 | 51 | p { 52 | color: @slate; 53 | } 54 | 55 | button, a { 56 | display: block; 57 | position: relative; 58 | width: 170px; 59 | height: 140px; 60 | margin-bottom: 20px; 61 | 62 | border: none; 63 | outline: none; 64 | background-color: @white; 65 | border-radius: 5px; 66 | box-shadow: 0px 6px 24px 0px rgba(0,0,0,0.3); 67 | 68 | .icon { 69 | width: 50px; 70 | height: 50px; 71 | display: block; 72 | margin: 20px auto 0 auto; 73 | } 74 | } 75 | } 76 | } 77 | 78 | 79 | /********************************************************* 80 | * Editor preview and options 81 | */ 82 | 83 | // These are outside the id 84 | // because adding them inside #editor would make 85 | // them too specific and therefore really difficult to override 86 | @editor-preview-height: 150px; 87 | .editor-preview { 88 | box-shadow: 0px 4px 10px 0px rgba(84,94,102,0.30); 89 | text-align: center; 90 | background: @softGrey; 91 | height: 150px; 92 | display: flex; 93 | align-items: center; 94 | justify-content: center; 95 | overflow-y: scroll; 96 | position: fixed; 97 | top: 0; 98 | height: 150px; 99 | z-index: 2; 100 | width: 100%; 101 | img { 102 | box-sizing: content-box; 103 | height: 70%; 104 | } 105 | p { 106 | max-width: 320px; 107 | font-size: 1.6rem; 108 | } 109 | } 110 | 111 | .editor-scroll{ 112 | background-image: url("./img/scrubber.svg"); 113 | background-position-x: 100%; 114 | background-repeat: no-repeat repeat; 115 | padding: 20px 40px 20px 0; 116 | } 117 | .editor-options { 118 | font-size: 15px; 119 | margin-top: @editor-preview-height; 120 | padding: 0 0 0 20px; 121 | height: calc(~"100% - " @editor-preview-height); 122 | overflow-y: scroll; 123 | label, input { 124 | width: 100%; 125 | display: block; 126 | } 127 | label { 128 | margin-bottom: @element-label_margin-bottom; 129 | } 130 | .form-group { 131 | margin-bottom: @editor-form-group_margin-bottom; 132 | &:last-child { 133 | margin-bottom: 0; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/pages/element/font-selector.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | /** 4 | * This is a mixin due to the fact that we rely on this.linkState, 5 | * which handles the automatic state binding to whatever component 6 | * is using this font selection component for changing font-family 7 | */ 8 | module.exports = { 9 | /** 10 | * Generate the dropdown selector for fonts, with each font option 11 | * styled in the appropriate font. 12 | * @return {[type]} 13 | */ 14 | generateFontSelector: function() { 15 | var fonts = ["Roboto", "Bitter", "Pacifico"]; 16 | var options = fonts.map(name => { 17 | // setting style on an ; 19 | }); 20 | return ; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/pages/element/page-editor.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var LinkedStateMixin = require('react-addons-linked-state-mixin'); 3 | var ColorGroup = require('../../components/color-group/color-group.jsx'); 4 | 5 | var colorChoices = ["#f2f6fc","#484B50","#6D7886","#98CC3B","#E26A1F","#8173E4"]; 6 | 7 | var PageEditor = React.createClass({ 8 | mixins: [ 9 | LinkedStateMixin, 10 | require('react-intl').IntlMixin 11 | ], 12 | getInitialState: function () { 13 | return { 14 | type: "page", 15 | backgroundColor: this.props.element.styles.backgroundColor || '#f2f6fc' 16 | }; 17 | }, 18 | componentDidUpdate: function (prevProps, prevState) { 19 | var state = this.state; 20 | 21 | // Update state if parent properties change 22 | if (this.props.element !== prevProps.element) { 23 | state = this.getInitialState(); 24 | this.setState(state); 25 | } 26 | 27 | // Cache edits if internal state changes 28 | if (this.state !== prevState) { 29 | this.props.cacheEdits(state); 30 | } 31 | }, 32 | componentWillMount: function () { 33 | }, 34 | render: function () { 35 | return ( 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | }); 51 | 52 | module.exports = PageEditor; 53 | -------------------------------------------------------------------------------- /src/pages/element/witheditable.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | editText: function(evt) { 3 | if (evt) { 4 | evt.stopPropagation(); 5 | evt.preventDefault(); 6 | } 7 | this.refs.element.toggleEditing(); 8 | // calls our setEditing function after changing state 9 | }, 10 | stopEditing: function(evt) { 11 | this.refs.element.stopEditing(); 12 | }, 13 | setEditing: function(boolval) { 14 | this.setState({ 15 | editing: boolval 16 | }); 17 | }, 18 | updateText: function (text) { 19 | this.setState({ 20 | innerHTML: text 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/pages/login/form-input.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FormattedHTMLMessage = require('react-intl').FormattedHTMLMessage; 3 | var Link = require('../../components/link/link.jsx'); 4 | 5 | var FormInput = React.createClass({ 6 | mixins: [require('react-intl').IntlMixin], 7 | getDefaultProps: function () { 8 | return { 9 | type: 'text', 10 | tabIndex: 0 11 | }; 12 | }, 13 | checkForReturn: function (e) { 14 | if (e.keyCode === 13) { 15 | if (typeof this.props.onReturn === 'function') { 16 | this.props.onReturn(); 17 | } 18 | } 19 | }, 20 | render: function () { 21 | var helpText; 22 | if(this.props.helpText) { 23 | helpText = (); 24 | } 25 | return (
26 |
27 | 28 | 29 |
30 | 33 | 34 |
); 35 | } 36 | }); 37 | 38 | module.exports = FormInput; 39 | -------------------------------------------------------------------------------- /src/pages/login/login.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var render = require('../../lib/render.jsx'); 3 | 4 | var Loading = require('../../components/loading/loading.jsx'); 5 | var SignIn = require('./sign-in.jsx'); 6 | var SignUp = require('./sign-up.jsx'); 7 | 8 | // 9 | // View that renders sign-in and sign-up forms 10 | var Login = React.createClass({ 11 | mixins: [require('react-intl').IntlMixin, require('../../lib/router')], 12 | 13 | // Props: 14 | // (none) 15 | 16 | // State: 17 | // mode 18 | // ['sign-in', 'sign-up'] 19 | // Indicates which form is currently visible 20 | // loading 21 | // boolean 22 | // Turns the UI-blocking loader on/off 23 | getInitialState: function () { 24 | var mode = this.getRouteParams().mode; 25 | if (['sign-up', 'sign-in'].indexOf(mode) === -1) { 26 | mode = 'sign-up'; 27 | } 28 | return { 29 | mode, 30 | loading: false 31 | }; 32 | }, 33 | componentWillMount: function () { 34 | this.props.update({ 35 | onBackPressed: () => { 36 | window.Platform.goToHomeScreen(); 37 | } 38 | }); 39 | }, 40 | setParentState: function (state) { 41 | this.setState(state); 42 | }, 43 | render: function () { 44 | return
45 |
46 | 47 | 48 | 49 |
; 50 | } 51 | }); 52 | 53 | render(Login); 54 | -------------------------------------------------------------------------------- /src/pages/login/login.less: -------------------------------------------------------------------------------- 1 | #login { 2 | padding: 20px 0 0 0; 3 | background: @blue; 4 | color: @white; 5 | 6 | h3 { 7 | margin: 0 0 20px 0; 8 | text-align: center; 9 | font-weight: 400; 10 | font-size: 20px; 11 | } 12 | 13 | .logo { 14 | text-align: center; 15 | img { 16 | width: 80px; 17 | } 18 | } 19 | 20 | .login-options { 21 | padding: 10px 0; 22 | font-size: 15px; 23 | label, input { 24 | width: 85%; 25 | display: block; 26 | margin: 12px auto; 27 | } 28 | .form-group { 29 | margin: 0; 30 | border-bottom: 2px solid fade(@white, 15%); 31 | } 32 | >div:first-of-type { 33 | .form-group { 34 | border-top: 2px solid fade(@white, 15%); 35 | } 36 | } 37 | .checkbox { 38 | display: flex; 39 | align-items: center; 40 | margin-top: 20px; 41 | font-size: 14px; 42 | span { 43 | padding: 2px 0 0 0; 44 | } 45 | .checkbox-ui { 46 | flex-shrink: 0; 47 | display: block; 48 | margin-right: 5px; 49 | height: 24px; 50 | width: 24px; 51 | background: url(./img/material-check.png) center center / contain no-repeat; 52 | } 53 | input:checked + .checkbox-ui { 54 | background-image: url(./img/material-check-on.png); 55 | } 56 | input { 57 | .sr-only; 58 | } 59 | } 60 | } 61 | 62 | a { 63 | color: inherit; 64 | } 65 | 66 | .help-text { 67 | margin: 10px 30px 0 0; 68 | font-size: 14px; 69 | } 70 | 71 | .by-joining { 72 | text-align: center; 73 | max-width: 250px; 74 | margin: 10px auto 0 auto; 75 | opacity: 0.9; 76 | font-size: 12px; 77 | } 78 | 79 | .already-joined { 80 | margin-top: 25px; 81 | } 82 | 83 | .text-larger { 84 | font-size: 16px; 85 | font-weight: 500; 86 | } 87 | 88 | .btn { 89 | width: 90%; 90 | margin: 20px auto !important; 91 | font-size: 16px; 92 | line-height: 1.7; 93 | margin-bottom: 0; 94 | color: @blue; 95 | border-radius: 30px; 96 | border: 1px solid @shadowBlue; 97 | box-shadow: none; 98 | &[disabled] { 99 | background: darken(@blue, 5%); 100 | color: @white; 101 | opacity: 0.5; 102 | box-shadow: none; 103 | } 104 | } 105 | 106 | input[type="text"], 107 | input[type="password"], 108 | input[type="email"] { 109 | background: transparent; 110 | border: none; 111 | font-size: 20px; 112 | transition: all 0.2s ease-in-out; 113 | &::placeholder { 114 | color: fade(@white, 50%); 115 | font-size: 14px; 116 | padding-top: 4px; 117 | opacity: 1; 118 | } 119 | } 120 | 121 | .error { 122 | margin: 10px auto; 123 | width: 85%; 124 | background: @yellow; 125 | color: darken(@yellow, 40%); 126 | padding: 10px; 127 | border-radius: 3px; 128 | text-align: center; 129 | position: relative; 130 | &::after { 131 | bottom: 100%; 132 | left: 50%; 133 | border: solid transparent; 134 | content: " "; 135 | height: 0; 136 | width: 0; 137 | position: absolute; 138 | pointer-events: none; 139 | border-color: none; 140 | border-bottom-color: @yellow; 141 | border-width: 4px; 142 | margin-left: -4px; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/pages/make/make.less: -------------------------------------------------------------------------------- 1 | #make { 2 | display: block; 3 | position: relative; 4 | min-width: 100%; 5 | min-height: 100%; 6 | padding: 20px 0; 7 | 8 | .btn-create { 9 | width: calc(100% - 10px); 10 | margin: 20px auto; 11 | text-decoration: none; 12 | } 13 | 14 | .profile-card { 15 | border-radius: 3px; 16 | box-shadow: 0 2px 0 0 @heatherGrey; 17 | width: calc(100% - 10px); 18 | background: @white; 19 | color: @slate; 20 | margin: 0 auto 20px auto; 21 | padding: 10px; 22 | text-align: center; 23 | p { 24 | margin: 0; 25 | display: flex; 26 | } 27 | .btn { 28 | box-shadow: inset 0 0 0 2px fade(@slate, 50%); 29 | background: fade(@slate, 4%); 30 | flex: 1; 31 | margin: 5px; 32 | padding: 10px; 33 | } 34 | } 35 | .action { 36 | button { 37 | margin: 0; 38 | } 39 | } 40 | .text { 41 | padding: 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/page/flattening.js: -------------------------------------------------------------------------------- 1 | var types = require('../../components/basic-element/basic-element.jsx').types; 2 | module.exports = { 3 | flatten: function (element) { 4 | if (!types[element.type]) { 5 | return false; 6 | } 7 | return types[element.type].spec.flatten(element); 8 | }, 9 | 10 | expand: function (element) { 11 | if (!types[element.type]) { 12 | return false; 13 | } 14 | return types[element.type].spec.expand(element); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/pages/page/page-controls.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classNames = require('classnames'); 3 | var Link = require('../../components/link/link.jsx'); 4 | var platform = require('../../lib/platform'); 5 | 6 | var PageControls = React.createClass({ 7 | 8 | mixins: [ 9 | require("./flattening") 10 | ], 11 | 12 | secondaryButtonClass: function(name) { 13 | var names = { 14 | secondary: true, 15 | active: this.props.currentElementId > -1 && !this.props.showAddMenu 16 | }; 17 | names[name] = true; 18 | return classNames(names); 19 | }, 20 | 21 | render: function() { 22 | return ( 23 |
27 |
28 | 29 | 30 | 31 |
32 | 35 | 36 | 41 | 42 | 43 |
44 | ); 45 | } 46 | }); 47 | 48 | module.exports = PageControls; 49 | -------------------------------------------------------------------------------- /src/pages/project-settings/project-settings.less: -------------------------------------------------------------------------------- 1 | #projectSettings { 2 | padding: 30px; 3 | display: flex; 4 | flex-direction: column; 5 | min-height: 100%; 6 | 7 | > div:first-child { 8 | flex: 1; 9 | } 10 | 11 | .cc { 12 | font-size: 0.9em; 13 | color: @slate; 14 | 15 | img { 16 | opacity: 0.5; 17 | height: 18px; 18 | margin-right: 5px; 19 | } 20 | 21 | .mark { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | p:nth-child(2) { 27 | margin-bottom: 0; 28 | } 29 | 30 | a { 31 | color: @slate; 32 | } 33 | 34 | .explanation { 35 | color: lighten(@slate, 20%); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/project/dpad-logic.js: -------------------------------------------------------------------------------- 1 | function getTargetCoords(sourceCoords, direction) { 2 | var cx = sourceCoords.x, 3 | cy = sourceCoords.y; 4 | 5 | var panTargets = { 6 | left: { x: cx - 1, y: cy }, 7 | right: { x: cx + 1, y: cy }, 8 | up: { x: cx, y: cy - 1 }, 9 | down: { x: cx, y: cy + 1 } 10 | }; 11 | 12 | var target = panTargets[direction]; 13 | 14 | return target; 15 | } 16 | 17 | function verifyCoordsExist(pages, target) { 18 | if (pages.some(p => p.coords.x === target.x && p.coords.y === target.y)) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | } 24 | 25 | module.exports = { 26 | handleDirectionClick: function (event) { 27 | var target = getTargetCoords(this.state.zoomedPageCoords, event); 28 | 29 | if (verifyCoordsExist(this.state.pages, target)) { 30 | this.zoomToPage(target); 31 | } 32 | }, 33 | componentDidMount: function () { 34 | if (window) { 35 | window.addEventListener('resize', (event) => { 36 | this.setDPadStyle(); 37 | }); 38 | } 39 | }, 40 | componentDidUpdate: function () { 41 | this.setDPadStyle(); 42 | }, 43 | setDPadStyle: function () { 44 | // Check what directions have pages that exist and update the dpad UI accordingly 45 | if (this.state.isPageZoomed && this.state.pages.length) { 46 | this.refs.dpad.bulkSetVisibility({ 47 | showUp: verifyCoordsExist(this.state.pages, getTargetCoords(this.state.zoomedPageCoords, 'up')), 48 | showDown: verifyCoordsExist(this.state.pages, getTargetCoords(this.state.zoomedPageCoords, 'down')), 49 | showLeft: verifyCoordsExist(this.state.pages, getTargetCoords(this.state.zoomedPageCoords, 'left')), 50 | showRight: verifyCoordsExist(this.state.pages, getTargetCoords(this.state.zoomedPageCoords, 'right')) 51 | }); 52 | } 53 | 54 | if (this.state.params.mode === 'play' && this.state.isPageZoomed) { 55 | this.refs.dpad.positionAroundContainer(this.getPageBoundingRect()); 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/pages/project/form-pages.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PageBlock = require("./pageblock.jsx"); 3 | 4 | module.exports = { 5 | formPages: function() { 6 | return this.state.pages.map((page, index) => { 7 | var props = { 8 | page, 9 | selected: page.id === this.state.selectedEl, 10 | source: page.id === this.state.sourcePageID, 11 | target: page.id === this.state.selectedEl && this.state.params.mode === 'link', 12 | blurred: this.state.isPageZoomed && (page.coords.x !== this.state.zoomedPageCoords.x || page.coords.y !== this.state.zoomedPageCoords.y), 13 | transform: this.cartesian.getTransform(page.coords), 14 | interactive: this.state.isPageZoomed, 15 | onClick: this.onPageClick.bind(this, page) 16 | }; 17 | return ; 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/pages/project/loader.js: -------------------------------------------------------------------------------- 1 | var api = require('../../lib/api'); 2 | var platform = require('../../lib/platform'); 3 | var reportError = require('../../lib/errors'); 4 | var types = require('../../components/basic-element/basic-element.jsx').types; 5 | 6 | function findLandingPage(pages) { 7 | var result; 8 | // ... first, try to select 0, 0 9 | pages.forEach((page) => { 10 | if (page.coords.x === 0 && page.coords.y === 0) { 11 | result = page; 12 | } 13 | }); 14 | // ... and if it was deleted, select the first page in the array 15 | return result || pages[0]; 16 | } 17 | 18 | module.exports = { 19 | uri: function () { 20 | return `/users/${this.state.params.user}/projects/${this.state.params.project}/pages`; 21 | }, 22 | 23 | load: function () { 24 | this.setState({loading: true}); 25 | api({uri: this.uri()}, (err, data) => { 26 | 27 | this.setState({ 28 | loading: false, 29 | sourcePageID: false 30 | }); 31 | 32 | if (err) { 33 | reportError('Error loading project', err); 34 | } else if (!data || !data.pages) { 35 | reportError('No project found...'); 36 | } else { 37 | var state = { 38 | isFirstLoad: false 39 | }; 40 | var pages = this.formatPages(data.pages); 41 | 42 | // Set cartesian coordinates 43 | this.cartesian.allCoords = pages.map(el => el.coords); 44 | 45 | state.pages = pages; 46 | 47 | var landingPage = findLandingPage(pages); 48 | var {x, y} = this.cartesian.getFocusTransform(landingPage.coords, this.state.matrix[0]); 49 | 50 | if (this.state.params.mode === 'play' && typeof this.state.isFirstLoad) { 51 | this.zoomToPage(landingPage.coords); 52 | } else if (this.state.params.mode === 'edit' && !this.state.selectedEl) { 53 | state.selectedEl = landingPage.id; 54 | state.matrix = [this.state.matrix[0], 0, 0, this.state.matrix[0], x, y]; 55 | } else if (this.state.isFirstLoad) { 56 | state.matrix = [this.state.matrix[0], 0, 0, this.state.matrix[0], x, y]; 57 | } 58 | 59 | this.setState(state); 60 | 61 | // Highlight the source page if you're in link destination mode 62 | var java = platform.getAPI(); 63 | if (java) { 64 | var payloads = java.getPayloads("link-element"); 65 | if (payloads) { 66 | // We do not clear the payload, since it is also 67 | // required later on in setdestination.js 68 | try { 69 | var pageId; 70 | var list = JSON.parse(payloads); 71 | if (list.length > 0) { 72 | var entry = list[0]; 73 | if (entry.metadata) { 74 | var metadata = entry.metadata; 75 | pageId = metadata.pageID; 76 | this.highlightPage(pageId, 'source'); 77 | } else { 78 | console.error("no entry metadata available...", entry); 79 | } 80 | var element = types.link.spec.expand(entry.data); 81 | pageId = element.attributes.targetPageId; 82 | if (pageId) { 83 | this.highlightPage(pageId, 'selected'); 84 | } 85 | } else { 86 | console.error("link-element payloads was an empty array:" + payloads); 87 | } 88 | } catch (e) { 89 | console.error("malformed JSON found while loading in link-element data..."); 90 | } 91 | } 92 | } 93 | else if (this.state.params.mode === 'link') { 94 | this.highlightPage(this.state.routeData.pageID, 'source'); 95 | } 96 | } 97 | }); 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /src/pages/project/pageblock.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classNames = require('classnames'); 3 | var ElementGroup = require('../../components/element-group/element-group.jsx'); 4 | 5 | /** 6 | * This is the component used in the Project view that draws pages as 7 | * a "simple page representation" rather than the detailed representation 8 | * you get when you navigate to the Page view. 9 | */ 10 | var PageBlock = React.createClass({ 11 | 12 | render: function() { 13 | var classes = classNames('page-container', { 14 | selected: this.props.selected, 15 | unselected: this.props.unselected, 16 | source: this.props.source, 17 | target: this.props.target, 18 | blurred: this.props.blurred 19 | }); 20 | 21 | var style = { 22 | backgroundColor: this.props.page.styles.backgroundColor, 23 | transform: this.props.transform, 24 | WebkitTransform: this.props.transform 25 | }; 26 | 27 | // The "shim" and "indicator" divs don't actually house any content, 28 | // suggesting that either they are intended to be empty, or they will 29 | // be hooked into later, in which case we have failed to uphold React 30 | // practices and reverted to traditional HTML work, which would be bad. 31 | return (
32 |
33 |
34 |
35 | 36 | ); 38 | } 39 | }); 40 | 41 | module.exports = PageBlock; 42 | -------------------------------------------------------------------------------- /src/pages/project/remix.js: -------------------------------------------------------------------------------- 1 | var dispatcher = require('../../lib/dispatcher'); 2 | var api = require('../../lib/api'); 3 | 4 | module.exports = { 5 | componentDidMount: function() { 6 | // Handle remix calls from Android wrapper 7 | window.createRemix = () => { 8 | var uri = `/users/${this.state.params.user}/projects/${this.state.params.project}/remixes`; 9 | 10 | // Duplicate project via API call 11 | api({ 12 | method: 'post', 13 | uri: uri 14 | }, (err, data) => { 15 | if (err) { 16 | window.Platform.trackEvent('Remix', 'Remix Error', err); 17 | return console.error('Error remixing project', err); 18 | } 19 | 20 | var projectID = data.project.id; 21 | var projectTitle = data.project.title; 22 | 23 | // Get author's username 24 | api({ 25 | method: 'GET', 26 | uri: `/users/${this.state.params.user}/projects/${this.state.params.project}` 27 | }, (err, moreData) => { 28 | if (err) { 29 | window.Platform.trackEvent('Remix', 'Remix Error', err); 30 | return console.error('Error remixing project', err); 31 | } 32 | 33 | window.Platform.trackEvent('Remix', 'Remix Success', projectID); 34 | 35 | if (window.Platform) { 36 | window.Platform.setView( 37 | `/users/${this.state.user.id}/projects/${projectID}`, 38 | JSON.stringify({ 39 | isFreshRemix: true, 40 | title: projectTitle, 41 | originalAuthor: moreData.project.author.username 42 | }) 43 | ); 44 | } 45 | }); 46 | }); 47 | }; 48 | 49 | if (this.android && this.state.routeData.isFreshRemix) { 50 | // Notify user that THIS IS A REEEEEEMIXXXXX 51 | dispatcher.fire('modal-confirm:show', { 52 | config: { 53 | header: 'Project Remix', 54 | body: `This is your copy of ${this.state.routeData.title}. You can add or change anything. The original will stay the same. Have fun!`, 55 | attribution: this.state.routeData.originalAuthor, 56 | icon: 'tinker.png', 57 | buttons: [{ 58 | text: this.getIntlMessage('got_it') 59 | }] 60 | } 61 | }); 62 | 63 | // Prepend "Remix of..." to project name 64 | 65 | var remixTitle = this.state.routeData.title; 66 | 67 | if (!remixTitle.match(/^Remix of/)) { 68 | remixTitle = 'Remix of ' + remixTitle; 69 | } 70 | 71 | api({ 72 | method: 'PATCH', 73 | uri: `/users/${this.state.user.id}/projects/${this.state.params.project}`, 74 | json: { 75 | title: remixTitle 76 | } 77 | }, function (err, body) { 78 | if (err) { 79 | window.Platform.trackEvent('Remix', 'Remix Rename Error', this.state.params.project); 80 | console.error('Could not update project settings.'); 81 | } 82 | }); 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/pages/project/renderhelpers.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var assign = require('react/lib/Object.assign'); 3 | var {SecondaryButton} = require('../../components/action-menu/action-menu.jsx'); 4 | 5 | module.exports = { 6 | getContainerStyle: function() { 7 | return { 8 | width: this.cartesian.width + 'px', 9 | height: this.cartesian.height + 'px' 10 | }; 11 | }, 12 | 13 | getBoundingStyle: function() { 14 | var transformString = `matrix(${this.state.matrix.join(', ')})`; 15 | return assign({ 16 | transform: transformString, 17 | WebkitTransform: transformString, 18 | opacity: this.state.pages.length ? 1 : 0 19 | }, this.cartesian.getBoundingSize() ); 20 | }, 21 | 22 | getPageURL: function(params, selectedEl) { 23 | return `/users/${params.user}/projects/${params.project}/pages/${selectedEl}`; 24 | }, 25 | 26 | generateAddContainers: function(isPlayOnly) { 27 | if (isPlayOnly) { 28 | return false; 29 | } 30 | 31 | return this.cartesian.edges.map(coords => { 32 | var transformString = this.cartesian.getTransform(coords); 33 | return (
37 | 38 |
); 39 | }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/pages/project/setdestination.js: -------------------------------------------------------------------------------- 1 | var types = require('../../components/basic-element/basic-element.jsx').types; 2 | var api = require('../../lib/api'); 3 | var platform = require('../../lib/platform'); 4 | var reportError = require('../../lib/errors'); 5 | var dispatcher = require('../../lib/dispatcher'); 6 | 7 | module.exports = { 8 | componentDidMount: function () { 9 | // Handle button actions 10 | dispatcher.on('linkClicked', (event) => { 11 | if (event.props.targetWebURL) { 12 | if (window.Platform) { 13 | window.Platform.openExternalUrl(event.props.targetWebURL); 14 | } else { 15 | document.location.href = event.props.targetWebURL; 16 | } 17 | } else { 18 | if (event.props.targetPageId && this.state.isPageZoomed) { 19 | this.zoomToPage( this.pageIdToCoords(event.props.targetPageId) ); 20 | } else { 21 | this.highlightPage(event.props.targetPageId, 'selected'); 22 | } 23 | } 24 | }); 25 | }, 26 | 27 | componentWillMount: function () { 28 | var java = platform.getAPI(); 29 | 30 | if (java) { 31 | this.props.update({ 32 | onBackPressed: () => { 33 | java.clearPayloads(`link-element`); 34 | window.Platform.goBack(); 35 | } 36 | }); 37 | } 38 | }, 39 | 40 | setDestination: function () { 41 | var java = platform.getAPI(); 42 | 43 | // If we have java caching available, there should be a link-element in cache 44 | if(java) { 45 | var payloads = java.getPayloads("link-element"); 46 | java.clearPayloads("link-element"); 47 | 48 | var list = JSON.parse(payloads); 49 | var element = types.link.spec.expand(list[0].data); 50 | element.attributes.targetPageId = this.state.selectedEl; 51 | element.attributes.targetProjectId = this.state.params.project; 52 | element.attributes.targetUserId = this.state.params.user; 53 | element.attributes.targetWebURL = ``; // Remove web url 54 | 55 | var serialized = JSON.stringify({ 56 | data: element 57 | }); 58 | java.queue("edit-element", serialized); 59 | platform.goBack(); 60 | } else { 61 | console.warn(`This feature only works in Android.`); 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/pages/project/transforms.js: -------------------------------------------------------------------------------- 1 | var Panzoom = require('panzoom'); 2 | 3 | var MAX_ZOOM = 0.5; 4 | var MIN_ZOOM = 0.18; 5 | 6 | module.exports = { 7 | componentDidMount: function () { 8 | var pz = new Panzoom(this.refs.bounding, { 9 | minScale: MIN_ZOOM, 10 | maxScale: MAX_ZOOM, 11 | onEnd: (e) => { 12 | var matrix = pz.getMatrix(); 13 | this.setState({matrix}); 14 | } 15 | }); 16 | this.panzoom = pz; 17 | if (this.state.isPageZoomed) { 18 | pz.disable(); 19 | } 20 | }, 21 | componentDidUpdate: function (prevProps, prevState) { 22 | 23 | // Disable panzoom if state goes from zoomedIn to not zoomedIn 24 | if (this.panzoom && this.state.isPageZoomed && !prevState.isPageZoomed) { 25 | this.panzoom.disable(); 26 | } 27 | 28 | // Disable if state goes from zoomedIn to not zoomedIn 29 | if (this.panzoom && !this.state.isPageZoomed && prevState.isPageZoomed) { 30 | this.panzoom.enable(); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/style-guide/style-guide.less: -------------------------------------------------------------------------------- 1 | #style-guide { 2 | padding: 30px 0; 3 | background: linear-gradient(180deg, #141C29, #31325F); 4 | 5 | > .wrapper { 6 | padding: 20px 30px; 7 | margin: 0 auto; 8 | max-width: 960px; 9 | background: #F5F4FF; 10 | border-radius: 4px; 11 | } 12 | 13 | code { 14 | background: @slate; 15 | color: @white; 16 | padding: 0.2em 0.5em; 17 | margin: 0 10px; 18 | font-weight: 100; 19 | font-size: 0.8em; 20 | } 21 | 22 | .pattern { 23 | border: 1px solid @heatherGrey; 24 | border-radius: 2px; 25 | margin: 0 0 25px 0; 26 | 27 | .title { 28 | padding: 14px; 29 | margin: 0 0 10px 0; 30 | text-transform: uppercase; 31 | font-size: 14px; 32 | line-height: 15px; 33 | letter-spacing: 0.12rem; 34 | color: #2B3747; 35 | background: @heatherGrey; 36 | } 37 | .content { 38 | padding: 0 10px 10px 10px; 39 | } 40 | } 41 | 42 | .swatches { 43 | .color { 44 | color: @white; 45 | margin-bottom: 5px; 46 | font-family: monospace; 47 | font-size: 14px; 48 | 49 | span { 50 | display: inline-block; 51 | background: rgba(0,0,0,0.75); 52 | padding: 10px; 53 | border-right: 1px solid rgba(0,0,0,0.8); 54 | border-top-right-radius: 10px; 55 | } 56 | 57 | &.blue { 58 | background: @blue; 59 | } 60 | &.shadowBlue { 61 | background: @shadowBlue; 62 | } 63 | &.sapphire { 64 | background: @sapphire; 65 | } 66 | &.teal { 67 | background: @teal; 68 | } 69 | &.slate { 70 | background: @slate; 71 | } 72 | &.darkSlate { 73 | background: @darkSlate; 74 | } 75 | &.heatherGrey { 76 | background: @heatherGrey; 77 | } 78 | &.lightGrey { 79 | background: @lightGrey; 80 | } 81 | &.plum { 82 | background: @plum; 83 | } 84 | &.shadowPlum { 85 | background: @shadowPlum; 86 | } 87 | &.brick { 88 | background: @brick; 89 | } 90 | &.softGrey { 91 | background: @softGrey; 92 | } 93 | &.aqua { 94 | background: @aqua; 95 | } 96 | &.green { 97 | background: @green; 98 | } 99 | &.yellow { 100 | background: @yellow; 101 | } 102 | &.orange { 103 | background: @orange; 104 | } 105 | &.purple { 106 | background: @purple; 107 | } 108 | } 109 | } 110 | 111 | .alert.hidden { 112 | // Alerts are hidden by default, but we want to see one in the style guide... 113 | display: flex; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/pages/tag-list/tag-list.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var api = require('../../lib/api'); 3 | var reportError = require('../../lib/errors'); 4 | var i18n = require('../../lib/i18n'); 5 | var render = require('../../lib/render.jsx'); 6 | var platform = require('../../lib/platform'); 7 | 8 | var ProjectList = require('../../components/project-list/project-list.jsx'); 9 | 10 | var lang = i18n.isSupportedLanguage(i18n.currentLanguage) ? i18n.currentLanguage : i18n.defaultLang; 11 | 12 | var TagList = React.createClass({ 13 | mixins: [ 14 | require('react-intl').IntlMixin, 15 | require('../../lib/router') 16 | ], 17 | goBack: function () { 18 | platform.goBack(); 19 | }, 20 | render: function () { 21 | return ( 22 |
23 |
24 |
25 | #{this.state.params.tag} 26 |
27 |
28 | 29 |
30 | ); 31 | } 32 | }); 33 | 34 | render(TagList); 35 | -------------------------------------------------------------------------------- /src/pages/tag-list/tag-list.less: -------------------------------------------------------------------------------- 1 | #tag-list { 2 | header { 3 | width: 90%; 4 | margin: auto; 5 | 6 | p { 7 | color: darken(@heatherGrey, 25%); 8 | margin-bottom: 0; 9 | font-size: 12px; 10 | } 11 | } 12 | .tag-name { 13 | color: @slate; 14 | font-size: 36px; 15 | margin: 20px 0; 16 | line-height: 1.5; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/tinker/tinker.less: -------------------------------------------------------------------------------- 1 | 2 | #tinker { 3 | @_spectrum-pickerHeight: calc(~"100vh -" @editorPreviewHeight + @color-preview-height + @tabs_total-height + @editor-form-group_margin-bottom + @range_total-height + @element-label_margin-bottom + @editor-options-font-size*1.5); //line height of a label 4 | @_bg: #31344F; 5 | @_bgLight: #414468; 6 | @_greenText: #98FE90; 7 | @color-preview-height: 55px; 8 | @editorPreviewHeight: 140px; 9 | @editor-options-font-size: 15px; 10 | 11 | background-color: @_bg; 12 | color: @white; 13 | font-size: @editor-options-font-size; 14 | 15 | height: 100%; 16 | overflow: hidden; 17 | 18 | .editor-flex-container { 19 | height: 100%; 20 | overflow: hidden; 21 | } 22 | 23 | .editor-options { 24 | flex-grow: 1; 25 | overflow: scroll; 26 | padding: 0 20px 10px 20px; 27 | margin-top: 0; 28 | } 29 | .editor-scroll{ 30 | background-image: url("./img/scrubber-white.svg"); 31 | margin-right: -20px; 32 | } 33 | 34 | .editor-preview, 35 | .color-preview { 36 | flex-shrink: 0; 37 | } 38 | 39 | .editor-preview { 40 | height: @editorPreviewHeight; 41 | button.debug { 42 | position: absolute; 43 | color: #333; 44 | } 45 | } 46 | 47 | .tab-content{ 48 | margin-top: @tabs_total-height; 49 | } 50 | 51 | .range { 52 | color: @_greenText; 53 | input[type=range] { 54 | background: #414468; 55 | } 56 | input[type=range]::-webkit-slider-thumb { 57 | box-shadow: 0px 1px 0 1px darken(@_bg, 4%); 58 | } 59 | } 60 | 61 | .color-preview { 62 | font-size: 14px; 63 | padding: 10px 20px; 64 | background: @_bgLight; 65 | display: flex; 66 | align-items: center; 67 | margin-top: 140px; 68 | height: @color-preview-height; 69 | .color-preview-right { 70 | flex-grow: 1; 71 | display: flex; 72 | justify-content: flex-end; 73 | } 74 | .color-preview-swatch { 75 | position: relative; 76 | height: 35px; 77 | width: 48px; 78 | border-radius: 2px; 79 | background: url(./img/gray_and_white_checkers.gif); 80 | > div { 81 | position: absolute; 82 | top: 0; 83 | left: 0; 84 | bottom: 0; 85 | right: 0; 86 | border-radius: 2px; 87 | } 88 | } 89 | } 90 | .spectrum-container{ 91 | .saturation{ 92 | height: @_spectrum-pickerHeight; 93 | } 94 | .hue{ 95 | width: @_spectrum-pickerHeight; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/pages/user-projects/user-projects.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var router = require('../../lib/router'); 3 | 4 | var render = require('../../lib/render.jsx'); 5 | var ProjectList = require ('../../components/project-list/project-list.jsx'); 6 | 7 | var UserProjects = React.createClass({ 8 | mixins: [router,require('react-intl').IntlMixin], 9 | render: function () { 10 | return ( 11 | 12 | ); 13 | } 14 | }); 15 | 16 | render(UserProjects); 17 | -------------------------------------------------------------------------------- /src/pages/user-projects/user-projects.less: -------------------------------------------------------------------------------- 1 | #userProjects { 2 | display: block; 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | padding: 20px 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/web-link/web-link.less: -------------------------------------------------------------------------------- 1 | #webLink { 2 | padding: 20px; 3 | 4 | h4 { 5 | margin-bottom: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/static/fonts/FiraSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/fonts/FiraSans-Medium.woff -------------------------------------------------------------------------------- /src/static/fonts/FiraSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/fonts/FiraSans-Regular.woff -------------------------------------------------------------------------------- /src/static/fonts/lamson-sharpie-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/fonts/lamson-sharpie-webfont.woff -------------------------------------------------------------------------------- /src/static/img/B-white.svg: -------------------------------------------------------------------------------- 1 | B -------------------------------------------------------------------------------- /src/static/img/B.svg: -------------------------------------------------------------------------------- 1 | B -------------------------------------------------------------------------------- /src/static/img/I-white.svg: -------------------------------------------------------------------------------- 1 | I -------------------------------------------------------------------------------- /src/static/img/I.svg: -------------------------------------------------------------------------------- 1 | I -------------------------------------------------------------------------------- /src/static/img/U-white.svg: -------------------------------------------------------------------------------- 1 | U -------------------------------------------------------------------------------- /src/static/img/U.svg: -------------------------------------------------------------------------------- 1 | U -------------------------------------------------------------------------------- /src/static/img/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/add.png -------------------------------------------------------------------------------- /src/static/img/align-center.svg: -------------------------------------------------------------------------------- 1 | align-center -------------------------------------------------------------------------------- /src/static/img/align-left.svg: -------------------------------------------------------------------------------- 1 | align-left -------------------------------------------------------------------------------- /src/static/img/align-right.svg: -------------------------------------------------------------------------------- 1 | align-right -------------------------------------------------------------------------------- /src/static/img/avatar-icon.svg: -------------------------------------------------------------------------------- 1 | avatar-iconCreated with Sketch. -------------------------------------------------------------------------------- /src/static/img/back-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/back-arrow.png -------------------------------------------------------------------------------- /src/static/img/brush.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/img/camera-gallery.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/img/camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/img/caret-down.svg: -------------------------------------------------------------------------------- 1 | Triangle 1 -------------------------------------------------------------------------------- /src/static/img/cc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/static/img/change-image.svg: -------------------------------------------------------------------------------- 1 | change-image -------------------------------------------------------------------------------- /src/static/img/curved-arrow-wide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/static/img/curved-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/static/img/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Thumbnail 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/static/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/demo.png -------------------------------------------------------------------------------- /src/static/img/external-url.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/img/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/static/img/gray_and_white_checkers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/gray_and_white_checkers.gif -------------------------------------------------------------------------------- /src/static/img/link.svg: -------------------------------------------------------------------------------- 1 | linkCreated with Sketch. -------------------------------------------------------------------------------- /src/static/img/material-check-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/material-check-on.png -------------------------------------------------------------------------------- /src/static/img/material-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/material-check.png -------------------------------------------------------------------------------- /src/static/img/more-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/static/img/nub-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/static/img/nub.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/static/img/oval-droplet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/static/img/oval-flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/static/img/oval-source.svg: -------------------------------------------------------------------------------- 1 | source-iconCreated with Sketch. -------------------------------------------------------------------------------- /src/static/img/page-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/page-screenshot.png -------------------------------------------------------------------------------- /src/static/img/palette-icon.svg: -------------------------------------------------------------------------------- 1 | android-color-palette - IoniconsCreated with Sketch. -------------------------------------------------------------------------------- /src/static/img/pencil-blue.svg: -------------------------------------------------------------------------------- 1 | pencilCreated with Sketch. -------------------------------------------------------------------------------- /src/static/img/pencil.svg: -------------------------------------------------------------------------------- 1 | Untitled 3 -------------------------------------------------------------------------------- /src/static/img/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/img/scrubber-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 773 Copy 57 + Rectangle 773 Copy 31 + Rectangle 773 Copy 32 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/static/img/scrubber.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 773 Copy 57 + Rectangle 773 Copy 31 + Rectangle 773 Copy 32 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/static/img/settings.svg: -------------------------------------------------------------------------------- 1 | settings -------------------------------------------------------------------------------- /src/static/img/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/slider.png -------------------------------------------------------------------------------- /src/static/img/take-photo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/img/text.svg: -------------------------------------------------------------------------------- 1 | Aa -------------------------------------------------------------------------------- /src/static/img/tinker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/webmaker-core/69fd62ea9bdcc14fcb71575393ff95baa5e6f889/src/static/img/tinker.png -------------------------------------------------------------------------------- /src/static/img/toucan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/static/img/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/img/web-search.svg: -------------------------------------------------------------------------------- 1 | web-searchCreated with Sketch.Web Search -------------------------------------------------------------------------------- /src/static/img/webmaker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/static/img/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/img/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/static/img/zoom-out-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/static/img/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/tests/color-spectrum.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var React = require('react'); 3 | 4 | // Polyfill Intl for node -- some versions (>=10) don't support the Intl API 5 | var areIntlLocalesSupported = require('intl-locales-supported'); 6 | var localesMyAppSupports = ['en-US']; 7 | if (global.Intl) { 8 | if (!areIntlLocalesSupported(localesMyAppSupports)) { 9 | require('intl'); 10 | Intl.NumberFormat = IntlPolyfill.NumberFormat; 11 | Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; 12 | } 13 | } else { 14 | global.Intl = require('intl'); 15 | } 16 | 17 | var ColorSpectrum = require('../components/color-spectrum/color-spectrum.jsx'); 18 | 19 | describe('ColorSpectrum', function() { 20 | describe("getColor()", function() { 21 | var getColor = ColorSpectrum.prototype.getColor; 22 | 23 | it('returns color if it is well-formed', function() { 24 | getColor('rgba(255, 0, 0, 0)', 'rgba(0, 0, 0, 0)').rgbaString() 25 | .should.eql('rgba(255, 0, 0, 0)'); 26 | }); 27 | 28 | it('returns default color if color is malformed', function() { 29 | getColor('aweg', 'rgba(0, 0, 0, 0)').rgbaString() 30 | .should.eql('rgba(0, 0, 0, 0)'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/tests/jsonUtils.test.js: -------------------------------------------------------------------------------- 1 | var {jsonToFormEncoded, parseJSON} = require('../lib/jsonUtils'); 2 | var should = require('should'); 3 | 4 | describe('jsonUtils', function () { 5 | 6 | describe('#jsonToFormEncoded', function () { 7 | [ 8 | [ 9 | {foo: 'bar'}, 10 | 'foo=bar' 11 | ], 12 | [ 13 | {foo: 'bar', bar: 'baz'}, 14 | 'foo=bar&bar=baz' 15 | ], 16 | [ 17 | {a: 'a@b.com', 'f_%2': 'b'}, 18 | 'a=a%40b.com&f_%252=b' 19 | ] 20 | ].forEach(test => { 21 | it(`should convert ${JSON.stringify(test[0])}`, function () { 22 | should(jsonToFormEncoded(test[0])).equal(test[1]); 23 | }); 24 | }); 25 | }); 26 | 27 | describe('#parseJson', function () { 28 | [ 29 | [ 30 | {foo: 'bar'}, 31 | {foo: 'bar'} 32 | ], 33 | [ 34 | '{"foo": "bar"}', 35 | {foo: 'bar'} 36 | ], 37 | [ 38 | undefined, 39 | {} 40 | ], 41 | [ 42 | null, 43 | {} 44 | ], 45 | [ 46 | '', 47 | {} 48 | ], 49 | [ 50 | 'fooinvalid', 51 | {} 52 | ] 53 | ].forEach(test => { 54 | it(`should convert ${typeof test[0] === 'object' ? JSON.stringify(test[0]) : test[0]}`, function () { 55 | should.deepEqual(parseJSON(test[0]), (test[1])); 56 | }); 57 | }); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /src/variables.less: -------------------------------------------------------------------------------- 1 | // Action/+ menu 2 | @ZINDEX_ACTION_OVERLAY: 999; 3 | 4 | // Set link destination button 5 | @ZINDEX_META_BUTTON: @ZINDEX_ACTION_OVERLAY - 2; 6 | 7 | // Loading indicator 8 | @ZINDEX_LOADING: 10000; 9 | 10 | @ZINDEX_MAX: 2147483647; /* signed 32bit maxint */ 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var getPages = require('./npm_tasks/get-pages'); 2 | var path = require('path'); 3 | 4 | // Prep all entry points 5 | var entry = {}; 6 | getPages().forEach(function (page) { 7 | entry[page] = './src/pages/' + page + '/' + page + '.jsx'; 8 | }); 9 | 10 | module.exports = { 11 | entry: entry, 12 | devtool: 'source-map', //not good for ff 13 | output: { 14 | path: __dirname + '/dest/js', 15 | filename: '[name].bundle.js' 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.js$/, 21 | loaders: ['babel-loader'], 22 | include: path.resolve(__dirname, 'src') 23 | }, 24 | { 25 | test: /\.jsx$/, 26 | loaders: ['babel-loader', 'jsx-loader'], 27 | include: path.resolve(__dirname, 'src') 28 | }, 29 | { 30 | test: /\.json$/, 31 | loader: 'json-loader', 32 | include: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')] 33 | } 34 | ] 35 | } 36 | }; 37 | --------------------------------------------------------------------------------