diff --git a/app/directives/on-file-change.directive.js b/app/directives/on-file-change.directive.js index d90e446e7..af55a3db8 100644 --- a/app/directives/on-file-change.directive.js +++ b/app/directives/on-file-change.directive.js @@ -11,7 +11,7 @@ return { restrict: 'A', link: function(scope, element, attr, ctrl) { - element.bind("change", function() { + element.bind('change', function() { scope.vm.onFileChange(element[0].files[0]); this.value = ''; }); diff --git a/app/directives/tc-file-input/tc-file-input.directive.js b/app/directives/tc-file-input/tc-file-input.directive.js new file mode 100644 index 000000000..532d56232 --- /dev/null +++ b/app/directives/tc-file-input/tc-file-input.directive.js @@ -0,0 +1,43 @@ +(function() { + 'use strict'; + + angular.module('tcUIComponents').directive('tcFileInput', tcFileInput); + + function tcFileInput() { + return { + restrict: 'E', + templateUrl: 'directives/tc-file-input/tc-file-input.html', + scope: { + labelText: '@', + fieldId: '@', + placeholder: '@', + fileType: '@', + mandatory: '=', + buttonText: '@', + setFileReference: '&' + }, + link: function(scope, element, attrs) { + scope.selectFile = selectFile; + + // fieldId is not set on element at this point, so grabbing with class .none + // which exists on the element right away + var fileInput = $(element[0]).find('.none'); + var fileNameInput = $(element[0]).find('input[type=text]'); + + fileInput.bind('change', function() { + var file = fileInput[0].files[0]; + + // Pass file object up through callback into controller + scope.setFileReference({file: file, fieldId: scope.fieldId}); + + // Set the file name as the value of the disabled input + fileNameInput[0].value = file.name; + }); + + function selectFile() { + fileInput.click(); + } + } + } + } +})(); diff --git a/app/directives/tc-file-input/tc-file-input.jade b/app/directives/tc-file-input/tc-file-input.jade new file mode 100644 index 000000000..6392e032a --- /dev/null +++ b/app/directives/tc-file-input/tc-file-input.jade @@ -0,0 +1,12 @@ +.tc-file-field__label + label.tc-label {{labelText}} + span.lowercase(ng-if="fileType") {{fileType | addBeginningSpace}} + + span.tc-label__mandatory.lowercase(ng-if="mandatory") #[span *]mandatory + +.tc-file-field__inputs + input.tc-file-field__input(type="text", placeholder="{{placeholder}}", required) + + button.tc-btn(ng-click="selectFile()") {{buttonText}} + + input.none(type="file", id="{{fieldId}}") diff --git a/app/directives/tc-file-input/tc-file-input.spec.js b/app/directives/tc-file-input/tc-file-input.spec.js new file mode 100644 index 000000000..3f2b398aa --- /dev/null +++ b/app/directives/tc-file-input/tc-file-input.spec.js @@ -0,0 +1,19 @@ +/* jshint -W117, -W030 */ +describe('Topcoder File Input Directive', function() { + var scope; + + // USE AS TEMPLATE FOR DIRECTIVES + + beforeEach(function() { + bard.appModule('tcUIComponents'); + bard.inject(this, '$compile', '$rootScope'); + }); + + bard.verifyNoOutstandingHttpRequests(); + + xdescribe('', function() { + beforeEach(function() {}); + + it('', function() {}); + }); +}); diff --git a/app/directives/tc-input/tc-input.directive.js b/app/directives/tc-input/tc-input.directive.js new file mode 100644 index 000000000..7bc1402ac --- /dev/null +++ b/app/directives/tc-input/tc-input.directive.js @@ -0,0 +1,17 @@ +(function() { + 'use strict'; + + angular.module('tcUIComponents').directive('tcInput', tcInput); + + function tcInput() { + return { + restrict: 'E', + templateUrl: 'directives/tc-input/tc-input.html', + scope: { + labelText: '@', + placeholder: '@', + inputValue: '=' + } + } + } +})(); diff --git a/app/directives/tc-input/tc-input.jade b/app/directives/tc-input/tc-input.jade new file mode 100644 index 000000000..f78f19134 --- /dev/null +++ b/app/directives/tc-input/tc-input.jade @@ -0,0 +1,3 @@ +label.tc-label {{labelText}} + +input(type="text", placeholder="{{placeholder}}", ng-model="inputValue") diff --git a/app/directives/tc-textarea/tc-textarea.directive.js b/app/directives/tc-textarea/tc-textarea.directive.js new file mode 100644 index 000000000..312bd5612 --- /dev/null +++ b/app/directives/tc-textarea/tc-textarea.directive.js @@ -0,0 +1,19 @@ +(function() { + 'use strict'; + + angular.module('tcUIComponents').directive('tcTextarea', tcTextarea); + + function tcTextarea() { + return { + restrict: 'E', + templateUrl: 'directives/tc-textarea/tc-textarea.html', + scope: { + labelText: '@', + placeholder: '@', + characterCount: '=', + characterCountMax: '@', + value: '=' + } + } + } +})(); diff --git a/app/directives/tc-textarea/tc-textarea.jade b/app/directives/tc-textarea/tc-textarea.jade new file mode 100644 index 000000000..33dbab40d --- /dev/null +++ b/app/directives/tc-textarea/tc-textarea.jade @@ -0,0 +1,7 @@ +label.tc-label.tc-textarea__label {{labelText}} + span.tc-textarea__char-count(ng-if="characterCount") + span.tc-textarea__char-count--current {{value.length || 0}} + + span {{' / ' + characterCountMax}} + +textarea(placeholder="{{placeholder}}", ng-model="value", maxlength="{{characterCountMax}}") diff --git a/app/filters/add-beginning-space.filter.js b/app/filters/add-beginning-space.filter.js new file mode 100644 index 000000000..ae6469419 --- /dev/null +++ b/app/filters/add-beginning-space.filter.js @@ -0,0 +1,11 @@ +(function() { + 'use strict'; + + angular.module('topcoder').filter('addBeginningSpace', addBeginningSpace); + + function addBeginningSpace() { + return function(input) { + return ' ' + input; + }; + }; +})(); diff --git a/app/filters/filters.spec.js b/app/filters/filters.spec.js index 57213ea63..fc6a6d662 100644 --- a/app/filters/filters.spec.js +++ b/app/filters/filters.spec.js @@ -3,7 +3,7 @@ describe('filters', function() { beforeEach(function() { bard.appModule('topcoder'); - bard.inject(this, 'CONSTANTS', 'roleFilter', 'percentageFilter', 'ordinalFilter', 'displayLocationFilter', 'listRolesFilter', 'trackFilter', 'challengeLinksFilter', 'externalLinkColorFilter', 'emptyFilter', 'ternaryFilter', 'urlProtocolFilter'); + bard.inject(this, 'CONSTANTS', 'roleFilter', 'percentageFilter', 'ordinalFilter', 'displayLocationFilter', 'listRolesFilter', 'trackFilter', 'challengeLinksFilter', 'externalLinkColorFilter', 'emptyFilter', 'ternaryFilter', 'urlProtocolFilter', 'addBeginningSpaceFilter'); domain = CONSTANTS.domain; }); @@ -100,7 +100,7 @@ describe('filters', function() { }); describe('externalLinkColorFilter', function() { - + it('should handle twitter and linkedin correctly', function() { expect(externalLinkColorFilter('el-twitter')).to.be.equal('#62AADC'); expect(externalLinkColorFilter('el-linkedin')).to.be.equal('#127CB5'); @@ -150,4 +150,10 @@ describe('filters', function() { expect(urlProtocolFilter('https://google.com')).to.be.equal('https://google.com'); }); }); + + describe('addBeginningSpaceFilter', function() { + it('should add a space to the beginning of the input', function() { + expect(addBeginningSpaceFilter('some text')).to.equal(' some text'); + }); + }); }); diff --git a/app/index.jade b/app/index.jade index 20421c2cc..635966d81 100644 --- a/app/index.jade +++ b/app/index.jade @@ -16,6 +16,7 @@ html link(rel='stylesheet', href='../bower_components/angular-dropdowns/dist/angular-dropdowns.css') link(rel='stylesheet', href='../bower_components/intro.js/introjs.css') link(rel='stylesheet', href='../bower_components/angularjs-toaster/toaster.css') + link(rel='stylesheet', href='../bower_components/react-select/dist/react-select.min.css') link(rel='stylesheet', href='../bower_components/fontawesome/css/font-awesome.css') link(rel='stylesheet', href='../bower_components/ng-notifications-bar/dist/ngNotificationsBar.min.css') link(rel='stylesheet', href='../bower_components/ngDialog/css/ngDialog.css') @@ -26,8 +27,11 @@ html // build:css /styles/app.css //- inject:css + link(rel="stylesheet", href="assets/css/vendors/introjs.css") link(rel="stylesheet", href="assets/css/vendors/angucomplete.css") link(rel="stylesheet", href="assets/css/topcoder.css") + link(rel="stylesheet", href="assets/css/submissions/submit-file.css") + link(rel="stylesheet", href="assets/css/submissions/submissions.css") link(rel="stylesheet", href="assets/css/skill-picker/skill-picker.css") link(rel="stylesheet", href="assets/css/sitemap/sitemap.css") link(rel="stylesheet", href="assets/css/settings/update-password.css") @@ -124,6 +128,7 @@ html // build:js /js/vendor.js //- bower:js + script(src='../bower_components/zepto/zepto.js') script(src='../bower_components/angular/angular.js') script(src='../bower_components/a0-angular-storage/dist/angular-storage.js') script(src='../bower_components/angucomplete-alt/angucomplete-alt.js') @@ -143,9 +148,18 @@ html script(src='../bower_components/angular-animate/angular-animate.js') script(src='../bower_components/angularjs-toaster/toaster.js') script(src='../bower_components/appirio-tech-ng-iso-constants/dist/ng-iso-constants.js') + script(src='../bower_components/angular-resource/angular-resource.js') + script(src='../bower_components/moment/moment.js') + script(src='../bower_components/angular-scroll/angular-scroll.js') + script(src='../bower_components/react/react.js') + script(src='../bower_components/react/react-dom.js') + script(src='../bower_components/classnames/index.js') + script(src='../bower_components/react-input-autosize/dist/react-input-autosize.min.js') + script(src='../bower_components/react-select/dist/react-select.min.js') + script(src='../bower_components/ngReact/ngReact.js') + script(src='../bower_components/appirio-tech-ng-ui-components/dist/main.js') script(src='../bower_components/d3/d3.js') script(src='../bower_components/jstzdetect/jstz.min.js') - script(src='../bower_components/moment/moment.js') script(src='../bower_components/ng-busy/build/angular-busy.js') script(src='../bower_components/ng-notifications-bar/dist/ngNotificationsBar.min.js') script(src='../bower_components/ngDialog/js/ngDialog.js') @@ -212,13 +226,17 @@ html script(src="directives/slideable.directive.js") script(src="directives/srm-tile/srm-tile.directive.js") script(src="directives/tc-endless-paginator/tc-endless-paginator.directive.js") + script(src="directives/tc-file-input/tc-file-input.directive.js") + script(src="directives/tc-input/tc-input.directive.js") script(src="directives/tc-paginator/tc-paginator.directive.js") script(src="directives/tc-section/tc-section.directive.js") script(src="directives/tc-sticky/tc-sticky.directive.js") script(src="directives/tc-tabs/tc-tabs.directive.js") + script(src="directives/tc-textarea/tc-textarea.directive.js") script(src="directives/tc-transclude.directive.js") script(src="directives/track-toggle/track-toggle.directive.js") script(src="topcoder.module.js") + script(src="filters/add-beginning-space.filter.js") script(src="filters/challengeLinks.filter.js") script(src="filters/deadline-msg.filter.js") script(src="filters/empty.filter.js") @@ -291,6 +309,7 @@ html script(src="services/scorecard.service.js") script(src="services/srm.service.js") script(src="services/statistics.service.js") + script(src="services/submissions.service.js") script(src="services/tags.service.js") script(src="services/tcAuth.service.js") script(src="services/user.service.js") @@ -309,6 +328,7 @@ html script(src="submissions/submissions.module.js") script(src="submissions/submissions.controller.js") script(src="submissions/submissions.routes.js") + script(src="submissions/submit-file/submit-file.controller.js") script(src="topcoder.constants.js") script(src="topcoder.controller.js") script(src="topcoder.interceptors.js") diff --git a/app/services/api.service.js b/app/services/api.service.js index 57c0e8b7a..a6936e50a 100644 --- a/app/services/api.service.js +++ b/app/services/api.service.js @@ -93,6 +93,13 @@ param: element }; } + + if (url.indexOf('submissions') > -1 && (operation.toLowerCase() === 'put' || operation.toLowerCase() === 'post')) { + return { + param: element + }; + } + return element; }) .addResponseInterceptor(function(data, operation, what, url, response, deferred) { diff --git a/app/services/submissions.service.js b/app/services/submissions.service.js new file mode 100644 index 000000000..7dcc8e433 --- /dev/null +++ b/app/services/submissions.service.js @@ -0,0 +1,118 @@ +(function() { + 'use strict'; + + angular.module('tc.services').factory('SubmissionsService', SubmissionsService); + + SubmissionsService.$inject = ['CONSTANTS', 'ApiService', '$q', '$log', 'toaster']; + + function SubmissionsService(CONSTANTS, ApiService, $q, $log, toaster) { + var api = ApiService.restangularV3; + + var service = { + getPresignedURL: getPresignedURL, + uploadSubmissionFileToS3: uploadSubmissionFileToS3, + updateSubmissionStatus: updateSubmissionStatus, + recordCompletedSubmission: recordCompletedSubmission + }; + + return service; + + function getPresignedURL(body, files) { + console.log('Body of request for presigned url: ', body); + + return api.all('submissions').customPOST(body) + .then(function(response) { + console.log('POST/Presigned URL Response: ', response.plain()); + + uploadSubmissionFileToS3(response, response.data.files, files); + }) + .catch(function(err) { + console.log(err); + $log.info('Error getting presigned url'); + toaster.pop('error', 'Whoops!', 'There was an error uploading your submissions. Please try again later.'); + }); + } + + function uploadSubmissionFileToS3(presignedURLResponse, files) { + var filesWithPresignedURL = presignedURLResponse.data.files; + + var promises = filesWithPresignedURL.map(function(fileWithPresignedURL) { + var deferred = $q.defer(); + var xhr = new XMLHttpRequest(); + + xhr.open('PUT', fileWithPresignedURL.preSignedUploadUrl, true); + xhr.setRequestHeader('Content-Type', fileWithPresignedURL.mediaType); + + // xhr version of the success callback + xhr.onreadystatechange = function() { + var status = xhr.status; + if (((status >= 200 && status < 300) || status === 304) && xhr.readyState === 4) { + $log.info('Successfully uploaded file'); + console.log('xhr response: ', xhr.responseText); + + // updateSubmissionStatus and then resolve? + deferred.resolve(); + + } else if (status >= 400) { + $log.error('Error uploading to S3 with status: ' + status); + toaster.pop('error', 'Whoops!', 'There was an error uploading your files. Please try again later.'); + deferred.reject(err); + } + }; + + xhr.onerror = function(err) { + $log.info('Error uploading to s3'); + toaster.pop('error', 'Whoops!', 'There was an error uploading your files. Please try again later.'); + deferred.reject(err); + } + + xhr.send(files[fileWithPresignedURL.type]); + + return deferred.promise; + }); + + return $q.all(promises) + .then(function(response) { + console.log('response from S3: ', response); + console.log('response to use .save restnagular with: ', presignedURLResponse); + + // Update and start processing + updateSubmissionStatus(presignedURLResponse.plain()); + + }) + .catch(function(err) { + console.log('error uploading to S3: ', err); + }); + } + + function updateSubmissionStatus(body) { + // Pass data from upload to S3 + body.data.files.forEach(function(file) { + file.status = 'UPLOADED'; + }); + + return api.one('submissions', body.id).customPUT(body) + .then(function(response) { + $log.info('Successfully updated file statuses'); + recordCompletedSubmission(response.plain()); + }) + .catch(function(err) { + $log.info('Error updating file statuses'); + $log.error(err); + }); + } + + function recordCompletedSubmission(body) { + // Once all uploaded, make record and begin processing + return api.one('submissions', body.id).customPOST(body, 'process') + .then(function(response) { + $log.info('Successfully made file record. Beginning processing'); + console.log('response from process call: ', response); + }) + .catch(function(err) { + $log.info('Error in starting processing'); + $log.error(err); + }); + } + }; +})(); diff --git a/app/settings/account-info/account-info.jade b/app/settings/account-info/account-info.jade index 758b5b624..11b40aaac 100644 --- a/app/settings/account-info/account-info.jade +++ b/app/settings/account-info/account-info.jade @@ -16,7 +16,7 @@ .form-label Current password .validation-bar(ng-class="{ 'error-bar': (vm.newPasswordForm.currentPassword.$dirty && vm.newPasswordForm.currentPassword.$invalid) }") - toggle-password.tc-input.password(ng-model="vm.currentPassword") + toggle-password.topcoder-input.password(ng-model="vm.currentPassword") .form-input-error(ng-show="vm.newPasswordForm.currentPassword.$dirty && vm.newPasswordForm.currentPassword.$invalid") p(ng-show="vm.newPasswordForm.currentPassword.$error.required") This is a required field. @@ -26,7 +26,7 @@ .form-label New Password .validation-bar - toggle-password-with-tips.tc-input.password(placeholder="Pick a new password") + toggle-password-with-tips.topcoder-input.password(placeholder="Pick a new password") .tips.password-tips(ng-show="vm.passwordFocus") .arrow @@ -64,7 +64,7 @@ span.mandatory *mandatory .validation-bar(ng-class="{ 'error-bar': (vm.accountInfoForm.$dirty && vm.accountInfoForm.firstname.$invalid), 'success-bar': (vm.accountInfoForm.$dirty && vm.accountInfoForm.firstname.$valid)}") - input.tc-input( + input.topcoder-input( name="firstname", type="text", placeholder="First", ng-model="vm.userData.firstName", @@ -78,7 +78,7 @@ span(style="text-transform: none;")  (Surname) span.mandatory *mandatory .validation-bar(ng-class="{ 'error-bar': (vm.accountInfoForm.$dirty && vm.accountInfoForm.lastname.$invalid), 'success-bar': (vm.accountInfoForm.$dirty && vm.accountInfoForm.lastname.$valid)}") - input.tc-input( + input.topcoder-input( name="lastname", type="text", placeholder="Last", ng-model="vm.userData.lastName", @@ -95,7 +95,7 @@ .section-fields .form-label.address Address - input.tc-input( + input.topcoder-input( name="address", type="text", placeholder="123 Topcoder Ave.", value="{{vm.homeAddress.streetAddr1}}", @@ -105,7 +105,7 @@ .form-label Address 2 span(style="text-transform: none;")  (apt., suite, etc.) - input.tc-input( + input.topcoder-input( name="address2", type="text", placeholder="Suite 42", @@ -115,7 +115,7 @@ ) .form-label City - input.tc-input( + input.topcoder-input( name="city", type="text", value="{{vm.homeAddress.city}}", placeholder="Best City in the World", @@ -124,7 +124,7 @@ ) .form-label State/Province - input.tc-input( + input.topcoder-input( name="state", type="text", value="{{vm.homeAddress.stateCode}}", placeholder="California", @@ -133,7 +133,7 @@ ) .form-label Zip/Post Code - input.tc-input( + input.topcoder-input( name="zipcode", type="text", placeholder="Zip" value="{{vm.homeAddress.zip}}", diff --git a/app/settings/edit-profile/edit-profile.jade b/app/settings/edit-profile/edit-profile.jade index 56d63a3bc..9008cf203 100644 --- a/app/settings/edit-profile/edit-profile.jade +++ b/app/settings/edit-profile/edit-profile.jade @@ -19,7 +19,7 @@ button.file-upload.tc-btn.tc-btn-primary.tc-btn-s(ng-click="vm.changeImage()", type="button") span(ng-show="vm.userData.photoURL && vm.userData.photoURL.length") Change Image span(ng-show="!vm.userData.photoURL || !vm.userData.photoURL.length") Add Image - input(type='file', name='image', on-file-change='on-file-change', id="change-image-input", style="display: none;") + input(type="file", name="image", on-file-change="on-file-change", id="change-image-input", style="display: none;") .file-delete(ng-show="vm.userData.photoURL && vm.userData.photoURL.length") button.tc-btn.tc-btn-secondary.tc-btn-s(ng-click="vm.deleteImage()", type="button") Delete @@ -47,7 +47,7 @@ .form-label(style="width: 100%;") short bio span.char-count {{vm.userData.description.length || 0}} span.grey  / 256 - textarea.tc-input(name="description", ng-model="vm.userData.description", data-ng-trim="false", maxlength="256", placeholder="E.g., I'm a JS architect interested in creating new data interchange formats. I love sci-fi and riding my motorcycle.") + textarea.topcoder-input(name="description", ng-model="vm.userData.description", data-ng-trim="false", maxlength="256", placeholder="E.g., I'm a JS architect interested in creating new data interchange formats. I love sci-fi and riding my motorcycle.") .settings-section.tracks .section-info diff --git a/app/submissions/submissions.controller.js b/app/submissions/submissions.controller.js index 17540cfa1..63c24e445 100644 --- a/app/submissions/submissions.controller.js +++ b/app/submissions/submissions.controller.js @@ -3,15 +3,18 @@ angular.module('tc.submissions').controller('SubmissionsController', SubmissionsController); - SubmissionsController.$inject = []; + SubmissionsController.$inject = ['challengeToSubmitTo']; - function SubmissionsController() { + function SubmissionsController(challengeToSubmitTo) { var vm = this; + var challenge = challengeToSubmitTo.challenge; + vm.challengeTitle = challenge.name; + vm.challengeId = challenge.id; + vm.track = challenge.track.toLowerCase(); + activate(); - function activate() { - vm.testValue = 'testValue'; - } + function activate() {} } })(); diff --git a/app/submissions/submissions.jade b/app/submissions/submissions.jade index 791a65834..309269929 100644 --- a/app/submissions/submissions.jade +++ b/app/submissions/submissions.jade @@ -1,2 +1,12 @@ -h1 Hello -h2 {{submissions.testValue}} +.panel-page + .panel-header.flex.space-between + a.panel-header__back-button.flex.space-between.middle(ng-href="https://www.{{DOMAIN}}/challenge-details/{{submissions.challengeId}}/?type={{submissions.track}}") + + //- TODO: Replace below with svg tag + .icon-arrow + + p Back + + h1.panel-header__title(ng-bind="submissions.challengeTitle") + + ui-view diff --git a/app/submissions/submissions.module.js b/app/submissions/submissions.module.js index a17096edd..4c6fb4832 100644 --- a/app/submissions/submissions.module.js +++ b/app/submissions/submissions.module.js @@ -5,7 +5,8 @@ 'ui.router', 'tc.services', 'tcUIComponents', - 'toaster' + 'toaster', + 'appirio-tech-ng-ui-components' ]; angular.module('tc.submissions', dependencies); diff --git a/app/submissions/submissions.routes.js b/app/submissions/submissions.routes.js index 608c12f01..8c491da9f 100644 --- a/app/submissions/submissions.routes.js +++ b/app/submissions/submissions.routes.js @@ -11,13 +11,76 @@ var states = { submissions: { parent: 'root', - url: '/challenges/:challengeId/submit/?method=file', + abstract: true, + url: '/challenges/:challengeId/submit/', templateUrl: 'submissions/submissions.html', controller: 'SubmissionsController', controllerAs: 'submissions', data: { - authRequired: true + authRequired: true, + + // TODO: Get title from PMs + title: 'Submit' + }, + resolve: { + challengeToSubmitTo: ['ChallengeService', '$stateParams', 'UserService', function(ChallengeService, $stateParams, UserService) { + // This page is only available to users that are registered to the challenge (submitter role) and the challenge is in the Checkpoint Submission or Submission phase. + var params = { + filter: 'id=' + $stateParams.challengeId + }; + + var userHandle = UserService.getUserIdentity().handle; + + return ChallengeService.getUserChallenges(userHandle, params) + .then(function(challenge) { + challenge = challenge[0]; + + if (!challenge) { + // There should be a challenge, redirect? + alert('User is not associated with this challenge.'); + } + + var phaseType; + var phaseId; + + var isPhaseSubmission = _.some(challenge.currentPhases, function(phase) { + if (phase.phaseStatus === 'Open' && phase.phaseType === 'Submission') { + phaseType = 'Submission'; + phaseId = phase.id; + return true; + } + + return false; + }); + + var isSubmitter = _.some(challenge.userDetails.roles, function(role) { + return role === 'Submitter'; + }); + + if (!isPhaseSubmission || !isSubmitter) { + // TODO: Where do we redirect if you can't submit? + alert('You should not have access to this page'); + } + + return { + challenge: challenge, + phaseType: phaseType, + phaseId: phaseId + }; + }) + .catch(function(err) { + console.log('ERROR GETTING CHALLENGE: ', err); + alert('There was an error accessing this page'); + // TODO: Where do we redirect if there is an error? + }); + }] } + }, + 'submissions.file': { + url: '?method=file', + templateUrl: 'submissions/submit-file/submit-file.html', + controller: 'SubmitFileController', + controllerAs: 'vm', } }; diff --git a/app/submissions/submissions.spec.js b/app/submissions/submissions.spec.js index 7f0ee32c8..7b8c912ab 100644 --- a/app/submissions/submissions.spec.js +++ b/app/submissions/submissions.spec.js @@ -19,6 +19,5 @@ describe('Submissions Controller', function() { it('should exist', function() { expect(vm).to.exist; - expect(vm.testValue).to.equal('testValue'); }); }); diff --git a/app/submissions/submit-file/submit-file.controller.js b/app/submissions/submit-file/submit-file.controller.js new file mode 100644 index 000000000..223e07975 --- /dev/null +++ b/app/submissions/submit-file/submit-file.controller.js @@ -0,0 +1,182 @@ +(function () { + 'use strict'; + + angular.module('tc.submissions').controller('SubmitFileController', SubmitFileController); + + SubmitFileController.$inject = ['$stateParams', 'UserService', 'SubmissionsService', 'challengeToSubmitTo']; + + function SubmitFileController($stateParams, UserService, SubmissionsService, challengeToSubmitTo) { + var vm = this; + + // Must provide React Select component a list with ID, since currently + // the onChange callback does not indicate which dropdown called the callback. + // There are pull requets pending for react-select which will clean this code up + vm.fontList1 = [ + { label: 'Studio Standard Fonts List', value: 'STUDIO_STANDARDS_FONTS_LIST', id: 1 }, + { label: 'Fonts.com', value: 'FONTS_DOT_COM', id: 1 }, + { label: 'MyFonts', value: 'MYFONTS', id: 1 }, + { label: 'Adobe Fonts', value: 'ADOBE_FONTS', id: 1 }, + { label: 'Font Shop', value: 'FONT_SHOP', id: 1 }, + { label: 'T.26 Digital Type Foundry', value: 'T26_DIGITAL_TYPE_FOUNDRY', id: 1 }, + { label: 'Font Squirrel', value: 'FONT_SQUIRREL', id: 1 }, + { label: 'Typography.com', value: 'TYPOGRAPHY_DOT_COM', id: 1 } + ]; + + var files = {}; + vm.comments = ''; + vm.submissionForm = { + files: [], + + // Should the rank input field be set to 1 automatically? + submitterRank: 1, + submitterComments: '', + fonts: [{ + id: 1, + source: '', + name: '', + sourceUrl: '' + }], + stockArts: [{ + id: 1, + description: '', + sourceUrl: '', + fileNumber: '' + }], + hasAgreedToTerms: false + }; + + var userId = parseInt(UserService.getUserIdentity().userId); + + vm.submissionsBody = { + reference: { + + // type dynamic or static? + type: 'CHALLENGE', + id: $stateParams.challengeId, + phaseType: challengeToSubmitTo.phaseType, + phaseId: challengeToSubmitTo.phaseId + }, + userId: userId, + data: { + + // Dynamic or static? + method: 'DESIGN_CHALLENGE_ZIP_FILE', + files: [], + submitterRank: 1, + submitterComments: '', + fonts: [], + stockArts: [] + } + }; + + vm.setFileReference = setFileReference; + vm.uploadSubmission = uploadSubmission; + vm.selectFont = selectFont; + vm.createAnotherFontFieldset = createAnotherFontFieldset; + vm.createAnotherStockArtFieldset = createAnotherStockArtFieldset; + + activate(); + + function activate() {} + + function setFileReference(file, fieldId) { + files[fieldId] = file; + + var fileObject = { + name: file.name, + type: fieldId, + status: 'PENDING' + }; + + switch(fieldId) { + case 'SUBMISSION_ZIP': + fileObject.mediaType = 'application/octet-stream'; + break; + case 'SOURCE_ZIP': + fileObject.mediaType = 'application/octet-stream'; + break; + default: + fileObject.mediaType = file.type; + } + + + + // If user picks a new file, replace the that file's fileObject with a new one + // Or add it the list if it's not there + if (vm.submissionsBody.data.files.length) { + vm.submissionsBody.data.files.some(function(file, i, filesArray) { + if (file.type === fileObject.type) { + file = fileObject; + } else if (filesArray.length === i + 1) { + filesArray.push(fileObject); + } + }); + } else { + vm.submissionsBody.data.files.push(fileObject); + } + } + + function selectFont(newFont) { + // See above for explanation + var id = newFont.id - 1; + vm.submissionForm.fonts[id].source = newFont.value; + } + + function createAnotherFontFieldset() { + // See above for explanation on why this is done the way it is + var id = vm.submissionForm.fonts.length; + + // Create copy of list with new, incremented ID + var newFontList = vm['fontList' + (id + 1)] = angular.copy(vm['fontList' + id]); + + newFontList.forEach(function(font) { + font.id++; + }); + + vm.submissionForm.fonts.push({ + id: vm.submissionForm.fonts.length + 1, + source: '', + name: '', + sourceUrl: '' + }); + } + + function createAnotherStockArtFieldset() { + vm.submissionForm.stockArts.push({ + id: vm.submissionForm.stockArts.length + 1, + description: '', + sourceUrl: '', + fileNumber: '' + }); + } + + function uploadSubmission() { + vm.submissionsBody.data.submitterComments = vm.comments; + vm.submissionsBody.data.submitterRank = vm.submissionForm.submitterRank; + + if (vm.submissionForm.stockArts[0].description === '') { + vm.submissionsBody.data.stockArts = []; + } else { + var stockArts = angular.copy(vm.submissionForm.stockArts); + vm.submissionsBody.data.stockArts = stockArts.map(function(stockArt) { + delete stockArt.id; + return stockArt; + }); + } + + if (vm.submissionForm.fonts[0].source === '') { + vm.submissionsBody.data.fonts = []; + } else { + var fonts = angular.copy(vm.submissionForm.fonts); + vm.submissionsBody.data.fonts = fonts.map(function(font) { + if (font.source) { + delete font.id; + return font; + } + }); + } + + SubmissionsService.getPresignedURL(vm.submissionsBody, files); + } + } +})(); diff --git a/app/submissions/submit-file/submit-file.jade b/app/submissions/submit-file/submit-file.jade new file mode 100644 index 000000000..495d8670a --- /dev/null +++ b/app/submissions/submit-file/submit-file.jade @@ -0,0 +1,153 @@ +.mobile-redirect + .mobile-redirect__title File upload is not available for mobile + + .mobile-redirect__body + p Our team is working hard on new features, and file upload currently only works on the web. Please open this page on your desktop if you want to create a submission. + + a Learn more about formatting your submission file + + a.tc-btn.tc-btn-s(ng-click="'TODO: go back to challenge details'") Back to Challenge Details + +.panel-body + form.form-blocks(name="submissionForm", role="form", ng-submit="submissionForm.$valid && vm.uploadSubmission()", novalidate) + .form-block.flex.wrap + .form-block__instructions + .form-block__title Files + + .form-block__text + p Please follow the instructions on the Challenge Details page regarding what your submission, source, and preview files should contain. + + p Do not name any of your files "declaration.txt", as this is added by our system. + + p Please be sure to double-check that you have submitted the correct files and that your JPG files (if applicable) are in RGB color mode. + + a(href="http://help.{{DOMAIN}}/design/submitting-to-a-design-challenge/formatting-your-submission-for-design-challenges/") Learn more about formatting your submission file + + .form-block__fields + .fieldset + tc-file-input.tc-file-field( + label-text="Submission", + field-id="SUBMISSION_ZIP", + button-text="Add File", + file-type="(*.zip)", + placeholder="Attach all visible files as a single .zip file", + mandatory="true", + set-file-reference="vm.setFileReference(file, fieldId)" + ) + + tc-file-input.tc-file-field( + label-text="Source", + field-id="SOURCE_ZIP", + button-text="Add File", + file-type="(*.zip)", + placeholder="Attach all source files as a single .zip file", + mandatory="true", + set-file-reference="vm.setFileReference(file, fieldId)" + ) + + tc-file-input.tc-file-field( + label-text="Preview Image", + field-id="DESIGN_COVER", + button-text="Add File", + placeholder="Image file as .jpg or .png", + mandatory="true", + set-file-reference="vm.setFileReference(file, fieldId)" + ) + + tc-input.fieldset__input.submitterRank(label-text="Rank #", input-value="vm.submissionForm.submitterRank") + + .form-block.flex.wrap + .form-block__instructions + .form-block__title Notes + + .form-block__text + p Type a short note about your design here. Explain revisions or other design elements that may not be clear. + + .form-block__fields + tc-textarea.tc-textarea( + label-text="Comments", + placeholder="My design tries to solve the problem with a particular idea in mind. The use of color is based on the provided brand guideline. The flows are included in the sub folder. I followed all revisions as per the directions provided.", + character-count="true", + character-count-max="500", + value="vm.comments" + ) + + .form-block.flex.wrap + .form-block__instructions + .form-block__title Fonts + + .form-block__text + p Check to see if your font is on the Studio Standard Fonts list. If it is, leave the URL field blank. + + p Read the #[a(href="Need link") Studio Fonts Policy]. + + p If you only used fonts that came with the client files, choose "I did not introduce any new fonts" from the dropdown box. + + p If your font is not on the list, you must provide the URL to the font page (not file) from one of the approved font websites in the dropdown box. + + .form-block__fields + .fieldsets + ng-form.fieldset(name="font_{{$index + 1}}", ng-repeat="font in vm.submissionForm.fonts track by font.id") + dropdown( + name="'font-source{{$index + 1}}'", + options="vm.fontList{{$index + 1}}", + placeholder="'Select from the list'", + searchable="false", + clearable="false", + on-change="vm.selectFont", + value="vm.submissionForm.fonts[{{$index}}].source" + ) + + tc-input.fieldset__input( + label-text="Font Name", + placeholder="Select font source to edit field" + input-value="font.name" + ) + + tc-input.fieldset__input( + label-text="Font URL", + placeholder="Select font source to edit field", + input-value="font.sourceUrl" + ) + + button.fieldset__button.tc-btn.tc-btn-s(type="button", ng-click="vm.createAnotherFontFieldset()") + Add Font + + .form-block.flex.wrap + .form-block__instructions + .form-block__title Stock Art + + .form-block__text + p If you used any stock photos in your design mocks, please provide the location and details so that the client can obtain them. Follow the guidelines at our #[a(href="Need link") Studio Stock Art Policy]. + + .form-block__fields + .fieldsets + ng-form.fieldset(name="stockArt_{{$index + 1}}", ng-repeat="stockArt in vm.submissionForm.stockArts track by stockArt.id") + tc-input.fieldset__input( + label-text="Photo Description", + placeholder="A picture of a girl", + input-value="stockArt.description" + ) + + tc-input.fieldset__input( + label-text="Photo URL", + placeholder="www.istockphoto.com", + input-value="stockArt.sourceUrl" + ) + + tc-input.fieldset__input( + label-text="File Number", + placeholder="u2434312", + input-value="stockArt.fileNumber" + ) + + button.fieldset__button.tc-btn.tc-btn-s(type="button", ng-click="vm.createAnotherStockArtFieldset()") + Add Stock Photo + + .panel-footer + p Submitting your files means you hereby agree to the #[a(href="https://www.{{DOMAIN}}/community/how-it-works/terms/", target="_blank") Topcoder terms of use] and to the extent your uploaded file wins a Topcoder Competition, you hereby assign, grant, and transfer to Topcoder all rights in and title to the Winning Submission (as further described in the terms of use). + + .checkbox.flex.center + input(type="checkbox", ng-model="vm.submissionForm.hasAgreedToTerms", id="agree-to-terms", required) + + label(for="agree-to-terms") I understand and agree + + button.tc-btn.tc-btn-secondary(type="submit", ng-disabled="submissionForm.$invalid") Submit diff --git a/app/submissions/submit-file/submit-file.spec.js b/app/submissions/submit-file/submit-file.spec.js new file mode 100644 index 000000000..339459315 --- /dev/null +++ b/app/submissions/submit-file/submit-file.spec.js @@ -0,0 +1,22 @@ +/* jshint -W117, -W030 */ +describe('Submit File Controller', function() { + var controller; + var vm; + + beforeEach(function() { + bard.appModule('tc.submissions'); + bard.inject(this, '$controller'); + }); + + bard.verifyNoOutstandingHttpRequests(); + + beforeEach(function() { + controller = $controller('SubmitFileController', {}); + vm = controller; + }); + + it('should exist', function() { + expect(vm).to.exist; + expect(vm.submitFile).to.be.true; + }); +}); diff --git a/app/topcoder.interceptors.js b/app/topcoder.interceptors.js index bef0b4671..78751516f 100644 --- a/app/topcoder.interceptors.js +++ b/app/topcoder.interceptors.js @@ -1,6 +1,6 @@ (function() { 'use strict'; - var HeaderInterceptor, JwtConfig; + var JwtConfig; JwtConfig = function($httpProvider, jwtInterceptorProvider) { jwtInterceptorProvider.tokenGetter = ['config', 'JwtInterceptorService', function(config, JwtInterceptorService) { @@ -9,18 +9,6 @@ return $httpProvider.interceptors.push('jwtInterceptor'); }; - HeaderInterceptor = function() { - var attach; - return attach = { - request: function(request) { - request.headers['Accept'] = 'application/json'; - request.headers['Content-Type'] = 'application/json'; - return request; - } - }; - }; - - angular.module('topcoder').factory('HeaderInterceptor', HeaderInterceptor); angular.module('topcoder').config(['$httpProvider', 'jwtInterceptorProvider', JwtConfig]); })(); diff --git a/app/topcoder.module.js b/app/topcoder.module.js index 971a0f8f1..44bba2fc8 100644 --- a/app/topcoder.module.js +++ b/app/topcoder.module.js @@ -72,10 +72,9 @@ // NotificationService.getNotifications(); } - angular.module('topcoder').config(['$httpProvider', 'RestangularProvider', '$locationProvider', - function($httpProvider, RestangularProvider, $locationProvider) { + angular.module('topcoder').config(['RestangularProvider', '$locationProvider', + function(RestangularProvider, $locationProvider) { $locationProvider.html5Mode(true); - $httpProvider.interceptors.push('HeaderInterceptor'); RestangularProvider.setRequestSuffix('/'); }]); diff --git a/app/topcoder.routes.js b/app/topcoder.routes.js index 46ea347cd..e063cb6e5 100644 --- a/app/topcoder.routes.js +++ b/app/topcoder.routes.js @@ -59,7 +59,7 @@ controllerAs: 'vm' }, 'container@': { - template: "
" + template: "
" }, 'footer@': { templateUrl: 'layout/footer/footer.html', diff --git a/assets/css/my-dashboard/header-dashboard.scss b/assets/css/my-dashboard/header-dashboard.scss index 4832488cb..b9ebb9c4b 100644 --- a/assets/css/my-dashboard/header-dashboard.scss +++ b/assets/css/my-dashboard/header-dashboard.scss @@ -1,24 +1,8 @@ @import 'topcoder/tc-includes'; .header-dashboard { - margin-top: 15px; + // TODO: Use styleguide class + max-width: 1242px; margin-left: auto; margin-right: auto; } - - -@media (min-width: 768px) { - .header-dashboard { - @include module-l; - margin-top: 30px; - } -} - - -@media (min-width: 900px) { - .header-dashboard { - padding: 0px; - margin-left: auto; - margin-right: auto; - } -} diff --git a/assets/css/submissions/submissions.scss b/assets/css/submissions/submissions.scss new file mode 100644 index 000000000..b34324350 --- /dev/null +++ b/assets/css/submissions/submissions.scss @@ -0,0 +1,43 @@ +@import 'topcoder/tc-includes'; + +.mobile-redirect { + padding: 45px 15px 30px; + @media screen and (min-width: 768px) { + display: none; + } +} + +.mobile-redirect__title { + margin-bottom: 10px; + @include font-with-weight('Sofia Pro', 500); + font-size: 16px; + line-height: 19px; + color: $gray-darkest; + text-transform: uppercase; +} + +.mobile-redirect__body { + @include font-with-weight('Merriweather Sans'); + font-size: 13px; + font-style: italic; + line-height: 20px; + color: $accent-gray; + text-align: center; + + p { + text-align: left; + } + + a { + &:not(.tc-btn) { + text-align: left; + display: block; + margin-top: 20px; + } + + &.tc-btn { + display: inline-block; + margin-top: 20px; + } + } +} diff --git a/assets/css/submissions/submit-file.scss b/assets/css/submissions/submit-file.scss new file mode 100644 index 000000000..11c5689ed --- /dev/null +++ b/assets/css/submissions/submit-file.scss @@ -0,0 +1,38 @@ +@import 'topcoder/tc-includes'; + +.panel-body { + @media screen and (max-width: 767px) { + display: none; + } +} + +.panel-footer { + text-align: center; + + p { + margin-bottom: 30px; + @include font-with-weight('Merriweather Sans', 300); + font-size: 13px; + line-height: 24px; + color: $gray-darkest; + text-align: left; + } + + .checkbox { + label { + margin-left: 10px; + @include font-with-weight('Sofia Pro', 500); + font-size: 16px; + color: $gray-darkest; + text-transform: uppercase; + } + } + + button.tc-btn { + margin-top: 30px; + } +} + +.submitterRank { + margin-top: 10px; +} diff --git a/assets/css/topcoder.scss b/assets/css/topcoder.scss index ff59360bb..9af99612c 100644 --- a/assets/css/topcoder.scss +++ b/assets/css/topcoder.scss @@ -4,6 +4,7 @@ body { @include font-with-weight('Merriweather Sans', 400); background-color: $gray-lighter; + > svg { display: none; } @@ -32,28 +33,35 @@ body { @media screen and (min-device-width: 768px) { min-width: 768px; } + .notifications-container { position: static; z-index: 0; } + .error { background-color: #f2dede; border-color: #ebccd1; color: #a94442; } + + .close-click { + font-size: inherit; + } } .view-container { - min-height: 480px; + min-height: 440px; padding-bottom: 30px; @media screen and (min-device-width: 768px) { min-width: 768px; } } -.notifications { - .close-click { - font-size: inherit; +.page-container { + padding: 10px; + @media screen and (min-width:768px) { + padding: 30px 10px; } } @@ -64,8 +72,29 @@ body { min-height: 50px; } +#toast-container > div { + @include sofia-pro-light; + width: 400px; + opacity: .95; +} + + + +// FORMS +// ---------------------------------------------------------------------- +// .form-errors { + position: relative; width: 380px; + line-height: 22px; + text-align: left; + @media (max-width: 767px) { + width: 100%; + } + + p { + margin: 2px 0 12px 5px; + } } .form-notice { @@ -90,127 +119,6 @@ body { text-align: left; } -#toast-container > div { - @include sofia-pro-light; - width: 400px; - opacity: .95; -} - -@media (max-width: 767px) { - .form-errors { - width: 100%; - } -} - -// Intro JS -// ----------------------------------------------------------- - -.introjs-overlay { - background: $accent-gray-dark; - opacity: .94; -} - -.introjs-tooltip { - max-width: 320px; - padding: 0 20px 20px; -} - -.introjs-tooltiptext { - @include font-with-weight('Merriweather Sans', 400); - color: $gray-darker; - display: flex; - flex-direction: column; - align-items: center; - font-size: 13px; - line-height: 20px; - h1 { - @include sofia-pro-medium; - color: $gray-darkest; - font-size: 16px; - line-height: 24px; - margin-top: 20px; - text-align: center; - } - img { - display: none; - max-width: 320px; - @media only screen and (min-width : 768px) { - display: block; - } - } - p { - margin-top: 20px; - } -} - - -.introjs-helperLayer { - background: $white; - border: 1px solid #85ccff; - border-radius: 6px; - box-shadow: 0 0 4px 0 rgba(0, 150, 255, .20); -} - -.introjs-helperNumberLayer { - @include sofia-pro-medium; - background: $primary; - border: 3px solid $white; - box-shadow: 0 1px 4px 0 rgba(0,0,0,.20); - height: 30px; - width: 30px; - font-size: 17px; - line-height: 22px; - padding: 0; -} - -.introjs-tooltipbuttons { - float: right; - width: 280px; -} - -// Refactor buttons when button mixins and/or style guide is done -.introjs-button { - @extend .tc-btn; - @extend .tc-btn-s; -} - -.introjs-skipbutton { - @extend .tc-btn-ghost; - float: left; -} - - -.introjs-disabled { - @extend .tc-btn-s; - @extend :disabled; -} - -.introjs-prevbutton { - margin-right: 10px; -} - - -.introjs-bullets ul li { - margin-right: 3px; -} - -.introjs-bullets ul li a { - background-color: $gray; - border-radius: 0; - height: 5px; - width: 5px; - &.active { - background-color: $primary; - } -} - -// Intro JS END - -// FORMS -// ---------------------------------------------------------------------- -// -// Form Labels - .form-label { @include sofia-pro-medium; color: $gray-darkest; @@ -233,19 +141,6 @@ body { } } -.form-errors { - line-height: 22px; - position: relative; -} - - -.form-errors { - text-align: left; - p { - margin: 2px 0 12px 5px; - } -} - // Error and success styles .validation-bar { position: relative; diff --git a/assets/css/vendors/angucomplete.scss b/assets/css/vendors/angucomplete.scss index 839cf1500..09623989c 100644 --- a/assets/css/vendors/angucomplete.scss +++ b/assets/css/vendors/angucomplete.scss @@ -1,6 +1,11 @@ @import 'topcoder/tc-includes'; // angucomplete overrides +angucomplete-alt { + width: 100%; + margin-top: 6px; +} + .angucomplete-holder { position: relative; width: 100%; diff --git a/assets/css/vendors/introjs.scss b/assets/css/vendors/introjs.scss new file mode 100644 index 000000000..dba7c1982 --- /dev/null +++ b/assets/css/vendors/introjs.scss @@ -0,0 +1,101 @@ +@import 'topcoder/tc-includes'; + +// Intro JS overrides +.introjs-overlay { + background: $accent-gray-dark; + opacity: .94; +} + +.introjs-tooltip { + max-width: 320px; + padding: 0 20px 20px; +} + +.introjs-tooltiptext { + @include font-with-weight('Merriweather Sans', 400); + color: $gray-darker; + display: flex; + flex-direction: column; + align-items: center; + font-size: 13px; + line-height: 20px; + h1 { + @include sofia-pro-medium; + color: $gray-darkest; + font-size: 16px; + line-height: 24px; + margin-top: 20px; + text-align: center; + } + img { + display: none; + max-width: 320px; + @media only screen and (min-width : 768px) { + display: block; + } + } + p { + margin-top: 20px; + } +} + + +.introjs-helperLayer { + background: $white; + border: 1px solid #85ccff; + border-radius: 6px; + box-shadow: 0 0 4px 0 rgba(0, 150, 255, .20); +} + +.introjs-helperNumberLayer { + @include sofia-pro-medium; + background: $primary; + border: 3px solid $white; + box-shadow: 0 1px 4px 0 rgba(0,0,0,.20); + height: 30px; + width: 30px; + font-size: 17px; + line-height: 22px; + padding: 0; +} + +.introjs-tooltipbuttons { + float: right; + width: 280px; +} + +// Refactor buttons when button mixins and/or style guide is done +.introjs-button { + @extend .tc-btn; + @extend .tc-btn-s; +} + +.introjs-skipbutton { + @extend .tc-btn-ghost; + float: left; +} + + +.introjs-disabled { + @extend .tc-btn-s; + @extend :disabled; +} + +.introjs-prevbutton { + margin-right: 10px; +} + + +.introjs-bullets ul li { + margin-right: 3px; +} + +.introjs-bullets ul li a { + background-color: $gray; + border-radius: 0; + height: 5px; + width: 5px; + &.active { + background-color: $primary; + } +} diff --git a/bower.json b/bower.json index 46f63e492..e681645f6 100644 --- a/bower.json +++ b/bower.json @@ -23,6 +23,7 @@ "tests" ], "dependencies": { + "zepto": "1.1.x", "a0-angular-storage": "~0.0.11", "angucomplete-alt": "~1.1.0", "angular": "1.4.x", @@ -39,6 +40,7 @@ "angular-xml": "~2.1.1", "angularjs-toaster": "~0.4.15", "appirio-tech-ng-iso-constants": "git@github.com:appirio-tech/ng-iso-constants#~1.0.5", + "appirio-tech-ng-ui-components": "1.x.x", "d3": "~3.5.6", "fontawesome": "~4.3.0", "jstzdetect": "~1.0.6", diff --git a/package.json b/package.json index 172737628..2d4c5fe8b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "homepage": "https://github.com/appirio-tech/topcoder-app#readme", "devDependencies": { "appirio-gulp-tasks": "3.x.x", - "appirio-styles": "0.x.x", + "appirio-styles": "https://github.com/appirio-tech/styles.git#panels", "bower": "^1.6.8", "gulp": "^3.9.0", "wiredep": "^2.2.2"