diff --git a/single-page-app-exercise/.bowerrc b/single-page-app-exercise/.bowerrc new file mode 100644 index 0000000..69fad35 --- /dev/null +++ b/single-page-app-exercise/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} diff --git a/single-page-app-exercise/.editorconfig b/single-page-app-exercise/.editorconfig new file mode 100644 index 0000000..8a80734 --- /dev/null +++ b/single-page-app-exercise/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/single-page-app-exercise/.gitattributes b/single-page-app-exercise/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/single-page-app-exercise/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/single-page-app-exercise/.gitignore b/single-page-app-exercise/.gitignore new file mode 100644 index 0000000..dc5576c --- /dev/null +++ b/single-page-app-exercise/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/dist +/.tmp +/.sass-cache +/bower_components diff --git a/single-page-app-exercise/.jshintrc b/single-page-app-exercise/.jshintrc new file mode 100644 index 0000000..0469732 --- /dev/null +++ b/single-page-app-exercise/.jshintrc @@ -0,0 +1,21 @@ +{ + "bitwise": true, + "browser": true, + "curly": true, + "eqeqeq": true, + "esnext": true, + "latedef": true, + "noarg": true, + "node": true, + "strict": true, + "undef": true, + "unused": true, + "quotmark": "single", + "indent": 4, + "globals": { + "angular": false, + "$": false, + "singlePageAppExerciseApp": true, + "FastClick": true + } +} diff --git a/single-page-app-exercise/.travis.yml b/single-page-app-exercise/.travis.yml new file mode 100644 index 0000000..6fb6850 --- /dev/null +++ b/single-page-app-exercise/.travis.yml @@ -0,0 +1,9 @@ +sudo: false +language: node_js +node_js: + - 'iojs' + - '0.12' + - '0.10' +before_script: + - 'npm install -g bower grunt-cli' + - 'bower install' diff --git a/single-page-app-exercise/.yo-rc.json b/single-page-app-exercise/.yo-rc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/single-page-app-exercise/.yo-rc.json @@ -0,0 +1 @@ +{} diff --git a/single-page-app-exercise/Gruntfile.js b/single-page-app-exercise/Gruntfile.js new file mode 100644 index 0000000..7d30fe3 --- /dev/null +++ b/single-page-app-exercise/Gruntfile.js @@ -0,0 +1,617 @@ +// Generated on 2015-09-28 using generator-angular 0.12.1 +'use strict'; + +// # Globbing +// for performance reasons we're only matching one level down: +// 'test/spec/{,*/}*.js' +// use this if you want to recursively match all subfolders: +// 'test/spec/**/*.js' + +module.exports = function (grunt) { + + // Time how long tasks take. Can help when optimizing build times + require('time-grunt')(grunt); + + // Automatically load required Grunt tasks + require('jit-grunt')(grunt, { + useminPrepare: 'grunt-usemin', + ngtemplates: 'grunt-angular-templates', + cdnify: 'grunt-google-cdn' + }); + + // Configurable paths for the application + var appConfig = { + app: require('./bower.json').appPath || 'app', + dist: 'dist' + }; + + // Define the configuration for all the tasks + grunt.initConfig({ + + // Project settings + yeoman: appConfig, + + // Watches files for changes and runs tasks based on the changed files + watch: { + bower: { + files: ['bower.json'], + tasks: ['wiredep'] + }, + js: { + files: ['<%= yeoman.app %>/{components,modules}/**/*.js'], + tasks: ['newer:jshint:all', 'injector:scripts'], + options: { + livereload: '<%= connect.options.livereload %>' + } + }, + jsTest: { + files: ['test/unit/{,*/}*.js', 'test/midway/{,*/}*.js'], + tasks: ['newer:jshint:test', 'karma'] + }, + // * Introduced Sass + // styles: { + // files: ['<%= yeoman.app %>/styles/{,*/}*.css'], + // tasks: ['newer:copy:styles', 'autoprefixer'] + // }, + sass: { + files: ['<%= yeoman.app %>/{components,modules}/**/*.{scss,sass}'], + tasks: ['sass'] + }, + html: { + files: ['<%= yeoman.app %>/{components,modules}/**/*.html'], + tasks: ['ngtemplates'] + }, + gruntfile: { + files: ['Gruntfile.js'] + }, + livereload: { + files: [ + // '<%= yeoman.app %>/{,*/}*.html', + '.tmp/templates.js', + '.tmp/styles/{,*/}*.css', + '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' + ], + options: { + livereload: '<%= connect.options.livereload %>' + } + } + }, + + // The actual grunt server settings + connect: { + options: { + port: 9000, + // Change this to '0.0.0.0' to access the server from outside. + hostname: '0.0.0.0', + livereload: 35729 + }, + livereload: { + options: { + open: true, + middleware: function (connect) { + return [ + connect.static('.tmp'), + connect().use( + '/bower_components', + connect.static('./bower_components') + ), + connect.static(appConfig.app) + ]; + } + } + }, + test: { + options: { + port: 9001, + middleware: function (connect) { + return [ + connect.static('.tmp'), + connect.static('test'), + connect().use( + '/bower_components', + connect.static('./bower_components') + ), + connect.static(appConfig.app) + ]; + } + } + }, + dist: { + options: { + open: true, + base: '<%= yeoman.dist %>' + } + } + }, + + // Make sure code styles are up to par and there are no obvious mistakes + jshint: { + options: { + jshintrc: '.jshintrc', + reporter: require('jshint-stylish') + }, + all: { + src: [ + 'Gruntfile.js', + '<%= yeoman.app %>/{components,modules}/**/*.js' + ] + }, + test: { + options: { + jshintrc: 'test/.jshintrc' + }, + src: ['test/unit/{,*/}*.js', 'test/midway/{,*/}*.js'] + } + }, + + // Empties folders to start fresh + clean: { + dist: { + files: [{ + dot: true, + src: [ + '.tmp', + '<%= yeoman.dist %>/{,*/}*', + '!<%= yeoman.dist %>/.git{,*/}*' + ] + }] + }, + server: '.tmp' + }, + + // Add vendor prefixed styles + // * I don't use this because Compass takes care it for you. + // autoprefixer: { + // options: { + // browsers: ['last 1 version'] + // }, + // server: { + // options: { + // map: true, + // }, + // files: [{ + // expand: true, + // cwd: '.tmp/styles/', + // src: '{,*/}*.css', + // dest: '.tmp/styles/' + // }] + // }, + // dist: { + // files: [{ + // expand: true, + // cwd: '.tmp/styles/', + // src: '{,*/}*.css', + // dest: '.tmp/styles/' + // }] + // } + // }, + + // Automatically inject Bower components into the app + wiredep: { + app: { + src: ['<%= yeoman.app %>/index.html'], + ignorePath: /\.\.\//, + exclude: [ + /json3/, + /es5-shim/, + /font-awesome.css/, + /foundation.css/, + /foundation.js/, + /foundation-apps.css/ + ] + }, + test: { + devDependencies: true, + src: '<%= karma.unit.configFile %>', + ignorePath: /\.\.\//, + fileTypes:{ + js: { + block: /(([\s\t]*)\/{2}\s*?bower:\s*?(\S*))(\n|\r|.)*?(\/{2}\s*endbower)/gi, + detect: { + js: /'(.*\.js)'/gi + }, + replace: { + js: '\'{{filePath}}\',' + } + } + }, + exclude: [ + /es5-shim/ + ] + }, + sass: { + src: ['<%= yeoman.app %>/components/global/styles/main.scss'], + ignorePath: /(\.\.\/){1,4}bower_components\//, + exclude: [ + /bootstrap-sass/ + ] + } + }, + + // Automatically inject development components into the app + injector: { + // Inject script components into index.html + scripts: { + options: { + transform: function(filePath){ + filePath = filePath.replace('/app/', ''); + filePath = filePath.replace('/.tmp/', ''); + return ''; + }, + starttag: '', + endtag: '' + }, + files: { + '<%= yeoman.app %>/index.html': [ + '{.tmp,<%= yeoman.app %>}/{components,modules}/**/*.js', + '!<%= yeoman.app %>/components/app.js', + '!<%= yeoman.app %>/components/global/configs.js', + '!<%= yeoman.app %>/components/global/runs.js' + ] + } + }, + // Inject script components into test configuration file + scriptsTest: { + options: { + transform: function(filePath){ + filePath = filePath.replace('/app/', 'app/'); + return '\'' + filePath + '\','; + }, + starttag: '// injector:js', + endtag: '// endinjector' + }, + files: { + 'test/karma.conf.js': [ + '{.tmp,<%= yeoman.app %>}/{components,modules}/**/*.js', + '!<%= yeoman.app %>/components/app.js', + '!<%= yeoman.app %>/components/global/configs.js', + '!<%= yeoman.app %>/components/global/runs.js' + ] + } + }, + // Inject html components into test configuration file + htmlTest: { + options: { + transform: function(filePath){ + filePath = filePath.replace('/app/', 'app/'); + return '\'' + filePath + '\','; + }, + starttag: '// injector:html', + endtag: '// endinjector:html' + }, + files: { + 'test/karma.conf.js': [ + '<%= yeoman.app %>/{components,modules}/**/*.html' + ] + } + }, + // Inject _settings.scss components into main.scss + settingsSass: { + options: { + transform: function(filePath){ + filePath = filePath.replace('/app/components/', ''); + filePath = filePath.replace('/app/modules/', ''); + filePath = filePath.replace('_settings.scss', 'settings'); + return '@import \'' + filePath + '\';'; + }, + starttag: '// injector:settings', + endtag: '// endinjector:settings' + }, + files: { + '<%= yeoman.app %>/components/global/styles/main.scss': [ + '<%= yeoman.app %>/{components,modules}/**/_settings.scss', + '!<%= yeoman.app %>/components/global/styles/_settings.scss' + ] + } + }, + // Inject _styles.scss components into main.scss + stylesSass: { + options: { + transform: function(filePath){ + filePath = filePath.replace('/app/components/', ''); + filePath = filePath.replace('/app/modules/', ''); + filePath = filePath.replace('_styles.scss', 'styles'); + return '@import \'' + filePath + '\';'; + }, + starttag: '// injector:styles', + endtag: '// endinjector:styles' + }, + files: { + '<%= yeoman.app %>/components/global/styles/main.scss': [ + '<%= yeoman.app %>/{components,modules}/**/_styles.scss', + '!<%= yeoman.app %>/components/global/styles/_styles.scss' + ] + } + } + }, + + // Compiles Sass to CSS and generates necessary files if requested + sass: { + server: { + options: { + loadPath: [ + '<%= yeoman.app %>/components', + '<%= yeoman.app %>/modules', + './bower_components' + ], + compass: true + }, + files: { + '.tmp/styles/main.css': '<%= yeoman.app %>/components/global/styles/main.scss' + } + } + }, + + // Renames files for browser caching purposes + filerev: { + dist: { + src: [ + '<%= yeoman.dist %>/scripts/{,*/}*.js', + '<%= yeoman.dist %>/styles/{,*/}*.css', + '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', + '<%= yeoman.dist %>/fonts/*' + ] + } + }, + + // Reads HTML for usemin blocks to enable smart builds that automatically + // concat, minify and revision files. Creates configurations in memory so + // additional tasks can operate on them + useminPrepare: { + html: '<%= yeoman.app %>/index.html', + options: { + dest: '<%= yeoman.dist %>', + flow: { + html: { + steps: { + js: ['concat', 'uglifyjs'], + css: ['cssmin'] + }, + post: {} + } + } + } + }, + + // Performs rewrites based on filerev and the useminPrepare configuration + usemin: { + html: ['<%= yeoman.dist %>/{,*/}*.html'], + css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], + js: ['<%= yeoman.dist %>/scripts/{,*/}*.js'], + options: { + assetsDirs: [ + '<%= yeoman.dist %>', + '<%= yeoman.dist %>/images', + '<%= yeoman.dist %>/styles' + ], + patterns: { + js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']] + } + } + }, + + // The following *-min tasks will produce minified files in the dist folder + // By default, your `index.html`'s will take care of + // minification. These next options are pre-configured if you do not wish + // to use the Usemin blocks. + // cssmin: { + // dist: { + // files: { + // '<%= yeoman.dist %>/styles/main.css': [ + // '.tmp/styles/{,*/}*.css' + // ] + // } + // } + // }, + // uglify: { + // dist: { + // files: { + // '<%= yeoman.dist %>/scripts/scripts.js': [ + // '<%= yeoman.dist %>/scripts/scripts.js' + // ] + // } + // } + // }, + // concat: { + // dist: {} + // }, + + imagemin: { + dist: { + files: [{ + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '{,*/}*.{png,jpg,jpeg,gif}', + dest: '<%= yeoman.dist %>/images' + }] + } + }, + + svgmin: { + dist: { + files: [{ + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '{,*/}*.svg', + dest: '<%= yeoman.dist %>/images' + }] + } + }, + + htmlmin: { + dist: { + options: { + collapseWhitespace: true, + conservativeCollapse: true, + collapseBooleanAttributes: true, + removeCommentsFromCDATA: true + }, + files: [{ + expand: true, + cwd: '<%= yeoman.dist %>', + src: ['*.html'], + dest: '<%= yeoman.dist %>' + }] + } + }, + + ngtemplates: { + options: { + module: 'singlePageAppExerciseApp' + }, + main: { + cwd: '<%= yeoman.app %>', + src: '{components,modules}/**/*.html', + dest: '.tmp/templates.js', + } + }, + + // ng-annotate tries to make the code safe for minification automatically + // by using the Angular long form for dependency injection. + ngAnnotate: { + dist: { + files: [{ + expand: true, + cwd: '.tmp/concat/scripts', + src: '*.js', + dest: '.tmp/concat/scripts' + }] + } + }, + + // Replace Google CDN references + cdnify: { + dist: { + html: ['<%= yeoman.dist %>/*.html'] + } + }, + + // Copies remaining files to places other tasks can use + copy: { + foundation: { + files: [{ + expand: true, + dot: true, + cwd: 'bower_components/foundation-apps/js/angular/components', + dest: '<%= yeoman.app %>/components', + src: [ + '**/*.html' + ] + }] + }, + dist: { + files: [{ + expand: true, + dot: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>', + src: [ + '*.{ico,png,txt}', + '.htaccess', + '*.html', + 'images/{,*/}*.{webp}', + 'fonts/{,*/}*.*' + ] + }, { + expand: true, + cwd: '.tmp/images', + dest: '<%= yeoman.dist %>/images', + src: [ 'generated/*' ] + }, { + expand: true, + cwd: 'bower_components/angular-ui-grid', + src: [ '*.{eot,svg,ttf,woff}' ], + dest: '<%= yeoman.dist %>/styles' + }] + } + }, + + // Run some tasks in parallel to speed up the build process + concurrent: { + server: [ + 'sass', + 'ngtemplates' + ], + test: [ + 'sass', + 'ngtemplates' + ], + dist: [ + 'sass', + 'imagemin', + 'svgmin', + 'ngtemplates' + ] + }, + + // Test settings + karma: { + unit: { + configFile: 'test/karma.conf.js', + singleRun: true + } + } + }); + + grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { + if (target === 'dist') { + return grunt.task.run(['build', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'clean:server', + 'wiredep', + 'injector:settingsSass', + 'injector:stylesSass', + 'copy:foundation', + 'concurrent:server', + 'injector:scripts', + 'connect:livereload', + 'watch' + ]); + }); + + grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { + grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); + grunt.task.run(['serve:' + target]); + }); + + grunt.registerTask('test', [ + 'clean:server', + 'wiredep', + 'injector:settingsSass', + 'injector:stylesSass', + 'copy:foundation', + 'concurrent:test', + 'injector:scripts', + 'injector:scriptsTest', + 'injector:htmlTest', + 'connect:test', + 'karma' + ]); + + grunt.registerTask('build', [ + 'clean:dist', + 'wiredep', + 'useminPrepare', + 'injector:settingsSass', + 'injector:stylesSass', + 'concurrent:dist', + 'injector:scripts', + 'concat', + 'ngAnnotate', + 'copy:dist', + 'cdnify', + 'cssmin', + 'uglify', + 'filerev', + 'usemin', + 'htmlmin' + ]); + + grunt.registerTask('default', [ + 'newer:jshint', + 'test', + 'build' + ]); +}; diff --git a/single-page-app-exercise/INSTRUCTIONS.md b/single-page-app-exercise/INSTRUCTIONS.md new file mode 100644 index 0000000..9cad7a8 --- /dev/null +++ b/single-page-app-exercise/INSTRUCTIONS.md @@ -0,0 +1,102 @@ +# Post Interview Challenge + +## Purpose: +Create a single-page webapp that meets all the stated requirements and industry standards of code quality and design patterns. The webapp must be implemented using the AngularJS framework and run on the included webserver (launched using grunt server) + +## Required Technologies and Libraries + +JavaScript libraries: +- AngularJS +- ui-grid (ng-grid) +- Restangular +- (Optional) lodash.js/undercore.js +- (Optional) momentjs +- (Optional) Bootstrap + +Required For Backend: + +- NodeJS + +### Browsers + +The developer should choose at least 2 browsers to developed against. The webapp must work in at least those 2 browsers. + +- Any version of IE +- Chrome +- FireFox + +### Running Backend + +- Run `grunt serve` +- Open browser of choice +- Navigate to `http://localhost:9000` +- The API URL to GET is `http://localhost:3000/stuff` +- The API URL to PATCH is `http://localhost:3000/stuff/123`, where 123 is the ID + + +## Submission + +- The challenge should be finished within 5 business days of the interview (since emailed) +- Minimal requirement is to provide as a ZIP file in an email +- Optionally, can provide as a Github pull request + +## Requirements + +There will be 2 pages the user can interact with, the "LIST" page and "EDIT" page. + +- Provide a README.md file +- README should describe basic instructions and which browsers the webapp is tested against + +### List Page +The list page will need to use a GET to fetch the data from the provided backend (using Restangular) and display it in the grid (implemented in ui-grid). Each of the following columns should be shown: + +- Name +- Description +- Last Time Edited (Displayed in the following format: "Jan 22, 2015 5:12:11") + +Note the edited time will be in a field called "timeStamp" and will be a UNIX format timeStamp. + +### Accessing the Edit Page + +From the list page, the user must be able to select and deselect each row. When a row is selected, a button located above the grid should be enabled. Once the user presses that button, the user is taken to the "EDIT" page. When navigating to the edit page, the URL should contain the ID of the selected field. + +The selected row's data should be preserved and available to the edit page. + +### Edit Page + +The input fields should be pre-populated with the data from the user's selected field. + +The following input fields need to be available for the user to edit: + +- Name +- Description + +The following buttons should be placed above the input fields: + +- OK (Updates backend with changes and returns user to list page) +- Cancel (Cancels all edits and returns user to list page) + +When user hits "OK" button, a PATCH request should be sent to the backend with the updated name, description, and timeStamp. + +### Returning to List Page + +Each time the user returns to the list page, the data must be updated with any changes, Including the ones just made in the edit page. + +### Responsiveness + +The list and edit pages should be centered and scale well. + +## Code Quality Standards + +### Expectations + +- Use 4 spaces and no tabs +- All code should pass jshint +- Follow AngularJS best practices +- Variables/Services/Functions should be named well and meaningful +- Templates should not be done inline, they should be in separate files + +### File Layout and Naming + +- Code should be organized and follow AngularJS naming conventions +- Use the provided folder structure as your scaffolding diff --git a/single-page-app-exercise/QUESTIONS.md b/single-page-app-exercise/QUESTIONS.md new file mode 100644 index 0000000..727c537 --- /dev/null +++ b/single-page-app-exercise/QUESTIONS.md @@ -0,0 +1,136 @@ +## JavaScript Symatics + +- Q: What is `NaN`? + - "Not a Number" + - Returned when a math operation is done on a value that cannot be coriced into a number (float or integer) + - Returned when `parseInt()` is called on a non-number or string (or string that does not look like a number) + - Ex: `expect(3 + undefined).toBeNaN();` + +- Q: + +## Build Knowledge + +- Q: What is minification and what is it for? + - Process of removing all whitespace, comments, and renaming variables to single letters + - Purpose is to reduce size of JS files (reduces load times) + - Obfuscates code. + +- Q: This AngularJS code will be minified. What is the bug? How do you fix it? +``` +angular.module('Ui').controller('MainCtrl', + function($scope, $filter, $log) { + $scope.onClick = function(event) { + $log.log('Clicked'); + }; + + $scope.filteredVal = $filter('orderby')($scope.someStuff); + } +); +``` + + - The dependency injector will fail to find the needed services. + - To fix, either use "array notation" or add this.$injector to function. + +``` +angular.module('Ui').controller('MainCtrl', [ + '$scope', + '$filter', + '$log', + function($scope, $filter, $log) { + $scope.onClick = function(event) { + $log.log('Clicked'); + }; + + $scope.filteredVal = $filter('orderby')($scope.someStuff); + } +]); + +// OR ... +function mainCtrl($scope, $filter, $log) { + $scope.onClick = function(event) { + $log.log('Clicked'); + }; + + $scope.filteredVal = $filter('orderby')($scope.someStuff); +} + +mainCtrl.$injector = [ '$scope', '$filter', '$log' ]; + +angular.module('Ui').controller('MainCtrl', mainCtrl); + +``` + +## Technical Knowledge + +- Q: What is immutable data (or state)? Why is it important and why should it be used? + - Immutable data typically refers to data structure that cannot be changed. + - By preserving state, prevents data from being changed expectantly. + - Guarantees that an object or value is in a known condition when later referenced. + - Most common in functional languages. + - To change the object, you must make a copy, and change the copy. + +- Q: Name 3 or 4 Design Patterns + + +- Q: What is functional programming? + - Functional programming is a paradigm. + - treats computation as evaluation of math functions + - Avoids changing state and mutable data + +## HTML5 +[HTML 5 Interview Questions](http://www.toptal.com/html5/interview-questions) + +- Q: Describe correct usage of `
`, `
`, `
`, and `