├── .gitignore ├── LICENSE.md ├── README.md ├── ScreenShot.png ├── package.json ├── scripts ├── build-css.js ├── build-css.ts ├── run-tests.js ├── run-tests.ts └── tslint.json ├── server.js ├── server.ts ├── server ├── api │ ├── images-api.js │ └── images-api.ts ├── data │ ├── images.json │ └── images │ │ ├── full │ │ ├── Beach.jpg │ │ ├── Christmas Cactus.jpg │ │ ├── Clouds.jpg │ │ ├── Creek and Rocks.jpg │ │ ├── Creek.jpg │ │ ├── Field.jpg │ │ ├── Flower.jpg │ │ ├── Icicles.jpg │ │ ├── Lake.jpg │ │ ├── Leaves One.jpg │ │ ├── Leaves Two.jpg │ │ ├── Luna Moth.jpg │ │ ├── Moonlight.jpg │ │ ├── Moss and Leaf.jpg │ │ ├── Mountains.jpg │ │ ├── Ocean.jpg │ │ ├── Octopus.jpg │ │ ├── Pond Steps.jpg │ │ ├── Pond.jpg │ │ ├── Reef.jpg │ │ ├── River.jpg │ │ ├── Smoky Mountains.jpg │ │ ├── Stream.jpg │ │ ├── Tree One.jpg │ │ ├── Tree Two.jpg │ │ ├── Waterfall.jpg │ │ └── Woods.jpg │ │ └── thumbs │ │ ├── Beach.jpg │ │ ├── Christmas Cactus.jpg │ │ ├── Clouds.jpg │ │ ├── Creek and Rocks.jpg │ │ ├── Creek.jpg │ │ ├── Field.jpg │ │ ├── Flower.jpg │ │ ├── Icicles.jpg │ │ ├── Lake.jpg │ │ ├── Leaves One.jpg │ │ ├── Leaves Two.jpg │ │ ├── Luna Moth.jpg │ │ ├── Moonlight.jpg │ │ ├── Moss and Leaf.jpg │ │ ├── Mountains.jpg │ │ ├── Ocean.jpg │ │ ├── Octopus.jpg │ │ ├── Pond Steps.jpg │ │ ├── Pond.jpg │ │ ├── Reef.jpg │ │ ├── River.jpg │ │ ├── Smoky Mountains.jpg │ │ ├── Stream.jpg │ │ ├── Tree One.jpg │ │ ├── Tree Two.jpg │ │ ├── Waterfall.jpg │ │ └── Woods.jpg ├── routes.js ├── routes.ts ├── tslint.json └── utils │ ├── string-utils.js │ └── string-utils.ts ├── src ├── app │ ├── actions │ │ └── image-list-actions.ts │ ├── app-bootstrap.ts │ ├── app-routes.ts │ ├── components │ │ ├── demo-app │ │ │ ├── images-module.ts │ │ │ ├── images-routes.ts │ │ │ ├── images-section.ts │ │ │ └── layouts │ │ │ │ ├── edit-layout.css │ │ │ │ ├── edit-layout.html │ │ │ │ ├── edit-layout.scss │ │ │ │ ├── edit-layout.ts │ │ │ │ ├── list-group-layout.css │ │ │ │ ├── list-group-layout.html │ │ │ │ ├── list-group-layout.scss │ │ │ │ ├── list-group-layout.ts │ │ │ │ ├── list-layout.css │ │ │ │ ├── list-layout.html │ │ │ │ ├── list-layout.scss │ │ │ │ ├── list-layout.ts │ │ │ │ ├── view-layout.css │ │ │ │ ├── view-layout.html │ │ │ │ ├── view-layout.scss │ │ │ │ └── view-layout.ts │ │ ├── image-detail-list │ │ │ ├── image-detail-list.ts │ │ │ ├── image-detail-row.ts │ │ │ ├── image-detail-table.html │ │ │ ├── image-detail-table.ts │ │ │ ├── image-tag-selector.css │ │ │ ├── image-tag-selector.html │ │ │ ├── image-tag-selector.scss │ │ │ ├── image-tag-selector.ts │ │ │ └── sortable-column-header.ts │ │ ├── image-edit │ │ │ ├── image-edit.css │ │ │ ├── image-edit.html │ │ │ ├── image-edit.scss │ │ │ └── image-edit.ts │ │ ├── image-group-list │ │ │ ├── image-group-list.css │ │ │ ├── image-group-list.html │ │ │ ├── image-group-list.scss │ │ │ └── image-group-list.ts │ │ ├── image-view │ │ │ ├── image-view.css │ │ │ ├── image-view.html │ │ │ ├── image-view.scss │ │ │ └── image-view.ts │ │ ├── loading-indicator │ │ │ ├── loading-indicator.css │ │ │ ├── loading-indicator.html │ │ │ ├── loading-indicator.scss │ │ │ └── loading-indicator.ts │ │ ├── tag-selector │ │ │ ├── tag-selector-input.ts │ │ │ ├── tag-selector.css │ │ │ ├── tag-selector.html │ │ │ ├── tag-selector.scss │ │ │ └── tag-selector.ts │ │ └── title-bar │ │ │ ├── title-bar.html │ │ │ └── title-bar.ts │ ├── decorators │ │ └── app-store-subscriber.ts │ ├── demo-app.ts │ ├── directives │ │ └── semanti-ui-init.ts │ ├── reducers │ │ └── image-list.ts │ ├── services │ │ └── app-store.ts │ └── utils │ │ ├── app-utils.ts │ │ └── tag-utils.ts ├── css │ ├── demo-app.css │ └── demo-app.scss ├── favicon.ico ├── index.html └── system.config.js ├── tests ├── jasmine.json └── specs │ ├── actions │ └── image-list-actions.spec.ts │ ├── reducers │ └── image-list.spec.ts │ └── utils │ └── tag-utils.spec.ts ├── tsconfig.json ├── tslint.json └── typings.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | src/app/**/*.js 4 | src/app/**/*.js.map 5 | src/app/**/*.d.ts 6 | tests/**/*.js 7 | tests/**/*.js.map 8 | tests/**/*.d.ts 9 | scripts/**/*.js 10 | scripts/**/*.js.map 11 | scripts/**/*.d.ts 12 | server/**/*.js 13 | server/**/*.js.map 14 | server/**/*.d.ts 15 | server.js 16 | server.js.map 17 | server.d.ts 18 | typings/ 19 | dist/ 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dave F. Baskin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 17 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Angular/Redux Complex UI Example 3 | 4 | Example of using Angular2 and Redux together to manage a "complex" user interface. 5 | 6 | ![Screen shot](ScreenShot.png) 7 | 8 | ## Building and Running 9 | 10 | ``` 11 | git clone https://github.com/ng-cookbook/angular2-redux-complex-ui.git 12 | cd angular2-redux-complex-ui 13 | npm install 14 | npm start 15 | ``` 16 | 17 | Then navigate to `http://localhost:9988/` 18 | 19 | ## Related Blog Posts 20 | 21 | - [Big Ideas Behind Angular2](http://dfbaskin.com/posts/big-ideas-behind-angular2/) 22 | - [Container and Presentation Components in Angular2](http://dfbaskin.com/posts/container-and-presentation-components-in-angular2/) 23 | - [Yes, I Used jQuery in my Angular2 Application](http://dfbaskin.com/posts/yes-i-used-jquery-in-my-angular2-application/) 24 | - [Using Redux to Manage Angular2 Application State](http://dfbaskin.com/posts/using-redux-to-manage-application-state/) 25 | 26 | -------------------------------------------------------------------------------- /ScreenShot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/ScreenShot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-redux-complex-ui", 3 | "version": "1.0.0", 4 | "description": "Example of using Angular2 and Redux together to manage a \"complex\" user interface.", 5 | "main": "index.js", 6 | "scripts": { 7 | "web": "node server", 8 | "lint": "tslint **/*.ts --exclude \"**/*.d.ts\" --exclude \"node_modules/**/*.ts\"", 9 | "build": "npm-run-all lint build:css build:ts test", 10 | "build:css": "node scripts/build-css", 11 | "build:ts": "tsc", 12 | "test": "node scripts/run-tests", 13 | "start": "npm-run-all build web", 14 | "postinstall": "typings install" 15 | }, 16 | "keywords": [ 17 | "Redux", 18 | "Angular2" 19 | ], 20 | "author": "Dave F. Baskin ", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/ng-cookbook/angular2-redux-complex-ui.git" 25 | }, 26 | "standard": { 27 | "ignore": [ 28 | "src/app/" 29 | ] 30 | }, 31 | "engines": { 32 | "node": ">=4.0" 33 | }, 34 | "dependencies": { 35 | "@angular/common": "2.0.0", 36 | "@angular/compiler": "2.0.0", 37 | "@angular/core": "2.0.0", 38 | "@angular/forms": "2.0.0", 39 | "@angular/http": "2.0.0", 40 | "@angular/platform-browser": "2.0.0", 41 | "@angular/platform-browser-dynamic": "2.0.0", 42 | "@angular/router": "3.0.0", 43 | "core-js": "^2.4.1", 44 | "express": "^4.13.4", 45 | "font-awesome": "^4.5.0", 46 | "jquery": "^2.2.3", 47 | "lodash": "^4.6.1", 48 | "redux": "^3.3.1", 49 | "redux-thunk": "^2.0.1", 50 | "reflect-metadata": "^0.1.3", 51 | "rxjs": "5.0.0-beta.12", 52 | "semantic-ui-css": "^2.1.8", 53 | "systemjs": "0.19.27", 54 | "zone.js": "^0.6.23" 55 | }, 56 | "devDependencies": { 57 | "glob": "^7.0.3", 58 | "jasmine": "^2.4.1", 59 | "jasmine-spec-reporter": "^2.4.0", 60 | "node-sass": "^3.4.2", 61 | "npm-run-all": "^3.1.0", 62 | "q": "^1.4.1", 63 | "tslint": "^3.5.0", 64 | "typescript": "^1.8.10", 65 | "typings": "^1.3.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/build-css.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var FS = require('fs'); 3 | var Q = require('q'); 4 | var glob = require('glob'); 5 | var sass = require('node-sass'); 6 | var globSearch = Q.denodeify(glob); 7 | var sassRender = Q.nbind(sass.render, sass); 8 | var writeFile = Q.denodeify(FS.writeFile); 9 | var sassDefaults = { 10 | includePaths: [], 11 | indentedSyntax: true 12 | }; 13 | globSearch('src/**/*.scss') 14 | .then(function (files) { 15 | var result = Q(); 16 | files.forEach(function (srcFile) { 17 | result = result 18 | .then(function () { 19 | var dstFile = srcFile.replace(/\.scss$/i, '.css'); 20 | return buildSassFile(srcFile, dstFile); 21 | }); 22 | }); 23 | return result; 24 | }) 25 | .then(function () { 26 | console.log('Compiled CSS files.'); 27 | }) 28 | .catch(function (err) { 29 | console.log('SASS Error:'); 30 | console.log(err); 31 | }); 32 | function buildSassFile(srcFile, dstFile) { 33 | var sassOptions = _.defaults({ file: srcFile }, sassDefaults); 34 | return sassRender(sassOptions) 35 | .then(function (result) { 36 | return writeFile(dstFile, result.css); 37 | }); 38 | } 39 | //# sourceMappingURL=build-css.js.map -------------------------------------------------------------------------------- /scripts/build-css.ts: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const FS = require('fs') 3 | const Q = require('q') 4 | const glob = require('glob') 5 | const sass = require('node-sass') 6 | 7 | let globSearch = Q.denodeify(glob) 8 | let sassRender = Q.nbind(sass.render, sass) 9 | let writeFile = Q.denodeify(FS.writeFile) 10 | let sassDefaults = { 11 | includePaths: [ 12 | ], 13 | indentedSyntax: true 14 | } 15 | 16 | globSearch('src/**/*.scss') 17 | .then((files) => { 18 | var result = Q() 19 | files.forEach((srcFile) => { 20 | result = result 21 | .then(() => { 22 | let dstFile = srcFile.replace(/\.scss$/i, '.css') 23 | return buildSassFile(srcFile, dstFile) 24 | }) 25 | }) 26 | return result 27 | }) 28 | .then(() => { 29 | console.log('Compiled CSS files.') 30 | }) 31 | .catch((err) => { 32 | console.log('SASS Error:') 33 | console.log(err) 34 | }) 35 | 36 | function buildSassFile (srcFile, dstFile) { 37 | let sassOptions = _.defaults({ file: srcFile }, sassDefaults) 38 | return sassRender(sassOptions) 39 | .then((result) => { 40 | return writeFile(dstFile, result.css) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /scripts/run-tests.js: -------------------------------------------------------------------------------- 1 | var Jasmine = require('jasmine'); 2 | var Reporter = require('jasmine-spec-reporter'); 3 | var testRunner = new Jasmine(); 4 | testRunner.loadConfigFile('tests/jasmine.json'); 5 | testRunner.addReporter(new Reporter({ 6 | isVerbose: false, 7 | showColors: true, 8 | includeStackTrace: false 9 | })); 10 | testRunner.execute(); 11 | //# sourceMappingURL=run-tests.js.map -------------------------------------------------------------------------------- /scripts/run-tests.ts: -------------------------------------------------------------------------------- 1 | 2 | const Jasmine = require('jasmine') 3 | const Reporter = require('jasmine-spec-reporter') 4 | 5 | let testRunner = new Jasmine() 6 | testRunner.loadConfigFile('tests/jasmine.json') 7 | testRunner.addReporter(new Reporter({ 8 | isVerbose: false, 9 | showColors: true, 10 | includeStackTrace: false 11 | })) 12 | testRunner.execute() 13 | -------------------------------------------------------------------------------- /scripts/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-require-imports": false, 4 | "no-var-requires": false 5 | } 6 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | require('core-js'); 3 | var routes_1 = require('./server/routes'); 4 | var portNum = process.env.PORT || 9988; 5 | routes_1.app.listen(portNum, function () { 6 | console.log('Web application listening on port ' + portNum); 7 | }); 8 | //# sourceMappingURL=server.js.map -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import {app} from './server/routes' 3 | 4 | const portNum = process.env.PORT || 9988 5 | 6 | app.listen(portNum, () => { 7 | console.log('Web application listening on port ' + portNum) 8 | }) 9 | -------------------------------------------------------------------------------- /server/api/images-api.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var path = require('path'); 3 | var string_utils_1 = require('../utils/string-utils'); 4 | var express = require('express'); 5 | var imageData = require('../data/images.json'); 6 | var dataPath = path.join(__dirname, '../data'); 7 | exports.imageApiRoutes = express.Router(); 8 | exports.imageApiRoutes.get('/images', function (req, res) { 9 | res.json(imageData); 10 | }); 11 | exports.imageApiRoutes.get('/images/:id', findImage, function (req, res) { 12 | res.json(req.imageInfo); 13 | }); 14 | exports.imageApiRoutes.get('/images/:id/image', findImage, function (req, res) { 15 | var imageFile = path.join(dataPath, 'images', 'full', req.imageInfo.fileName); 16 | res.sendFile(imageFile); 17 | }); 18 | exports.imageApiRoutes.get('/images/:id/thumb', findImage, function (req, res) { 19 | var imageFile = path.join(dataPath, 'images', 'thumbs', req.imageInfo.fileName); 20 | res.sendFile(imageFile); 21 | }); 22 | function findImage(req, res, next) { 23 | var id = req.params.id; 24 | var imageInfo = imageData.find(function (img) { return string_utils_1.isMatchingText(img.id, id); }); 25 | if (!imageInfo) { 26 | return res.sendStatus(404); 27 | } 28 | req.imageInfo = imageInfo; 29 | next(); 30 | } 31 | //# sourceMappingURL=images-api.js.map -------------------------------------------------------------------------------- /server/api/images-api.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path' 3 | import {isMatchingText} from '../utils/string-utils' 4 | const express = require('express') 5 | const imageData = require('../data/images.json') 6 | 7 | const dataPath = path.join(__dirname, '../data') 8 | 9 | export const imageApiRoutes = express.Router() 10 | 11 | imageApiRoutes.get('/images', (req, res) => { 12 | res.json(imageData) 13 | }) 14 | 15 | imageApiRoutes.get('/images/:id', findImage, (req, res) => { 16 | res.json(req.imageInfo) 17 | }) 18 | 19 | imageApiRoutes.get('/images/:id/image', findImage, (req, res) => { 20 | let imageFile = path.join(dataPath, 'images', 'full', req.imageInfo.fileName) 21 | res.sendFile(imageFile) 22 | }) 23 | 24 | imageApiRoutes.get('/images/:id/thumb', findImage, (req, res) => { 25 | let imageFile = path.join(dataPath, 'images', 'thumbs', req.imageInfo.fileName) 26 | res.sendFile(imageFile) 27 | }) 28 | 29 | function findImage (req, res, next) { 30 | let id = req.params.id 31 | let imageInfo = imageData.find((img) => isMatchingText(img.id, id)) 32 | if (!imageInfo) { 33 | return res.sendStatus(404) 34 | } 35 | req.imageInfo = imageInfo 36 | next() 37 | } 38 | 39 | -------------------------------------------------------------------------------- /server/data/images.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "beach", 4 | "fileName": "Beach.jpg", 5 | "title": "Beach", 6 | "size": 4497665, 7 | "dateTaken": "2014-02-16T10:58:34", 8 | "width": 2000, 9 | "height": 3552, 10 | "portrait": true, 11 | "tags": [ 12 | "water", 13 | "ocean" 14 | ] 15 | }, 16 | { 17 | "id": "christmas-cactus", 18 | "fileName": "Christmas Cactus.jpg", 19 | "title": "Christmas Cactus", 20 | "size": 2296281, 21 | "dateTaken": "2014-11-22T10:46:13", 22 | "width": 3552, 23 | "height": 2000, 24 | "landscape": true, 25 | "tags": [ 26 | "flowers" 27 | ] 28 | }, 29 | { 30 | "id": "clouds", 31 | "fileName": "Clouds.jpg", 32 | "title": "Clouds", 33 | "size": 1567612, 34 | "dateTaken": "2014-06-17T20:56:56", 35 | "width": 3552, 36 | "height": 2000, 37 | "landscape": true, 38 | "tags": [ 39 | "sky" 40 | ] 41 | }, 42 | { 43 | "id": "creek-and-rocks", 44 | "fileName": "Creek and Rocks.jpg", 45 | "title": "Creek and Rocks", 46 | "size": 3406175, 47 | "dateTaken": "2014-08-03T11:40:41", 48 | "width": 3552, 49 | "height": 2000, 50 | "landscape": true, 51 | "tags": [ 52 | "water", 53 | "rocks" 54 | ] 55 | }, 56 | { 57 | "id": "creek", 58 | "fileName": "Creek.jpg", 59 | "title": "Creek", 60 | "size": 3957470, 61 | "dateTaken": "2014-02-03T13:00:16", 62 | "width": 3552, 63 | "height": 2000, 64 | "landscape": true, 65 | "tags": [ 66 | "water" 67 | ] 68 | }, 69 | { 70 | "id": "field", 71 | "fileName": "Field.jpg", 72 | "title": "Field", 73 | "size": 2343100, 74 | "dateTaken": "2014-01-05T14:03:32", 75 | "width": 3552, 76 | "height": 2000, 77 | "landscape": true, 78 | "tags": [ 79 | "field" 80 | ] 81 | }, 82 | { 83 | "id": "flower", 84 | "fileName": "Flower.jpg", 85 | "title": "Flower", 86 | "size": 1445708, 87 | "dateTaken": "2014-11-02T16:28:27", 88 | "width": 2000, 89 | "height": 3552, 90 | "portrait": true, 91 | "tags": [ 92 | "flowers" 93 | ] 94 | }, 95 | { 96 | "id": "icicles", 97 | "fileName": "Icicles.jpg", 98 | "title": "Icicles", 99 | "size": 2783533, 100 | "dateTaken": "2014-02-16T14:31:38", 101 | "width": 3552, 102 | "height": 2000, 103 | "landscape": true, 104 | "tags": [ 105 | "water", 106 | "woods" 107 | ] 108 | }, 109 | { 110 | "id": "lake", 111 | "fileName": "Lake.jpg", 112 | "title": "Lake", 113 | "size": 2127018, 114 | "dateTaken": "2014-07-02T21:01:58", 115 | "width": 3552, 116 | "height": 2000, 117 | "landscape": true, 118 | "tags": [ 119 | "water" 120 | ] 121 | }, 122 | { 123 | "id": "leaves-one", 124 | "fileName": "Leaves One.jpg", 125 | "title": "Leaves One", 126 | "size": 3531544, 127 | "dateTaken": "2014-10-04T14:59:54", 128 | "width": 3552, 129 | "height": 2000, 130 | "landscape": true, 131 | "tags": [ 132 | "leaves", 133 | "woods" 134 | ] 135 | }, 136 | { 137 | "id": "leaves-two", 138 | "fileName": "Leaves Two.jpg", 139 | "title": "Leaves Two", 140 | "size": 3296138, 141 | "dateTaken": "2014-10-04T14:59:50", 142 | "width": 3552, 143 | "height": 2000, 144 | "landscape": true, 145 | "tags": [ 146 | "leaves", 147 | "woods" 148 | ] 149 | }, 150 | { 151 | "id": "luna-moth", 152 | "fileName": "Luna Moth.jpg", 153 | "title": "Luna Moth", 154 | "size": 2203407, 155 | "dateTaken": "2014-08-02T13:16:29", 156 | "width": 2000, 157 | "height": 3552, 158 | "portrait": true, 159 | "tags": [ 160 | "animals" 161 | ] 162 | }, 163 | { 164 | "id": "moonlight", 165 | "fileName": "Moonlight.jpg", 166 | "title": "Moonlight", 167 | "size": 2089603, 168 | "dateTaken": "2014-07-12T23:12:32", 169 | "width": 3552, 170 | "height": 2000, 171 | "landscape": true, 172 | "tags": [ 173 | "sky" 174 | ] 175 | }, 176 | { 177 | "id": "moss-and-leaf", 178 | "fileName": "Moss and Leaf.jpg", 179 | "title": "Moss and Leaf", 180 | "size": 2849103, 181 | "dateTaken": "2014-08-03T11:44:36", 182 | "width": 3552, 183 | "height": 2000, 184 | "landscape": true, 185 | "tags": [ 186 | "leaves", 187 | "woods" 188 | ] 189 | }, 190 | { 191 | "id": "mountains", 192 | "fileName": "Mountains.jpg", 193 | "title": "Mountains", 194 | "size": 2013406, 195 | "dateTaken": "2014-01-15T11:00:20", 196 | "width": 3552, 197 | "height": 2000, 198 | "landscape": true, 199 | "tags": [ 200 | "mountains" 201 | ] 202 | }, 203 | { 204 | "id": "ocean", 205 | "fileName": "Ocean.jpg", 206 | "title": "Ocean", 207 | "size": 3988856, 208 | "dateTaken": "2014-02-16T10:58:24", 209 | "width": 3552, 210 | "height": 2000, 211 | "landscape": true, 212 | "tags": [ 213 | "ocean", 214 | "water" 215 | ] 216 | }, 217 | { 218 | "id": "octopus", 219 | "fileName": "Octopus.jpg", 220 | "title": "Octopus", 221 | "size": 3003644, 222 | "dateTaken": "2014-08-28T19:29:30", 223 | "width": 3552, 224 | "height": 2000, 225 | "landscape": true, 226 | "tags": [ 227 | "animals", 228 | "water" 229 | ] 230 | }, 231 | { 232 | "id": "pond-steps", 233 | "fileName": "Pond Steps.jpg", 234 | "title": "Pond Steps", 235 | "size": 2860438, 236 | "dateTaken": "2014-10-30T15:49:29", 237 | "width": 2000, 238 | "height": 3552, 239 | "portrait": true, 240 | "tags": [ 241 | "water", 242 | "woods" 243 | ] 244 | }, 245 | { 246 | "id": "pond", 247 | "fileName": "Pond.jpg", 248 | "title": "Pond", 249 | "size": 3631675, 250 | "dateTaken": "2014-10-30T15:50:26", 251 | "width": 2000, 252 | "height": 3552, 253 | "portrait": true, 254 | "tags": [ 255 | "water", 256 | "woods" 257 | ] 258 | }, 259 | { 260 | "id": "reef", 261 | "fileName": "Reef.jpg", 262 | "title": "Reef", 263 | "size": 5565099, 264 | "dateTaken": "2014-02-16T11:25:16", 265 | "width": 2000, 266 | "height": 3552, 267 | "portrait": true, 268 | "tags": [ 269 | "ocean", 270 | "water" 271 | ] 272 | }, 273 | { 274 | "id": "river", 275 | "fileName": "River.jpg", 276 | "title": "River", 277 | "size": 2735207, 278 | "dateTaken": "2013-12-22T16:43:10", 279 | "width": 3552, 280 | "height": 2000, 281 | "landscape": true, 282 | "tags": [ 283 | "woods", 284 | "water" 285 | ] 286 | }, 287 | { 288 | "id": "smoky-mountains", 289 | "fileName": "Smoky Mountains.jpg", 290 | "title": "Smoky Mountains", 291 | "size": 2123949, 292 | "dateTaken": "2014-11-15T15:03:14", 293 | "width": 3552, 294 | "height": 2000, 295 | "landscape": true, 296 | "tags": [ 297 | "mountains", 298 | "sky", 299 | "woods" 300 | ] 301 | }, 302 | { 303 | "id": "stream", 304 | "fileName": "Stream.jpg", 305 | "title": "Stream", 306 | "size": 2579914, 307 | "dateTaken": "2014-08-03T11:39:42", 308 | "width": 3552, 309 | "height": 2000, 310 | "landscape": true, 311 | "tags": [ 312 | "water", 313 | "woods" 314 | ] 315 | }, 316 | { 317 | "id": "tree-one", 318 | "fileName": "Tree One.jpg", 319 | "title": "Tree One", 320 | "size": 3896088, 321 | "dateTaken": "2014-10-29T14:43:50", 322 | "width": 2000, 323 | "height": 3552, 324 | "portrait": true, 325 | "tags": [ 326 | "woods" 327 | ] 328 | }, 329 | { 330 | "id": "tree-two", 331 | "fileName": "Tree Two.jpg", 332 | "title": "Tree Two", 333 | "size": 3481590, 334 | "dateTaken": "2014-10-29T14:42:30", 335 | "width": 3552, 336 | "height": 2000, 337 | "landscape": true, 338 | "tags": [ 339 | "woods" 340 | ] 341 | }, 342 | { 343 | "id": "waterfall", 344 | "fileName": "Waterfall.jpg", 345 | "title": "Waterfall", 346 | "size": 3961798, 347 | "dateTaken": "2014-02-03T13:02:57", 348 | "width": 2000, 349 | "height": 3552, 350 | "portrait": true, 351 | "tags": [ 352 | "water", 353 | "woods" 354 | ] 355 | }, 356 | { 357 | "id": "woods", 358 | "fileName": "Woods.jpg", 359 | "title": "Woods", 360 | "size": 3364043, 361 | "dateTaken": "2014-06-19T19:06:57", 362 | "width": 3552, 363 | "height": 2000, 364 | "landscape": true, 365 | "tags": [ 366 | "woods" 367 | ] 368 | } 369 | ] 370 | -------------------------------------------------------------------------------- /server/data/images/full/Beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Beach.jpg -------------------------------------------------------------------------------- /server/data/images/full/Christmas Cactus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Christmas Cactus.jpg -------------------------------------------------------------------------------- /server/data/images/full/Clouds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Clouds.jpg -------------------------------------------------------------------------------- /server/data/images/full/Creek and Rocks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Creek and Rocks.jpg -------------------------------------------------------------------------------- /server/data/images/full/Creek.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Creek.jpg -------------------------------------------------------------------------------- /server/data/images/full/Field.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Field.jpg -------------------------------------------------------------------------------- /server/data/images/full/Flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Flower.jpg -------------------------------------------------------------------------------- /server/data/images/full/Icicles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Icicles.jpg -------------------------------------------------------------------------------- /server/data/images/full/Lake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Lake.jpg -------------------------------------------------------------------------------- /server/data/images/full/Leaves One.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Leaves One.jpg -------------------------------------------------------------------------------- /server/data/images/full/Leaves Two.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Leaves Two.jpg -------------------------------------------------------------------------------- /server/data/images/full/Luna Moth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Luna Moth.jpg -------------------------------------------------------------------------------- /server/data/images/full/Moonlight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Moonlight.jpg -------------------------------------------------------------------------------- /server/data/images/full/Moss and Leaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Moss and Leaf.jpg -------------------------------------------------------------------------------- /server/data/images/full/Mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Mountains.jpg -------------------------------------------------------------------------------- /server/data/images/full/Ocean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Ocean.jpg -------------------------------------------------------------------------------- /server/data/images/full/Octopus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Octopus.jpg -------------------------------------------------------------------------------- /server/data/images/full/Pond Steps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Pond Steps.jpg -------------------------------------------------------------------------------- /server/data/images/full/Pond.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Pond.jpg -------------------------------------------------------------------------------- /server/data/images/full/Reef.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Reef.jpg -------------------------------------------------------------------------------- /server/data/images/full/River.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/River.jpg -------------------------------------------------------------------------------- /server/data/images/full/Smoky Mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Smoky Mountains.jpg -------------------------------------------------------------------------------- /server/data/images/full/Stream.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Stream.jpg -------------------------------------------------------------------------------- /server/data/images/full/Tree One.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Tree One.jpg -------------------------------------------------------------------------------- /server/data/images/full/Tree Two.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Tree Two.jpg -------------------------------------------------------------------------------- /server/data/images/full/Waterfall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Waterfall.jpg -------------------------------------------------------------------------------- /server/data/images/full/Woods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/full/Woods.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Beach.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Christmas Cactus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Christmas Cactus.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Clouds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Clouds.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Creek and Rocks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Creek and Rocks.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Creek.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Creek.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Field.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Field.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Flower.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Icicles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Icicles.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Lake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Lake.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Leaves One.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Leaves One.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Leaves Two.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Leaves Two.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Luna Moth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Luna Moth.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Moonlight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Moonlight.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Moss and Leaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Moss and Leaf.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Mountains.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Ocean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Ocean.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Octopus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Octopus.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Pond Steps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Pond Steps.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Pond.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Pond.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Reef.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Reef.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/River.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/River.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Smoky Mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Smoky Mountains.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Stream.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Stream.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Tree One.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Tree One.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Tree Two.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Tree Two.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Waterfall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Waterfall.jpg -------------------------------------------------------------------------------- /server/data/images/thumbs/Woods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/server/data/images/thumbs/Woods.jpg -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var path = require('path'); 3 | var images_api_1 = require('./api/images-api'); 4 | var express = require('express'); 5 | exports.app = express(); 6 | var vendorFiles = { 7 | 'systemjs': 'node_modules/systemjs/dist', 8 | 'corejs': 'node_modules/core-js/client', 9 | 'zonejs': 'node_modules/zone.js/dist', 10 | 'reflect-metadata': 'node_modules/reflect-metadata', 11 | 'angular': 'node_modules/@angular', 12 | 'rxjs': 'node_modules/rxjs', 13 | 'jquery': 'node_modules/jquery/dist', 14 | 'redux': 'node_modules/redux/dist', 15 | 'redux-thunk': 'node_modules/redux-thunk/lib', 16 | 'lodash': 'node_modules/lodash', 17 | 'font-awesome': 'node_modules/font-awesome', 18 | 'semantic-ui': 'node_modules/semantic-ui-css' 19 | }; 20 | for (var _i = 0, _a = Object.entries(vendorFiles); _i < _a.length; _i++) { 21 | var _b = _a[_i], name_1 = _b[0], path_1 = _b[1]; 22 | exports.app.use('/vendor/' + name_1, express.static(path_1)); 23 | } 24 | exports.app.use('/api', images_api_1.imageApiRoutes); 25 | var clientRoutes = /^\/($|images(\/|$))/i; 26 | var indexHtml = path.join(__dirname, '..', 'src', 'index.html'); 27 | exports.app.get(clientRoutes, function (req, res) { 28 | res.sendFile(indexHtml); 29 | }); 30 | exports.app.use('/', express.static('src')); 31 | //# sourceMappingURL=routes.js.map -------------------------------------------------------------------------------- /server/routes.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import {imageApiRoutes} from './api/images-api' 3 | const express = require('express') 4 | 5 | export const app = express() 6 | 7 | var vendorFiles = { 8 | 'systemjs': 'node_modules/systemjs/dist', 9 | 'corejs': 'node_modules/core-js/client', 10 | 'zonejs': 'node_modules/zone.js/dist', 11 | 'reflect-metadata': 'node_modules/reflect-metadata', 12 | 'angular': 'node_modules/@angular', 13 | 'rxjs': 'node_modules/rxjs', 14 | 'jquery': 'node_modules/jquery/dist', 15 | 'redux': 'node_modules/redux/dist', 16 | 'redux-thunk': 'node_modules/redux-thunk/lib', 17 | 'lodash': 'node_modules/lodash', 18 | 'font-awesome': 'node_modules/font-awesome', 19 | 'semantic-ui': 'node_modules/semantic-ui-css' 20 | } 21 | for (let [name, path] of Object.entries(vendorFiles)) { 22 | app.use('/vendor/' + name, express.static(path)) 23 | } 24 | 25 | app.use('/api', imageApiRoutes) 26 | 27 | let clientRoutes = /^\/($|images(\/|$))/i 28 | let indexHtml = path.join(__dirname, '..', 'src', 'index.html') 29 | app.get(clientRoutes, (req, res) => { 30 | res.sendFile(indexHtml) 31 | }) 32 | app.use('/', express.static('src')) 33 | 34 | -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-require-imports": false, 4 | "no-var-requires": false 5 | } 6 | } -------------------------------------------------------------------------------- /server/utils/string-utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function isMatchingText(text1, text2) { 3 | if (text1 == null || text2 == null) { 4 | return false; 5 | } 6 | return String(text1).trim().toLocaleLowerCase() === String(text2).trim().toLocaleLowerCase(); 7 | } 8 | exports.isMatchingText = isMatchingText; 9 | //# sourceMappingURL=string-utils.js.map -------------------------------------------------------------------------------- /server/utils/string-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function isMatchingText (text1, text2) { 3 | if (text1 == null || text2 == null) { 4 | return false 5 | } 6 | return String(text1).trim().toLocaleLowerCase() === String(text2).trim().toLocaleLowerCase() 7 | } 8 | -------------------------------------------------------------------------------- /src/app/actions/image-list-actions.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Http, Response} from '@angular/http' 3 | 4 | export const LOADING_IMAGE_DATA = 'LOADING_IMAGE_DATA' 5 | export const LOAD_IMAGE_DATA = 'LOAD_IMAGE_DATA' 6 | export const SORT_IMAGES = 'SORT_IMAGES' 7 | export const EXCLUDE_IMAGE_TAGS = 'EXCLUDE_IMAGE_TAGS' 8 | export const CLEAR_CURRENT_IMAGE = 'CLEAR_CURRENT_IMAGE' 9 | export const SELECT_CURRENT_IMAGE = 'SELECT_CURRENT_IMAGE' 10 | export const CHANGE_IMAGE_TITLE = 'CHANGE_IMAGE_TITLE' 11 | export const UPDATE_IMAGE_TAGS = 'UPDATE_IMAGE_TAGS' 12 | 13 | export enum ImageSortBy { 14 | title, 15 | size, 16 | date 17 | } 18 | 19 | export function loadingImages() { 20 | return { 21 | type: LOADING_IMAGE_DATA 22 | } 23 | } 24 | 25 | export function loadImageData(imageData) { 26 | return { 27 | type: LOAD_IMAGE_DATA, 28 | payload: imageData 29 | } 30 | } 31 | 32 | export function loadImageDataError(errorMessage) { 33 | return { 34 | type: LOAD_IMAGE_DATA, 35 | payload: { 36 | message: errorMessage 37 | }, 38 | error: true 39 | } 40 | } 41 | 42 | export function sortImages(sortBy: ImageSortBy, isAscending: boolean = true) { 43 | return { 44 | type: SORT_IMAGES, 45 | payload: { sortBy, isAscending } 46 | } 47 | } 48 | 49 | export function excludeImageTags(excludedTags: string[]) { 50 | return { 51 | type: EXCLUDE_IMAGE_TAGS, 52 | payload: { excludedTags } 53 | } 54 | } 55 | 56 | export function imageDataRequest(http: Http) { 57 | return dispatch => { 58 | dispatch(loadingImages()) 59 | http.get('/api/images') 60 | .map(res => res.json()) 61 | .map(imageData => imageData.map((img: any) => Object.assign(img, { 62 | dateTaken: new Date(img.dateTaken) 63 | }))) 64 | .subscribe( 65 | (imageData: any) => dispatch(loadImageData(imageData)), 66 | (err: Response) => dispatch(loadImageDataError(err.json().error || 'Server error')) 67 | ) 68 | } 69 | } 70 | 71 | export function clearCurrentImage() { 72 | return { 73 | type: CLEAR_CURRENT_IMAGE 74 | } 75 | } 76 | 77 | export function selectCurrentImage(imageId: string) { 78 | return { 79 | type: SELECT_CURRENT_IMAGE, 80 | payload: { imageId } 81 | } 82 | } 83 | 84 | export function changeImageTitle(imageId: string, title: string) { 85 | return { 86 | type: CHANGE_IMAGE_TITLE, 87 | payload: { imageId, title } 88 | } 89 | } 90 | 91 | export function updateImageTags(imageId: string, tags: string[]) { 92 | return { 93 | type: UPDATE_IMAGE_TAGS, 94 | payload: { imageId, tags } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/app-bootstrap.ts: -------------------------------------------------------------------------------- 1 | 2 | import {NgModule} from '@angular/core' 3 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic' 4 | import {BrowserModule} from '@angular/platform-browser' 5 | import {HttpModule} from '@angular/http' 6 | import {appRouting} from './app-routes' 7 | import {FormsModule} from '@angular/forms' 8 | import {DemoApp} from './demo-app' 9 | import {ImagesModule} from './components/demo-app/images-module' 10 | 11 | import {provideAppStore, provideReducer} from './services/app-store' 12 | import {imageData} from './reducers/image-list' 13 | 14 | import 'rxjs/add/operator/map' 15 | import 'rxjs/add/operator/filter' 16 | import 'rxjs/add/operator/do' 17 | import 'rxjs/add/operator/take' 18 | import 'rxjs/add/operator/switchMapTo' 19 | 20 | @NgModule({ 21 | declarations: [ 22 | DemoApp 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | HttpModule, 27 | FormsModule, 28 | appRouting, 29 | ImagesModule 30 | ], 31 | providers: [ 32 | provideReducer('imageData', imageData), 33 | provideAppStore() 34 | ], 35 | bootstrap: [ 36 | DemoApp 37 | ] 38 | }) 39 | export class AppModule { } 40 | 41 | platformBrowserDynamic().bootstrapModule(AppModule); 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/app/app-routes.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Routes, RouterModule} from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { path: '', redirectTo: '/images/list', pathMatch: 'full' } 6 | ] 7 | 8 | export const appRouting = RouterModule.forRoot(routes) 9 | -------------------------------------------------------------------------------- /src/app/components/demo-app/images-module.ts: -------------------------------------------------------------------------------- 1 | 2 | import {NgModule} from '@angular/core' 3 | import {CommonModule} from '@angular/common' 4 | import {FormsModule} from '@angular/forms' 5 | import {ImagesSection} from './images-section' 6 | import {imagesRouting} from './images-routes' 7 | import {ListLayout} from './layouts/list-layout' 8 | import {ListGroupLayout} from './layouts/list-group-layout' 9 | import {ViewLayout} from './layouts/view-layout' 10 | import {EditLayout} from './layouts/edit-layout' 11 | import {ImageEdit} from '../image-edit/image-edit'; 12 | import {ImageGroupList} from '../image-group-list/image-group-list'; 13 | import {ImageDetailList} from '../image-detail-list/image-detail-list'; 14 | import {TitleBar} from '../title-bar/title-bar'; 15 | import {ImageView} from '../image-view/image-view'; 16 | import {ImageDetailTable} from '../image-detail-list/image-detail-table'; 17 | import {ImageDetailRow} from '../image-detail-list/image-detail-row'; 18 | import {SortableColumnHeader} from '../image-detail-list/sortable-column-header'; 19 | import {ImageTagSelector} from '../image-detail-list/image-tag-selector'; 20 | import {AddTagOnEnter, TagSelectorInput} from '../tag-selector/tag-selector-input'; 21 | import {LoadingIndicator} from '../loading-indicator/loading-indicator'; 22 | import {InitializeDropdown} from '../../directives/semanti-ui-init'; 23 | import {TagSelector} from '../tag-selector/tag-selector'; 24 | 25 | @NgModule({ 26 | imports: [ 27 | CommonModule, 28 | FormsModule, 29 | imagesRouting 30 | ], 31 | declarations: [ 32 | ImagesSection, 33 | ListLayout, 34 | ListGroupLayout, 35 | ViewLayout, 36 | EditLayout, 37 | TitleBar, 38 | ImageDetailList, 39 | ImageGroupList, 40 | ImageEdit, 41 | ImageView, 42 | ImageDetailTable, 43 | ImageDetailRow, 44 | SortableColumnHeader, 45 | ImageTagSelector, 46 | TagSelectorInput, 47 | AddTagOnEnter, 48 | LoadingIndicator, 49 | InitializeDropdown, 50 | TagSelector 51 | ] 52 | }) 53 | export class ImagesModule { } 54 | 55 | -------------------------------------------------------------------------------- /src/app/components/demo-app/images-routes.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Routes, RouterModule} from '@angular/router' 3 | import {ImagesSection} from './images-section' 4 | import {ListLayout} from './layouts/list-layout' 5 | import {ListGroupLayout} from './layouts/list-group-layout' 6 | import {ViewLayout} from './layouts/view-layout' 7 | import {EditLayout} from './layouts/edit-layout' 8 | 9 | const imagesRoutes: Routes = [ 10 | { 11 | path: 'images', 12 | component: ImagesSection, 13 | children: [ 14 | { path: 'list', component: ListLayout }, 15 | { path: 'groups', component: ListGroupLayout }, 16 | { path: 'view/:id', component: ViewLayout }, 17 | { path: 'edit/:id', component: EditLayout } 18 | ] 19 | } 20 | ] 21 | 22 | export const imagesRouting = RouterModule.forChild(imagesRoutes) 23 | -------------------------------------------------------------------------------- /src/app/components/demo-app/images-section.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {Http} from '@angular/http' 6 | import {AppStore} from '../../services/app-store' 7 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../decorators/app-store-subscriber' 8 | import {imageDataRequest} from '../../actions/image-list-actions' 9 | 10 | @Component({ 11 | selector: 'demo-app', 12 | template: `` 13 | }) 14 | @AppStoreSubscriber() 15 | export class ImagesSection implements IAppStoreSubscriber { 16 | 17 | public state = {} 18 | 19 | constructor( 20 | private appStore: AppStore, 21 | private http: Http) { 22 | } 23 | 24 | public onInitAppStoreSubscription(source: Observable): Subscription { 25 | return source 26 | .map((state: any) => ({ 27 | isLoading: state.imageData.isLoading 28 | })) 29 | .subscribe((componentState: any) => { 30 | this.state = componentState 31 | }) 32 | } 33 | 34 | public ngOnInit() { 35 | // simulate a long load time 36 | setTimeout(() => this.appStore.dispatch(imageDataRequest(this.http)), 2000) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/edit-layout.css: -------------------------------------------------------------------------------- 1 | .root-layout { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; } 11 | 12 | .root-layout > div:nth-of-type(1) { 13 | flex: 0 0 auto; } 14 | 15 | .root-layout > div:nth-of-type(2) { 16 | flex: 1 1 50%; 17 | position: relative; 18 | overflow: auto; 19 | display: flex; 20 | flex-direction: row; 21 | flex-wrap: nowrap; 22 | justify-content: flex-start; } 23 | 24 | .root-layout > div:nth-of-type(2) > div:nth-of-type(1) { 25 | flex: 0 0 50%; 26 | position: relative; 27 | overflow: auto; } 28 | 29 | .root-layout > div:nth-of-type(2) > div:nth-of-type(2) { 30 | flex: 0 0 50%; 31 | position: relative; 32 | overflow: auto; } 33 | 34 | .root-layout > div:nth-of-type(3) { 35 | border-top: 1px solid #aaa; 36 | flex: 1 1 50%; 37 | position: relative; } 38 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/edit-layout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/edit-layout.scss: -------------------------------------------------------------------------------- 1 | 2 | .root-layout { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | flex-wrap: nowrap; 11 | justify-content: flex-start; 12 | } 13 | 14 | .root-layout > div:nth-of-type(1) { 15 | flex: 0 0 auto; 16 | } 17 | 18 | .root-layout > div:nth-of-type(2) { 19 | flex: 1 1 50%; 20 | position: relative; 21 | overflow: auto; 22 | 23 | display: flex; 24 | flex-direction: row; 25 | flex-wrap: nowrap; 26 | justify-content: flex-start; 27 | } 28 | 29 | .root-layout > div:nth-of-type(2) > div:nth-of-type(1) { 30 | flex: 0 0 50%; 31 | position: relative; 32 | overflow: auto; 33 | } 34 | 35 | .root-layout > div:nth-of-type(2) > div:nth-of-type(2) { 36 | flex: 0 0 50%; 37 | position: relative; 38 | overflow: auto; 39 | } 40 | 41 | .root-layout > div:nth-of-type(3) { 42 | border-top: 1px solid #aaa; 43 | flex: 1 1 50%; 44 | position: relative; 45 | } 46 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/edit-layout.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {ActivatedRoute} from '@angular/router' 6 | import {AppStore} from '../../../services/app-store' 7 | import {selectCurrentImage} from '../../../actions/image-list-actions' 8 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../../decorators/app-store-subscriber' 9 | import {waitForImageListToLoad, watchForImageIdChanges} from '../../../utils/app-utils'; 10 | 11 | @Component({ 12 | selector: 'edit-layout', 13 | templateUrl: 'app/components/demo-app/layouts/edit-layout.html', 14 | styleUrls: ['app/components/demo-app/layouts/edit-layout.css'] 15 | }) 16 | @AppStoreSubscriber() 17 | export class EditLayout implements IAppStoreSubscriber { 18 | 19 | public isLoading: boolean = true 20 | 21 | constructor( 22 | private route: ActivatedRoute, 23 | private appStore: AppStore) { 24 | } 25 | 26 | public onInitAppStoreSubscription(source: Observable): Subscription { 27 | return waitForImageListToLoad(source) 28 | .do(() => this.isLoading = false) 29 | .switchMapTo(watchForImageIdChanges(this.route.params)) 30 | .subscribe((imageId: string) => { 31 | this.appStore.dispatch(selectCurrentImage(imageId)) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-group-layout.css: -------------------------------------------------------------------------------- 1 | .root-layout { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; } 11 | 12 | .root-layout > div:nth-of-type(1) { 13 | flex: 0 0 auto; } 14 | 15 | .root-layout > div:nth-of-type(2) { 16 | flex: 1 1 auto; 17 | position: relative; 18 | overflow: auto; 19 | display: flex; 20 | flex-direction: row; 21 | flex-wrap: nowrap; 22 | justify-content: flex-start; } 23 | 24 | .root-layout > div:nth-of-type(2) > div:nth-of-type(1) { 25 | flex: 0 0 50%; 26 | position: relative; 27 | overflow: auto; } 28 | 29 | .root-layout > div:nth-of-type(2) > div:nth-of-type(2) { 30 | flex: 0 0 50%; 31 | position: relative; 32 | overflow: auto; } 33 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-group-layout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-group-layout.scss: -------------------------------------------------------------------------------- 1 | 2 | .root-layout { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | flex-wrap: nowrap; 11 | justify-content: flex-start; 12 | } 13 | 14 | .root-layout > div:nth-of-type(1) { 15 | flex: 0 0 auto; 16 | } 17 | 18 | .root-layout > div:nth-of-type(2) { 19 | flex: 1 1 auto; 20 | position: relative; 21 | overflow: auto; 22 | 23 | display: flex; 24 | flex-direction: row; 25 | flex-wrap: nowrap; 26 | justify-content: flex-start; 27 | } 28 | 29 | .root-layout > div:nth-of-type(2) > div:nth-of-type(1) { 30 | flex: 0 0 50%; 31 | position: relative; 32 | overflow: auto; 33 | } 34 | 35 | .root-layout > div:nth-of-type(2) > div:nth-of-type(2) { 36 | flex: 0 0 50%; 37 | position: relative; 38 | overflow: auto; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-group-layout.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {IAppStoreSubscriber, AppStoreSubscriber} from '../../../decorators/app-store-subscriber'; 6 | import {waitForImageListToLoad} from '../../../utils/app-utils'; 7 | 8 | @Component({ 9 | selector: 'list-group-layout', 10 | templateUrl: 'app/components/demo-app/layouts/list-group-layout.html', 11 | styleUrls: ['app/components/demo-app/layouts/list-group-layout.css'] 12 | }) 13 | @AppStoreSubscriber() 14 | export class ListGroupLayout implements IAppStoreSubscriber { 15 | 16 | public isLoading: boolean = true; 17 | 18 | public onInitAppStoreSubscription(source: Observable): Subscription { 19 | return waitForImageListToLoad(source) 20 | .subscribe(() => this.isLoading = false) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-layout.css: -------------------------------------------------------------------------------- 1 | .root-layout { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; } 11 | 12 | .root-layout > div:nth-of-type(1) { 13 | flex: 0 0 auto; } 14 | 15 | .root-layout > div:nth-of-type(2) { 16 | flex: 1 1 auto; 17 | position: relative; 18 | overflow: auto; } 19 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-layout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-layout.scss: -------------------------------------------------------------------------------- 1 | 2 | .root-layout { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | flex-wrap: nowrap; 11 | justify-content: flex-start; 12 | } 13 | 14 | .root-layout > div:nth-of-type(1) { 15 | flex: 0 0 auto; 16 | } 17 | 18 | .root-layout > div:nth-of-type(2) { 19 | flex: 1 1 auto; 20 | position: relative; 21 | overflow: auto; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/list-layout.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../../decorators/app-store-subscriber'; 6 | import {waitForImageListToLoad} from '../../../utils/app-utils'; 7 | 8 | @Component({ 9 | selector: 'list-layout', 10 | templateUrl: 'app/components/demo-app/layouts/list-layout.html', 11 | styleUrls: ['app/components/demo-app/layouts/list-layout.css'] 12 | }) 13 | @AppStoreSubscriber() 14 | export class ListLayout implements IAppStoreSubscriber { 15 | 16 | public isLoading: boolean = true; 17 | 18 | public onInitAppStoreSubscription(source: Observable): Subscription { 19 | return waitForImageListToLoad(source) 20 | .subscribe(() => this.isLoading = false) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/view-layout.css: -------------------------------------------------------------------------------- 1 | .root-layout { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; 11 | align-items: stretch; 12 | align-content: stretch; } 13 | 14 | .root-layout > div:nth-of-type(1) { 15 | flex: 0 0 auto; } 16 | 17 | .root-layout > div:nth-of-type(2) { 18 | flex: 1 1 auto; 19 | position: relative; } 20 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/view-layout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/view-layout.scss: -------------------------------------------------------------------------------- 1 | 2 | .root-layout { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | flex-wrap: nowrap; 11 | justify-content: flex-start; 12 | align-items: stretch; 13 | align-content: stretch; 14 | } 15 | 16 | .root-layout > div:nth-of-type(1) { 17 | flex: 0 0 auto; 18 | } 19 | 20 | .root-layout > div:nth-of-type(2) { 21 | flex: 1 1 auto; 22 | position: relative; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/components/demo-app/layouts/view-layout.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {ActivatedRoute} from '@angular/router' 6 | import {AppStore} from '../../../services/app-store' 7 | import {selectCurrentImage} from '../../../actions/image-list-actions' 8 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../../decorators/app-store-subscriber' 9 | import {waitForImageListToLoad, watchForImageIdChanges} from '../../../utils/app-utils'; 10 | 11 | @Component({ 12 | selector: 'view-layout', 13 | templateUrl: 'app/components/demo-app/layouts/view-layout.html', 14 | styleUrls: ['app/components/demo-app/layouts/view-layout.css'] 15 | }) 16 | @AppStoreSubscriber() 17 | export class ViewLayout implements IAppStoreSubscriber { 18 | 19 | public isLoading: boolean = true; 20 | 21 | constructor( 22 | private route: ActivatedRoute, 23 | private appStore: AppStore) { 24 | } 25 | 26 | public onInitAppStoreSubscription(source: Observable): Subscription { 27 | return waitForImageListToLoad(source) 28 | .do(() => this.isLoading = false) 29 | .switchMapTo(watchForImageIdChanges(this.route.params)) 30 | .subscribe((imageId: string) => { 31 | this.appStore.dispatch(selectCurrentImage(imageId)) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-detail-list.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash' 3 | import {Component} from '@angular/core' 4 | import {Observable} from 'rxjs/Observable' 5 | import {Subscription} from 'rxjs/Subscription' 6 | import {AppStore} from '../../services/app-store' 7 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../decorators/app-store-subscriber' 8 | import {sortImages, ImageSortBy} from '../../actions/image-list-actions' 9 | 10 | @Component({ 11 | selector: 'image-detail-list', 12 | template: ` 13 |
14 |
15 | 23 |
24 |
25 | ` 26 | }) 27 | @AppStoreSubscriber() 28 | export class ImageDetailList implements IAppStoreSubscriber { 29 | 30 | public imageList: any[]; 31 | private sortBy: ImageSortBy; 32 | private isAscending: boolean = true; 33 | 34 | constructor(private appStore: AppStore) { 35 | } 36 | 37 | public onInitAppStoreSubscription(source: Observable): Subscription { 38 | return source 39 | .subscribe((state: any) => { 40 | this.sortBy = state.imageData.sortBy; 41 | this.isAscending = state.imageData.isAscending; 42 | this.imageList = _.map(state.imageData.displayedItems, (v: any) => { 43 | return state.imageData.dataSet[v] 44 | }) 45 | }) 46 | } 47 | 48 | public sortByTitle() { 49 | this.appStore.dispatch(sortImages(ImageSortBy.title, this.sortAscending(ImageSortBy.title))) 50 | } 51 | 52 | public sortBySize() { 53 | this.appStore.dispatch(sortImages(ImageSortBy.size, this.sortAscending(ImageSortBy.size))) 54 | } 55 | 56 | public sortByDate() { 57 | this.appStore.dispatch(sortImages(ImageSortBy.date, this.sortAscending(ImageSortBy.date))) 58 | } 59 | 60 | private sortAscending(requestedSortBy: ImageSortBy) { 61 | return this.sortBy === requestedSortBy ? !this.isAscending : true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-detail-row.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core' 2 | import {ActivatedRoute} from '@angular/router' 3 | 4 | @Component({ 5 | selector: '.image-detail-row', 6 | template: ` 7 | 8 | 9 | {{rowData.title}} 10 | 11 | 12 | {{rowData.size | number}} 13 | {{rowData.dateTaken | date}} 14 | {{rowData.width | number}} x {{rowData.height | number}} 15 | {{rowData.portrait ? "Portrait" : ""}}{{rowData.landscape ? "Landscape" : ""}} 16 | {{rowData.tags}} 17 | ` 18 | }) 19 | export class ImageDetailRow { 20 | @Input() public rowData: any 21 | 22 | private isEditRoute: boolean = false; 23 | 24 | constructor( 25 | private route: ActivatedRoute) { 26 | } 27 | 28 | public ngOnInit() { 29 | this.route.url 30 | .subscribe((urlPaths) => { 31 | this.isEditRoute = urlPaths[0] && urlPaths[0].path && urlPaths[0].path === 'edit'; 32 | }) 33 | } 34 | 35 | public imageRouteFor(img) { 36 | return [ 37 | '/images', 38 | this.isEditRoute ? 'edit' : 'view', 39 | img.id 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-detail-table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 |
TitleSize (bytes)TakenDimensionsOrientation 10 | 11 | Tags 12 |
16 |
23 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-detail-table.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, Input, Output, EventEmitter} from '@angular/core' 3 | import {AppStore} from '../../services/app-store' 4 | import {ImageSortBy} from '../../actions/image-list-actions' 5 | 6 | @Component({ 7 | selector: 'image-detail-table', 8 | templateUrl: 'app/components/image-detail-list/image-detail-table.html' 9 | }) 10 | export class ImageDetailTable { 11 | 12 | @Input() public tableData: any; 13 | @Input() public sortBy: ImageSortBy; 14 | @Input() public isAscending: boolean; 15 | @Output() public toggleTitleSort: EventEmitter = new EventEmitter(); 16 | @Output() public toggleSizeSort: EventEmitter = new EventEmitter(); 17 | @Output() public toggleDateSort: EventEmitter = new EventEmitter(); 18 | 19 | public showTagSelector: boolean = false; 20 | 21 | constructor(private appStore: AppStore) { 22 | } 23 | 24 | public get titleSortIndicator() { 25 | return this.sortBy === ImageSortBy.title ? this.isAscending ? 1 : -1 : 0; 26 | } 27 | 28 | public get sizeSortIndicator() { 29 | return this.sortBy === ImageSortBy.size ? this.isAscending ? 1 : -1 : 0; 30 | } 31 | 32 | public get dateSortIndicator() { 33 | return this.sortBy === ImageSortBy.date ? this.isAscending ? 1 : -1 : 0; 34 | } 35 | 36 | public sortByTitle() { 37 | this.toggleTitleSort.emit(null); 38 | } 39 | 40 | public sortBySize() { 41 | this.toggleSizeSort.emit(null); 42 | } 43 | 44 | public sortByDate() { 45 | this.toggleDateSort.emit(null); 46 | } 47 | 48 | public toggleTagSelector() { 49 | this.showTagSelector = !this.showTagSelector; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-tag-selector.css: -------------------------------------------------------------------------------- 1 | label { 2 | display: inline-block; 3 | padding-right: 2rem; } 4 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-tag-selector.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-tag-selector.scss: -------------------------------------------------------------------------------- 1 | 2 | label { 3 | display: inline-block; 4 | padding-right: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/image-tag-selector.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {AppStore} from '../../services/app-store' 6 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../decorators/app-store-subscriber' 7 | import {excludeImageTags} from '../../actions/image-list-actions' 8 | import {isMatchingTag, getSelectedTagsList, getExcludedTagsFromSelectedTagsList} from '../../utils/tag-utils' 9 | 10 | @Component({ 11 | selector: '[image-tag-selector]', 12 | templateUrl: 'app/components/image-detail-list/image-tag-selector.html', 13 | styleUrls: ['app/components/image-detail-list/image-tag-selector.css'] 14 | }) 15 | @AppStoreSubscriber() 16 | export class ImageTagSelector implements IAppStoreSubscriber { 17 | 18 | public imageTags: any[] = []; 19 | 20 | constructor(private appStore: AppStore) { 21 | } 22 | 23 | public onInitAppStoreSubscription(source: Observable): Subscription { 24 | return source 25 | .map((state: any) => state.imageData) 26 | .subscribe((imageData: any) => { 27 | this.imageTags = getSelectedTagsList(imageData.dataSet, imageData.excludedTags) 28 | }) 29 | } 30 | 31 | public selectAll(): void { 32 | this.imageTags = this.imageTags.map(v => ({ tag: v.tag, isSelected: true })) 33 | this.onSelectionChanged() 34 | } 35 | 36 | public selectNone(): void { 37 | this.imageTags = this.imageTags.map(v => ({ tag: v.tag, isSelected: false })) 38 | this.onSelectionChanged() 39 | } 40 | 41 | public toggleSelectedTag(tag, isSelected) { 42 | this.imageTags = this.imageTags.map(v => isMatchingTag(v.tag, tag) ? {tag, isSelected} : v) 43 | this.onSelectionChanged() 44 | } 45 | 46 | private onSelectionChanged(): void { 47 | let excludedTags = getExcludedTagsFromSelectedTagsList(this.imageTags); 48 | this.appStore.dispatch(excludeImageTags(excludedTags)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/components/image-detail-list/sortable-column-header.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, Input, Output, EventEmitter} from '@angular/core' 3 | 4 | @Component({ 5 | selector: '.sortable-column-header', 6 | template: ` 7 |
8 | 9 | 10 | 11 | 12 |
13 | ` 14 | }) 15 | export class SortableColumnHeader { 16 | @Input() public sortIndicator: number; 17 | @Output() public toggleSort: EventEmitter = new EventEmitter(); 18 | 19 | public onHeaderClicked(event) { 20 | event.preventDefault(); 21 | this.toggleSort.emit(null); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/components/image-edit/image-edit.css: -------------------------------------------------------------------------------- 1 | .image-edit { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: row; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; 11 | align-items: stretch; 12 | align-content: stretch; } 13 | .image-edit > div:nth-of-type(1) { 14 | flex: 0 0 35%; 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: flex-start; 18 | overflow: hidden; } 19 | .image-edit > div:nth-of-type(1) > img { 20 | width: 100%; 21 | heigth: 100%; 22 | padding: 2rem; 23 | object-fit: contain; 24 | /* Not supported in Edge yet */ } 25 | .image-edit > div:nth-of-type(2) { 26 | flex: 1 1 auto; 27 | padding: 1rem; } 28 | -------------------------------------------------------------------------------- /src/app/components/image-edit/image-edit.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 | 18 | {{image.details.dateTaken | date:'medium' }} 19 |
20 |
21 | 22 | 23 | {{image.details.width | number }} x {{image.details.height | number }}, 24 | {{image.details.size | number }} bytes 25 | 26 |
27 |
28 |
29 |
30 | 34 |
35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /src/app/components/image-edit/image-edit.scss: -------------------------------------------------------------------------------- 1 | 2 | .image-edit { 3 | 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: row; 11 | flex-wrap: nowrap; 12 | justify-content: flex-start; 13 | align-items: stretch; 14 | align-content: stretch; 15 | 16 | > div:nth-of-type(1) { 17 | 18 | flex: 0 0 35%; 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: flex-start; 22 | overflow: hidden; 23 | 24 | > img { 25 | width: 100%; 26 | heigth: 100%; 27 | padding: 2rem; 28 | object-fit: contain; /* Not supported in Edge yet */ 29 | } 30 | } 31 | 32 | > div:nth-of-type(2) { 33 | flex: 1 1 auto; 34 | padding: 1rem; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/components/image-edit/image-edit.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {AppStore} from '../../services/app-store' 6 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../decorators/app-store-subscriber' 7 | import {changeImageTitle, updateImageTags} from '../../actions/image-list-actions' 8 | import {getUniqueTagsList} from '../../utils/tag-utils' 9 | 10 | @Component({ 11 | selector: 'image-edit', 12 | templateUrl: 'app/components/image-edit/image-edit.html', 13 | styleUrls: ['app/components/image-edit/image-edit.css'] 14 | }) 15 | @AppStoreSubscriber() 16 | export class ImageEdit implements IAppStoreSubscriber { 17 | 18 | public image: any 19 | public tagsList: string[] 20 | 21 | constructor(private appStore: AppStore) { 22 | } 23 | 24 | public onInitAppStoreSubscription(source: Observable): Subscription { 25 | return source 26 | .filter((state: any) => state.imageData.currentImageId) 27 | .map((state: any) => ({ 28 | image: { 29 | details: state.imageData.dataSet[state.imageData.currentImageId], 30 | url: ['/api', 'images', state.imageData.currentImageId, 'image'].join('/'), 31 | title: state.imageData.dataSet[state.imageData.currentImageId].title 32 | }, 33 | tagsList: getUniqueTagsList(state.imageData.dataSet) 34 | })) 35 | .subscribe((data: any) => { 36 | this.image = data.image 37 | this.tagsList = data.tagsList 38 | }) 39 | } 40 | 41 | public updateTitle(newTitle: string) { 42 | this.appStore.dispatch(changeImageTitle(this.image.details.id, newTitle)) 43 | } 44 | 45 | public onTagsChanged(eventData: any) { 46 | this.appStore.dispatch(updateImageTags(this.image.details.id, eventData.tags)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/components/image-group-list/image-group-list.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | font-size: 1.4rem; 3 | font-weight: normal; 4 | font-style: italic; 5 | border-bottom: 1px solid #eee; 6 | margin-top: 1rem; } 7 | 8 | .thumbnail-panel { 9 | display: inline-block; 10 | margin: 0 1rem 1rem 0; } 11 | 12 | .thumbnail-panel img.thumbnail { 13 | margin-bottom: 0; } 14 | 15 | .thumbnail-panel div { 16 | font-size: 0.9rem; 17 | text-align: left; 18 | overflow-wrap: break-word; 19 | word-wrap: break-word; } 20 | -------------------------------------------------------------------------------- /src/app/components/image-group-list/image-group-list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{group.name}} ({{group.included.length}})

4 |
5 |
6 | 7 | 8 | 9 |
{{img.title}}
10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/components/image-group-list/image-group-list.scss: -------------------------------------------------------------------------------- 1 | 2 | h2 { 3 | font-size: 1.4rem; 4 | font-weight: normal; 5 | font-style: italic; 6 | border-bottom: 1px solid #eee; 7 | margin-top: 1rem; 8 | } 9 | 10 | .thumbnail-panel { 11 | display: inline-block; 12 | margin: 0 1rem 1rem 0; 13 | } 14 | 15 | .thumbnail-panel img.thumbnail { 16 | margin-bottom: 0; 17 | } 18 | 19 | .thumbnail-panel div { 20 | font-size: 0.9rem; 21 | text-align: left; 22 | overflow-wrap: break-word; 23 | word-wrap: break-word; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/image-group-list/image-group-list.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {ActivatedRoute} from '@angular/router' 6 | import {AppStore} from '../../services/app-store' 7 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../decorators/app-store-subscriber' 8 | import {isTagIncludedInList, getUniqueTagsList} from '../../utils/tag-utils' 9 | 10 | @Component({ 11 | selector: 'image-group-list', 12 | templateUrl: 'app/components/image-group-list/image-group-list.html', 13 | styleUrls: ['app/components/image-group-list/image-group-list.css'] 14 | }) 15 | @AppStoreSubscriber() 16 | export class ImageGroupList implements IAppStoreSubscriber { 17 | 18 | public imageGroups: any[]; 19 | 20 | private isEditRoute: boolean = false; 21 | 22 | constructor( 23 | private appStore: AppStore, 24 | private route: ActivatedRoute) { 25 | } 26 | 27 | public ngOnInit() { 28 | this.route.url 29 | .subscribe((urlPaths) => { 30 | this.isEditRoute = urlPaths[0] && urlPaths[0].path && urlPaths[0].path === 'edit'; 31 | }) 32 | } 33 | 34 | public imageRouteFor(img) { 35 | return [ 36 | '/images', 37 | this.isEditRoute ? 'edit' : 'view', 38 | img.id 39 | ] 40 | } 41 | 42 | public onInitAppStoreSubscription(source: Observable): Subscription { 43 | return source 44 | .map((state: any) => state.imageData) 45 | .subscribe((imageData: any) => { 46 | this.imageGroups = _(getUniqueTagsList(imageData.dataSet)) 47 | .filter((tag: string) => !isTagIncludedInList(tag, imageData.excludedTags)) 48 | .orderBy(tag => tag) 49 | .map((tag: string) => ({ 50 | name: tag, 51 | included: _(imageData.displayedItems) 52 | .map((id: string) => imageData.dataSet[id]) 53 | .filter((img: any) => isTagIncludedInList(tag, img.tags)) 54 | .map((img: any) => ({ 55 | id: img.id, 56 | title: img.title, 57 | url: ['api', 'images', img.id, 'thumb'].join('/') 58 | })) 59 | .value() 60 | })) 61 | .value() 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/components/image-view/image-view.css: -------------------------------------------------------------------------------- 1 | .image-view { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | flex-wrap: nowrap; 10 | justify-content: flex-start; 11 | align-items: stretch; 12 | align-content: stretch; } 13 | .image-view > div:nth-of-type(1) { 14 | flex: 1 1 auto; 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: flex-start; 18 | overflow: hidden; } 19 | .image-view > div:nth-of-type(1) > img { 20 | width: 100%; 21 | heigth: 100%; 22 | padding: 2rem; 23 | object-fit: contain; 24 | /* Not supported in Edge yet */ } 25 | .image-view > div:nth-of-type(2) { 26 | flex: 0 0 auto; } 27 | -------------------------------------------------------------------------------- /src/app/components/image-view/image-view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 |
10 | 17 |
18 |

19 | Taken: 20 | {{image.details.dateTaken | date:'medium' }} 21 | Specs: 22 | 23 | {{image.details.width | number }} x {{image.details.height | number }}, 24 | {{image.details.size | number }} bytes 25 | 26 | Tags: 27 | {{image.details.tags}} 28 |

29 |
30 |
31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/app/components/image-view/image-view.scss: -------------------------------------------------------------------------------- 1 | 2 | .image-view { 3 | 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | flex-wrap: nowrap; 12 | justify-content: flex-start; 13 | align-items: stretch; 14 | align-content: stretch; 15 | 16 | > div:nth-of-type(1) { 17 | flex: 1 1 auto; 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: flex-start; 21 | overflow: hidden; 22 | 23 | > img { 24 | width: 100%; 25 | heigth: 100%; 26 | padding: 2rem; 27 | object-fit: contain; /* Not supported in Edge yet */ 28 | } 29 | } 30 | 31 | > div:nth-of-type(2) { 32 | flex: 0 0 auto; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/image-view/image-view.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | import {AppStoreSubscriber, IAppStoreSubscriber} from '../../decorators/app-store-subscriber' 6 | 7 | @Component({ 8 | selector: 'image-view', 9 | templateUrl: 'app/components/image-view/image-view.html', 10 | styleUrls: ['app/components/image-view/image-view.css'] 11 | }) 12 | @AppStoreSubscriber() 13 | export class ImageView implements IAppStoreSubscriber { 14 | 15 | public image: any 16 | 17 | public onInitAppStoreSubscription(source: Observable): Subscription { 18 | return source 19 | .filter((state: any) => state.imageData.currentImageId) 20 | .map((state: any) => ({ 21 | details: state.imageData.dataSet[state.imageData.currentImageId], 22 | url: ['/api', 'images', state.imageData.currentImageId, 'image'].join('/') 23 | })) 24 | .subscribe((image: any) => this.image = image) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/loading-indicator/loading-indicator.css: -------------------------------------------------------------------------------- 1 | svg path, svg rect { 2 | fill: #00a1ff; } 3 | 4 | svg > rect:nth-of-type(1) { 5 | animation: opacity-values 0.8s 0.0s ease infinite; } 6 | 7 | svg > rect:nth-of-type(2) { 8 | animation: opacity-values 0.8s 0.1s ease infinite; } 9 | 10 | svg > rect:nth-of-type(3) { 11 | animation: opacity-values 0.8s 0.2s ease infinite; } 12 | 13 | .loading-message { 14 | font-size: 0.8rem; 15 | font-style: italic; } 16 | 17 | @keyframes opacity-values { 18 | 0% { 19 | opacity: 0.2; 20 | height: 10px; 21 | y: 10; } 22 | 50% { 23 | opacity: 1; 24 | height: 20px; 25 | y: 5; } 26 | 100% { 27 | opacity: 0.2; 28 | height: 10px; 29 | y: 10; } } 30 | -------------------------------------------------------------------------------- /src/app/components/loading-indicator/loading-indicator.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | {{loadingMessage}} 10 | 11 | -------------------------------------------------------------------------------- /src/app/components/loading-indicator/loading-indicator.scss: -------------------------------------------------------------------------------- 1 | 2 | svg { 3 | 4 | path, rect { 5 | fill: #00a1ff; 6 | } 7 | 8 | > rect:nth-of-type(1) { 9 | animation: opacity-values 0.8s 0.0s ease infinite; 10 | } 11 | > rect:nth-of-type(2) { 12 | animation: opacity-values 0.8s 0.1s ease infinite; 13 | } 14 | > rect:nth-of-type(3) { 15 | animation: opacity-values 0.8s 0.2s ease infinite; 16 | } 17 | } 18 | 19 | .loading-message { 20 | font-size: 0.8rem; 21 | font-style: italic; 22 | } 23 | 24 | @keyframes opacity-values { 25 | 0% { 26 | opacity: 0.2; 27 | height: 10px; 28 | y: 10; 29 | } 30 | 50% { 31 | opacity: 1; 32 | height: 20px; 33 | y: 5; 34 | } 35 | 100% { 36 | opacity: 0.2; 37 | height: 10px; 38 | y: 10; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/loading-indicator/loading-indicator.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, Input} from '@angular/core' 3 | 4 | @Component({ 5 | selector: 'loading-indicator', 6 | templateUrl: 'app/components/loading-indicator/loading-indicator.html', 7 | styleUrls: ['app/components/loading-indicator/loading-indicator.css'] 8 | }) 9 | export class LoadingIndicator { 10 | @Input() public loadingMessage: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/components/tag-selector/tag-selector-input.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Directive, 4 | Input, 5 | Output, 6 | Optional, 7 | EventEmitter, 8 | ElementRef, 9 | OnInit, 10 | OnDestroy 11 | } from '@angular/core' 12 | 13 | declare var $: any 14 | 15 | @Directive({ 16 | selector: '[.ui.search]' 17 | }) 18 | export class TagSelectorInput implements OnInit, OnDestroy { 19 | 20 | @Input() public tagsList: string[]; 21 | @Output() public addTag: EventEmitter = new EventEmitter(); 22 | 23 | constructor(private searchBox: ElementRef) { 24 | } 25 | 26 | public ngOnInit() { 27 | let tagMap = this.tagsList.map(tag => ({ title: tag })) 28 | $(this.element) 29 | .search({ 30 | source: tagMap, 31 | minCharacters : 1, 32 | onSelect: (result) => { 33 | this.onTagSelected(result.title) 34 | }, 35 | templates: { 36 | message: (message, type) => { 37 | let html = '' 38 | if (message != null && type != null) { 39 | html = [ 40 | `
`, 41 | type === 'empty' ? 42 | `
Adding new tag ...
` : 43 | `
${message}
`, 44 | `
` 45 | ].join('') 46 | } 47 | return html 48 | } 49 | } 50 | }) 51 | } 52 | 53 | public ngOnDestroy() { 54 | $(this.element).search('destroy'); 55 | } 56 | 57 | public onTagSelected(tag: string) { 58 | this.addTag.emit(tag) 59 | setTimeout(() => { 60 | $(this.element).search('set value', '') 61 | $(this.element).search('hide results') 62 | }) 63 | } 64 | 65 | private get element() { 66 | return this.searchBox.nativeElement 67 | } 68 | } 69 | 70 | @Directive({ 71 | selector: '[add-tag-on-enter]', 72 | host: { 73 | '(keypress)': 'onKeyPress($event)' 74 | } 75 | }) 76 | export class AddTagOnEnter { 77 | 78 | constructor( 79 | private el: ElementRef, 80 | @Optional() private tagSelectorInput: TagSelectorInput) { 81 | } 82 | 83 | public onKeyPress(evt) { 84 | if (evt && evt.code === 'Enter') { 85 | 86 | evt.preventDefault() 87 | evt.stopPropagation() 88 | 89 | let tag = this.el.nativeElement.value 90 | if (tag && this.tagSelectorInput) { 91 | this.tagSelectorInput.onTagSelected(tag) 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/components/tag-selector/tag-selector.css: -------------------------------------------------------------------------------- 1 | span > label { 2 | display: inline-block; 3 | padding-right: 1rem; } 4 | -------------------------------------------------------------------------------- /src/app/components/tag-selector/tag-selector.html: -------------------------------------------------------------------------------- 1 | 11 |
12 | 16 |
17 | -------------------------------------------------------------------------------- /src/app/components/tag-selector/tag-selector.scss: -------------------------------------------------------------------------------- 1 | 2 | span > label { 3 | display: inline-block; 4 | padding-right: 1rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/components/tag-selector/tag-selector.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, Input, Output, EventEmitter} from '@angular/core' 3 | import {isMatchingTag} from '../../utils/tag-utils' 4 | 5 | @Component({ 6 | selector: 'tag-selector', 7 | templateUrl: 'app/components/tag-selector/tag-selector.html', 8 | styleUrls: ['app/components/tag-selector/tag-selector.css'] 9 | }) 10 | export class TagSelector { 11 | 12 | @Input() public tagsList: string[]; 13 | @Input() public selectedTags: string[]; 14 | @Output() public selectedTagsChanged: EventEmitter = new EventEmitter(); 15 | 16 | public removeSelectedTag(tag) { 17 | let tags = (this.selectedTags || []) 18 | .filter(selectedTag => !isMatchingTag(tag, selectedTag)) 19 | this.selectedTagsChanged.emit({ tags }); 20 | } 21 | 22 | public addNewTag(tag) { 23 | let tags = (this.selectedTags || []) 24 | .filter(selectedTag => !isMatchingTag(tag, selectedTag)) 25 | .concat([tag]) 26 | this.selectedTagsChanged.emit({ tags }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/title-bar/title-bar.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/app/components/title-bar/title-bar.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, Input} from '@angular/core' 3 | 4 | @Component({ 5 | selector: 'title-bar', 6 | templateUrl: 'app/components/title-bar/title-bar.html' 7 | }) 8 | export class TitleBar { 9 | @Input() public isLoading: boolean 10 | public loadingMessage: string = 'Loading images ...' 11 | } 12 | -------------------------------------------------------------------------------- /src/app/decorators/app-store-subscriber.ts: -------------------------------------------------------------------------------- 1 | 2 | import {AppStore} from '../services/app-store' 3 | import {Observable} from 'rxjs/Observable' 4 | import {Subscription} from 'rxjs/Subscription' 5 | 6 | const onDestroyName = 'ngOnDestroy' 7 | const onInitName = 'ngOnInit' 8 | const onInitAppStoreSubscriptionName = 'onInitAppStoreSubscription' 9 | const componentSubscriptionsName = Symbol('ComponentSubscriptions') 10 | 11 | export interface IAppStoreSubscriber { 12 | onInitAppStoreSubscription(source: Observable): Subscription 13 | } 14 | 15 | export function AppStoreSubscriber() { 16 | const { defineProperty } = Object 17 | return function(target) { 18 | 19 | let ngOnDestroyOriginal, ngOnInitOriginal, onInitAppStoreSubscription 20 | const targetPrototype = target.prototype; 21 | 22 | if (typeof targetPrototype[onInitAppStoreSubscriptionName] === 'function') { 23 | onInitAppStoreSubscription = targetPrototype[onInitAppStoreSubscriptionName] 24 | } else { 25 | throw new Error(`The required method, ${onInitAppStoreSubscriptionName}, was not defined on the object.`) 26 | } 27 | 28 | if (typeof targetPrototype[onInitName] === 'function') { 29 | ngOnInitOriginal = targetPrototype[onInitName] 30 | } 31 | if (typeof targetPrototype[onDestroyName] === 'function') { 32 | ngOnDestroyOriginal = targetPrototype[onDestroyName] 33 | } 34 | 35 | defineProperty(targetPrototype, onInitName, { 36 | configurable: true, 37 | enumerable: true, 38 | get() { 39 | return () => { 40 | if (ngOnInitOriginal) { 41 | ngOnInitOriginal.bind(this)() 42 | } 43 | let subscription = onInitAppStoreSubscription.bind(this)(AppStore.instance.source) 44 | if (!Array.isArray(subscription)) { 45 | subscription = [subscription] 46 | } 47 | this[componentSubscriptionsName] = [...subscription] 48 | } 49 | } 50 | }); 51 | 52 | defineProperty(targetPrototype, onDestroyName, { 53 | configurable: true, 54 | enumerable: true, 55 | get() { 56 | return () => { 57 | if (ngOnDestroyOriginal) { 58 | ngOnDestroyOriginal.bind(this)() 59 | } 60 | let subscriptionList = this[componentSubscriptionsName] 61 | subscriptionList.forEach((subscription: any) => subscription.unsubscribe()) 62 | delete this[componentSubscriptionsName] 63 | } 64 | } 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/demo-app.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core' 3 | 4 | @Component({ 5 | selector: 'demo-app', 6 | template: `` 7 | }) 8 | export class DemoApp { 9 | } 10 | -------------------------------------------------------------------------------- /src/app/directives/semanti-ui-init.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Directive, ElementRef, OnInit, OnDestroy} from '@angular/core' 3 | 4 | declare var $: any 5 | 6 | @Directive({ 7 | selector: '.ui.dropdown' 8 | }) 9 | export class InitializeDropdown implements OnInit, OnDestroy { 10 | 11 | constructor(private el: ElementRef) { 12 | } 13 | 14 | public ngOnInit() { 15 | $(this.el.nativeElement).dropdown(); 16 | } 17 | 18 | public ngOnDestroy() { 19 | $(this.el.nativeElement).dropdown('destroy'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/reducers/image-list.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash' 3 | import { 4 | LOADING_IMAGE_DATA, 5 | LOAD_IMAGE_DATA, 6 | SORT_IMAGES, 7 | EXCLUDE_IMAGE_TAGS, 8 | CLEAR_CURRENT_IMAGE, 9 | SELECT_CURRENT_IMAGE, 10 | CHANGE_IMAGE_TITLE, 11 | UPDATE_IMAGE_TAGS, 12 | ImageSortBy 13 | } from '../actions/image-list-actions' 14 | import {areAllTagsExcluded} from '../utils/tag-utils'; 15 | 16 | const defaultState = { 17 | sortBy: ImageSortBy.title, 18 | isAscending: true, 19 | isLoading: true, 20 | dataSet: {}, 21 | displayedItems: [], 22 | excludedTags: [], 23 | currentImageId: null 24 | } 25 | 26 | export function imageData(state: any = defaultState, action: any = {}) { 27 | switch (action.type) { 28 | case LOADING_IMAGE_DATA: 29 | return Object.assign({}, defaultState) 30 | case LOAD_IMAGE_DATA: 31 | return loadImageData(state, action) 32 | case SORT_IMAGES: 33 | return sortImageData(state, action) 34 | case EXCLUDE_IMAGE_TAGS: 35 | return excludeImageTags(state, action) 36 | case CLEAR_CURRENT_IMAGE: 37 | return Object.assign({}, state, { currentImageId: null }) 38 | case SELECT_CURRENT_IMAGE: 39 | return selectCurrentImage(state, action) 40 | case CHANGE_IMAGE_TITLE: 41 | return changeImageTitle(state, action) 42 | case UPDATE_IMAGE_TAGS: 43 | return updateImageTags(state, action) 44 | default: 45 | return state 46 | } 47 | } 48 | 49 | function loadImageData(state, action) { 50 | 51 | if (action.error) { 52 | return Object.assign({}, state, { 53 | isLoading: false, 54 | errorMessage: action.payload.message 55 | }) 56 | } 57 | 58 | let dataSet = _.fromPairs(action.payload.map(img => [img.id, img])) 59 | return Object.assign({}, state, { 60 | isLoading: false, 61 | dataSet, 62 | displayedItems: getDisplayedItems({ 63 | dataSet, 64 | sortBy: state.sortBy, 65 | isAscending: state.isAscending, 66 | excludedTags: state.excludedTags 67 | }) 68 | }) 69 | } 70 | 71 | function sortImageData(state, action) { 72 | return Object.assign({}, state, { 73 | sortBy: action.payload.sortBy, 74 | isAscending: action.payload.isAscending, 75 | displayedItems: getDisplayedItems({ 76 | dataSet: state.dataSet, 77 | sortBy: action.payload.sortBy, 78 | isAscending: action.payload.isAscending, 79 | excludedTags: state.excludedTags 80 | }) 81 | }) 82 | } 83 | 84 | function excludeImageTags(state, action) { 85 | return Object.assign({}, state, { 86 | excludedTags: action.payload.excludedTags, 87 | displayedItems: getDisplayedItems({ 88 | dataSet: state.dataSet, 89 | sortBy: state.sortBy, 90 | isAscending: state.isAscending, 91 | excludedTags: action.payload.excludedTags 92 | }) 93 | }) 94 | } 95 | 96 | function getDisplayedItems(options) { 97 | 98 | let sortOperator: any; 99 | switch (options.sortBy) { 100 | case ImageSortBy.size: 101 | sortOperator = (v: any) => v.size 102 | break 103 | case ImageSortBy.date: 104 | sortOperator = (v: any) => v.dateTaken 105 | break 106 | default: 107 | sortOperator = (v: any) => v.title.toLocaleLowerCase() 108 | break 109 | } 110 | 111 | return _(_.values(options.dataSet)) 112 | .filter((img: any) => !areAllTagsExcluded(img.tags, options.excludedTags)) 113 | .orderBy([sortOperator], [options.isAscending ? 'asc' : 'desc']) 114 | .map((img: any) => img.id) 115 | .value() 116 | } 117 | 118 | function selectCurrentImage(state, action) { 119 | let imageId = action.payload.imageId; 120 | return Object.assign({}, state, { 121 | currentImageId: state.dataSet[imageId] ? imageId : null 122 | }) 123 | } 124 | 125 | function changeImageTitle(state, action) { 126 | let imageId = action.payload.imageId; 127 | let title = action.payload.title; 128 | if (imageId && title && state.dataSet[imageId]) { 129 | let dataSet = Object.assign({}, state.dataSet, { 130 | [imageId]: Object.assign({}, state.dataSet[imageId], { title }) 131 | }) 132 | let displayedItems = getDisplayedItems({ 133 | dataSet, 134 | sortBy: state.sortBy, 135 | isAscending: state.isAscending, 136 | excludedTags: state.excludedTags 137 | }) 138 | state = Object.assign({}, state, { dataSet, displayedItems }) 139 | } 140 | return state; 141 | } 142 | 143 | function updateImageTags(state, action) { 144 | let imageId = action.payload.imageId; 145 | let tags = action.payload.tags || []; 146 | if (imageId && state.dataSet[imageId]) { 147 | let dataSet = Object.assign({}, state.dataSet, { 148 | [imageId]: Object.assign({}, state.dataSet[imageId], { tags }) 149 | }) 150 | let displayedItems = getDisplayedItems({ 151 | dataSet, 152 | sortBy: state.sortBy, 153 | isAscending: state.isAscending, 154 | excludedTags: state.excludedTags 155 | }) 156 | state = Object.assign({}, state, { dataSet, displayedItems }) 157 | } 158 | return state; 159 | } 160 | -------------------------------------------------------------------------------- /src/app/services/app-store.ts: -------------------------------------------------------------------------------- 1 | 2 | import {OpaqueToken} from '@angular/core' 3 | import {createStore, applyMiddleware, combineReducers, compose} from 'redux' 4 | import {Observable} from 'rxjs/Observable' 5 | import thunkMiddleware from 'redux-thunk' 6 | 7 | interface DevToolsWindow extends Window { 8 | devToolsExtension: any 9 | } 10 | 11 | declare var window: DevToolsWindow; 12 | 13 | export const APP_STORE_REDUCERS: OpaqueToken = new OpaqueToken('AppStoreReducers') 14 | 15 | export class AppStore { 16 | 17 | public static instance: AppStore 18 | 19 | private appStore 20 | private stateObservable 21 | 22 | constructor(reducer) { 23 | 24 | this.appStore = createStore(reducer, compose( 25 | applyMiddleware(thunkMiddleware), 26 | window.devToolsExtension ? window.devToolsExtension() : f => f 27 | )) 28 | 29 | this.stateObservable = Observable.create(observer => { 30 | let dispose = this.appStore.subscribe(() => observer.next(this.currentState)) 31 | observer.next(this.currentState) 32 | return function() { 33 | dispose() 34 | } 35 | }) 36 | 37 | } 38 | 39 | public dispatch(action) { 40 | this.appStore.dispatch(action) 41 | } 42 | 43 | public get source() { 44 | return this.stateObservable; 45 | } 46 | 47 | public get currentState() { 48 | return this.appStore.getState(); 49 | } 50 | } 51 | 52 | export function provideAppStore() { 53 | return { 54 | provide: AppStore, 55 | useFactory: reducers => { 56 | let combinedReducers = reducers.reduce((combined, reducer) => Object.assign(combined, reducer), {}) 57 | AppStore.instance = new AppStore(combineReducers(combinedReducers)) 58 | return AppStore.instance 59 | }, 60 | deps: [APP_STORE_REDUCERS] 61 | } 62 | } 63 | 64 | export function provideReducer(stateName: string, reducer: (state: any, action: any) => any) { 65 | return { 66 | provide: APP_STORE_REDUCERS, 67 | useValue: { 68 | [stateName]: reducer 69 | }, 70 | multi: true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app/utils/app-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Observable} from 'rxjs/Observable' 3 | 4 | export interface IImageIdRouteParam { 5 | id: string; 6 | } 7 | 8 | export function waitForImageListToLoad(appStoreSource: Observable): Observable { 9 | return appStoreSource 10 | .filter((state: any) => !state.imageData.isLoading) 11 | .take(1) 12 | } 13 | 14 | export function watchForImageIdChanges(routeParams: Observable): Observable { 15 | return routeParams 16 | .map((params: IImageIdRouteParam) => params.id) 17 | } 18 | -------------------------------------------------------------------------------- /src/app/utils/tag-utils.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | 3 | export function areAllTagsExcluded(tagList, excludedTags) { 4 | tagList = tagList || [] 5 | return tagList.length > 0 && _.every(tagList, tag => isTagIncludedInList(tag, excludedTags)) 6 | } 7 | 8 | export function isTagIncludedInList(tag, selectedTags) { 9 | return _.some(selectedTags || [], selectedTag => isMatchingTag(selectedTag, tag)); 10 | } 11 | 12 | export function isMatchingTag(tag1, tag2) { 13 | return tagCompareValue(tag1) === tagCompareValue(tag2); 14 | } 15 | 16 | export function tagCompareValue(tag) { 17 | return (tag || '').toLocaleLowerCase(); 18 | } 19 | 20 | export function getUniqueTagsList(imageDataSet) { 21 | return _(_.values(imageDataSet)) 22 | .map((v: any, k: any) => v.tags || []) 23 | .flatten() 24 | .filter(v => !!v) 25 | .map(tagCompareValue) 26 | .uniq() 27 | .sortBy(v => v) 28 | .value() 29 | } 30 | 31 | export function getSelectedTagsList(imageDataSet, excludedTags) { 32 | return _(getUniqueTagsList(imageDataSet)) 33 | .map((tag: string) => ({ 34 | tag, 35 | isSelected: !_.some(excludedTags, (exTag: string) => isMatchingTag(tag, exTag)) 36 | })) 37 | .value() 38 | } 39 | 40 | export function getExcludedTagsFromSelectedTagsList(tagsList) { 41 | return _(tagsList) 42 | .filter((v: any) => !v.isSelected) 43 | .map((v: any) => v.tag) 44 | .value() 45 | } 46 | -------------------------------------------------------------------------------- /src/css/demo-app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f0fff6; 3 | background: linear-gradient(to left, #f0fff6 0%, white 100%); } 4 | 5 | .initial-load { 6 | margin-top: 10rem; } 7 | .initial-load .ui.segment { 8 | height: 10rem; } 9 | -------------------------------------------------------------------------------- /src/css/demo-app.scss: -------------------------------------------------------------------------------- 1 | 2 | $site-bk-color: #f0fff6; 3 | 4 | body { 5 | background: $site-bk-color; 6 | background: linear-gradient(to left, $site-bk-color 0%, lighten($site-bk-color,40%) 100%); 7 | } 8 | 9 | .initial-load { 10 | margin-top: 10rem; 11 | .ui.segment { 12 | height: 10rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-cookbook/angular2-redux-complex-ui/378389d3c549ae6368112c0f9e221d451783eac6/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Complex UI Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
Loading ...
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/system.config.js: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | 3 | System.config({ 4 | paths: { 5 | 'npm:': 'vendor/' 6 | }, 7 | map: { 8 | 'app': 'app', 9 | '@angular/core': 'npm:angular/core/bundles/core.umd.js', 10 | '@angular/common': 'npm:angular/common/bundles/common.umd.js', 11 | '@angular/compiler': 'npm:angular/compiler/bundles/compiler.umd.js', 12 | '@angular/platform-browser': 'npm:angular/platform-browser/bundles/platform-browser.umd.js', 13 | '@angular/platform-browser-dynamic': 'npm:angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 14 | '@angular/http': 'npm:angular/http/bundles/http.umd.js', 15 | '@angular/router': 'npm:angular/router/bundles/router.umd.js', 16 | '@angular/forms': 'npm:angular/forms/bundles/forms.umd.js', 17 | 'rxjs': 'npm:rxjs', 18 | 'lodash': 'npm:lodash/lodash.js', 19 | 'redux': 'npm:redux/redux.js', 20 | 'redux-thunk': 'npm:redux-thunk/index.js' 21 | }, 22 | packages: { 23 | app: { 24 | main: './app-bootstrap.js', 25 | defaultExtension: 'js' 26 | }, 27 | rxjs: { 28 | defaultExtension: 'js' 29 | } 30 | } 31 | }); 32 | 33 | System 34 | .import('app/app-bootstrap') 35 | .then(null, console.error.bind(console)) 36 | 37 | })(this) 38 | -------------------------------------------------------------------------------- /tests/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "tests/specs", 3 | "spec_files": [ 4 | "**/*.spec.js" 5 | ] 6 | } -------------------------------------------------------------------------------- /tests/specs/actions/image-list-actions.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | LOADING_IMAGE_DATA, 4 | LOAD_IMAGE_DATA, 5 | SORT_IMAGES, 6 | EXCLUDE_IMAGE_TAGS, 7 | CLEAR_CURRENT_IMAGE, 8 | SELECT_CURRENT_IMAGE, 9 | CHANGE_IMAGE_TITLE, 10 | UPDATE_IMAGE_TAGS, 11 | ImageSortBy, 12 | loadingImages, 13 | loadImageData, 14 | loadImageDataError, 15 | sortImages, 16 | excludeImageTags, 17 | clearCurrentImage, 18 | selectCurrentImage, 19 | changeImageTitle, 20 | updateImageTags 21 | } from '../../../src/app/actions/image-list-actions' 22 | 23 | describe('Image actions creators', () => { 24 | 25 | describe('loadingImages', () => { 26 | 27 | it('should create LOADING_IMAGE_DATA action', () => { 28 | let action = loadingImages(); 29 | expect(action).toEqual({ 30 | type: LOADING_IMAGE_DATA 31 | }) 32 | }) 33 | 34 | }) 35 | 36 | describe('loadImageData', () => { 37 | 38 | it('should create LOAD_IMAGE_DATA action', () => { 39 | let action = loadImageData(['a', 'b', 'c']); 40 | expect(action).toEqual({ 41 | type: LOAD_IMAGE_DATA, 42 | payload: ['a', 'b', 'c'] 43 | }) 44 | }) 45 | 46 | }) 47 | 48 | describe('loadImageDataError', () => { 49 | 50 | it('should create LOAD_IMAGE_DATA action with error information', () => { 51 | let action = loadImageDataError('some error msg'); 52 | expect(action).toEqual({ 53 | type: LOAD_IMAGE_DATA, 54 | payload: { 55 | message: 'some error msg' 56 | }, 57 | error: true 58 | }) 59 | }) 60 | 61 | }) 62 | 63 | describe('sortImages', () => { 64 | 65 | it('should create SORT_IMAGES action', () => { 66 | let action = sortImages(ImageSortBy.date, false) 67 | expect(action).toEqual({ 68 | type: SORT_IMAGES, 69 | payload: { 70 | sortBy: ImageSortBy.date, 71 | isAscending: false 72 | } 73 | }) 74 | }) 75 | 76 | }) 77 | 78 | describe('excludeImageTags', () => { 79 | 80 | it('should create EXCLUDE_IMAGE_TAGS action', () => { 81 | let action = excludeImageTags(['a', 'b', 'c']) 82 | expect(action).toEqual({ 83 | type: EXCLUDE_IMAGE_TAGS, 84 | payload: { 85 | excludedTags: ['a', 'b', 'c'] 86 | } 87 | }) 88 | }) 89 | 90 | }) 91 | 92 | describe('clearCurrentImage', () => { 93 | 94 | it('should create CLEAR_CURRENT_IMAGE action', () => { 95 | let action = clearCurrentImage() 96 | expect(action).toEqual({ 97 | type: CLEAR_CURRENT_IMAGE 98 | }) 99 | }) 100 | 101 | }) 102 | 103 | describe('selectCurrentImage', () => { 104 | 105 | it('should create SELECT_CURRENT_IMAGE action', () => { 106 | let action = selectCurrentImage('abcxyz') 107 | expect(action).toEqual({ 108 | type: SELECT_CURRENT_IMAGE, 109 | payload: { 110 | imageId: 'abcxyz' 111 | } 112 | }) 113 | }) 114 | 115 | }) 116 | 117 | describe('changeImageTitle', () => { 118 | 119 | it('should create CHANGE_IMAGE_TITLE action', () => { 120 | let action = changeImageTitle('abcxyz', 'New Title') 121 | expect(action).toEqual({ 122 | type: CHANGE_IMAGE_TITLE, 123 | payload: { 124 | imageId: 'abcxyz', 125 | title: 'New Title' 126 | } 127 | }) 128 | }) 129 | 130 | }) 131 | 132 | describe('updateImageTags', () => { 133 | 134 | it('should create UPDATE_IMAGE_TAGS action', () => { 135 | let action = updateImageTags('abcxyz', ['a', 'b', 'c']) 136 | expect(action).toEqual({ 137 | type: UPDATE_IMAGE_TAGS, 138 | payload: { 139 | imageId: 'abcxyz', 140 | tags: ['a', 'b', 'c'] 141 | } 142 | }) 143 | }) 144 | 145 | }) 146 | 147 | }) 148 | -------------------------------------------------------------------------------- /tests/specs/reducers/image-list.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | LOADING_IMAGE_DATA, 4 | LOAD_IMAGE_DATA, 5 | SORT_IMAGES, 6 | EXCLUDE_IMAGE_TAGS, 7 | CLEAR_CURRENT_IMAGE, 8 | SELECT_CURRENT_IMAGE, 9 | CHANGE_IMAGE_TITLE, 10 | UPDATE_IMAGE_TAGS, 11 | ImageSortBy 12 | } from '../../../src/app/actions/image-list-actions' 13 | import {imageData} from '../../../src/app/reducers/image-list' 14 | 15 | const testImageData = [ 16 | { 17 | 'id': 'a', 18 | 'fileName': 'A.jpg', 19 | 'title': 'A', 20 | 'size': 22222, 21 | 'dateTaken': '2015-01-03T00:00:00', 22 | 'width': 200, 23 | 'height': 300, 24 | 'portrait': true, 25 | 'tags': [ 'aa', 'bb', 'cc' ] 26 | }, 27 | { 28 | 'id': 'b', 29 | 'fileName': 'B.jpg', 30 | 'title': 'B', 31 | 'size': 33333, 32 | 'dateTaken': '2015-01-01T00:00:00', 33 | 'width': 300, 34 | 'height': 200, 35 | 'landscape': true, 36 | 'tags': [ 'cc', 'dd' ] 37 | }, 38 | { 39 | 'id': 'c', 40 | 'fileName': 'C.jpg', 41 | 'title': 'C', 42 | 'size': 11111, 43 | 'dateTaken': '2015-01-02T00:00:00', 44 | 'width': 300, 45 | 'height': 200, 46 | 'landscape': true, 47 | 'tags': [ 'aa', 'dd', 'ee' ] 48 | } 49 | ] 50 | 51 | const initialDefaultState = { 52 | sortBy: ImageSortBy.title, 53 | isAscending: true, 54 | isLoading: true, 55 | dataSet: {}, 56 | displayedItems: [], 57 | excludedTags: [], 58 | currentImageId: null 59 | } 60 | 61 | const initialLoadedState = { 62 | sortBy: ImageSortBy.title, 63 | isAscending: true, 64 | isLoading: false, 65 | dataSet: { 66 | a: testImageData[0], 67 | b: testImageData[1], 68 | c: testImageData[2] 69 | }, 70 | displayedItems: ['a', 'b', 'c'], 71 | excludedTags: [], 72 | currentImageId: null 73 | } 74 | 75 | describe('Image Data reducer', () => { 76 | 77 | it('should return initial state', () => { 78 | let state = imageData() 79 | expect(state).toEqual(initialDefaultState) 80 | }) 81 | 82 | it('should pass through unknown action', () => { 83 | let state = imageData(initialDefaultState, { 84 | type: 'SOME_UNKNOWN_ACTION' 85 | }) 86 | expect(state).toEqual(initialDefaultState) 87 | }) 88 | 89 | describe('LOADING_IMAGE_DATA action', () => { 90 | 91 | it('should reset to default state', () => { 92 | let state = imageData(initialLoadedState, { 93 | type: LOADING_IMAGE_DATA 94 | }) 95 | expect(state).toEqual(initialDefaultState) 96 | }) 97 | 98 | }) 99 | 100 | describe('LOAD_IMAGE_DATA action', () => { 101 | 102 | it('should handle error', () => { 103 | let state = imageData(initialDefaultState, { 104 | type: LOAD_IMAGE_DATA, 105 | payload: { 106 | message: 'err msg' 107 | }, 108 | error: true 109 | }) 110 | expect(state).toEqual(Object.assign({}, initialDefaultState, { 111 | isLoading: false, 112 | errorMessage: 'err msg' 113 | })) 114 | }) 115 | 116 | it('should load and sort data', () => { 117 | let state = imageData(initialDefaultState, { 118 | type: LOAD_IMAGE_DATA, 119 | payload: testImageData 120 | }) 121 | expect(state).toEqual(initialLoadedState) 122 | }) 123 | 124 | }) 125 | 126 | describe('SORT_IMAGES action', () => { 127 | 128 | it('should sort by title ascending', () => { 129 | let state = imageData(initialLoadedState, { 130 | type: SORT_IMAGES, 131 | payload: { 132 | sortBy: ImageSortBy.title, 133 | isAscending: true 134 | } 135 | }) 136 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 137 | sortBy: ImageSortBy.title, 138 | isAscending: true, 139 | displayedItems: ['a', 'b', 'c'] 140 | })); 141 | }) 142 | 143 | it('should sort by title descending', () => { 144 | let state = imageData(initialLoadedState, { 145 | type: SORT_IMAGES, 146 | payload: { 147 | sortBy: ImageSortBy.title, 148 | isAscending: false 149 | } 150 | }) 151 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 152 | sortBy: ImageSortBy.title, 153 | isAscending: false, 154 | displayedItems: ['c', 'b', 'a'] 155 | })); 156 | }) 157 | 158 | it('should sort by date ascending', () => { 159 | let state = imageData(initialLoadedState, { 160 | type: SORT_IMAGES, 161 | payload: { 162 | sortBy: ImageSortBy.date, 163 | isAscending: true 164 | } 165 | }) 166 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 167 | sortBy: ImageSortBy.date, 168 | isAscending: true, 169 | displayedItems: ['b', 'c', 'a'] 170 | })); 171 | }) 172 | 173 | it('should sort by date descending', () => { 174 | let state = imageData(initialLoadedState, { 175 | type: SORT_IMAGES, 176 | payload: { 177 | sortBy: ImageSortBy.date, 178 | isAscending: false 179 | } 180 | }) 181 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 182 | sortBy: ImageSortBy.date, 183 | isAscending: false, 184 | displayedItems: ['a', 'c', 'b'] 185 | })); 186 | }) 187 | 188 | it('should sort by size ascending', () => { 189 | let state = imageData(initialLoadedState, { 190 | type: SORT_IMAGES, 191 | payload: { 192 | sortBy: ImageSortBy.size, 193 | isAscending: true 194 | } 195 | }) 196 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 197 | sortBy: ImageSortBy.size, 198 | isAscending: true, 199 | displayedItems: ['c', 'a', 'b'] 200 | })); 201 | }) 202 | 203 | it('should sort by size descending', () => { 204 | let state = imageData(initialLoadedState, { 205 | type: SORT_IMAGES, 206 | payload: { 207 | sortBy: ImageSortBy.size, 208 | isAscending: false 209 | } 210 | }) 211 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 212 | sortBy: ImageSortBy.size, 213 | isAscending: false, 214 | displayedItems: ['b', 'a', 'c'] 215 | })); 216 | }) 217 | 218 | }) 219 | 220 | describe('EXCLUDE_IMAGE_TAGS action', () => { 221 | 222 | it('should exclude no images', () => { 223 | let state = imageData(initialLoadedState, { 224 | type: EXCLUDE_IMAGE_TAGS, 225 | payload: { 226 | excludedTags: [] 227 | } 228 | }) 229 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 230 | displayedItems: ['a', 'b', 'c'], 231 | excludedTags: [] 232 | })); 233 | }) 234 | 235 | it('should exclude all images', () => { 236 | let state = imageData(initialLoadedState, { 237 | type: EXCLUDE_IMAGE_TAGS, 238 | payload: { 239 | excludedTags: ['aa', 'bb', 'cc', 'dd', 'ee'] 240 | } 241 | }) 242 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 243 | displayedItems: [], 244 | excludedTags: ['aa', 'bb', 'cc', 'dd', 'ee'] 245 | })); 246 | }) 247 | 248 | it('should exclude some images', () => { 249 | let state = imageData(initialLoadedState, { 250 | type: EXCLUDE_IMAGE_TAGS, 251 | payload: { 252 | excludedTags: ['cc', 'dd'] 253 | } 254 | }) 255 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 256 | displayedItems: ['a', 'c'], 257 | excludedTags: ['cc', 'dd'] 258 | })); 259 | }) 260 | 261 | }) 262 | 263 | describe('CLEAR_CURRENT_IMAGE action', () => { 264 | 265 | it('should clear the selected image', () => { 266 | let initialState = Object.assign({}, initialLoadedState, { 267 | currentImageId: 'abc' 268 | }) 269 | let state = imageData(initialState, { 270 | type: CLEAR_CURRENT_IMAGE 271 | }) 272 | expect(state).toEqual(initialLoadedState) 273 | }) 274 | 275 | }) 276 | 277 | describe('SELECT_CURRENT_IMAGE action', () => { 278 | 279 | it('should set the selected image id', () => { 280 | let state = imageData(initialLoadedState, { 281 | type: SELECT_CURRENT_IMAGE, 282 | payload: { 283 | imageId: 'b' 284 | } 285 | }) 286 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 287 | currentImageId: 'b' 288 | })) 289 | }) 290 | 291 | it('should set null for invalid image id', () => { 292 | let state = imageData(initialLoadedState, { 293 | type: SELECT_CURRENT_IMAGE, 294 | payload: { 295 | imageId: 'unknown' 296 | } 297 | }) 298 | expect(state).toEqual(Object.assign({}, initialLoadedState, { 299 | currentImageId: null 300 | })) 301 | }) 302 | 303 | }) 304 | 305 | describe('CHANGE_IMAGE_TITLE action', () => { 306 | 307 | it('should update the image title', () => { 308 | let state = imageData(initialLoadedState, { 309 | type: CHANGE_IMAGE_TITLE, 310 | payload: { 311 | imageId: 'b', 312 | title: 'Changed Title' 313 | } 314 | }) 315 | let expected = Object.assign({}, initialLoadedState, { 316 | dataSet: Object.assign({}, initialLoadedState.dataSet, { 317 | b: Object.assign({}, initialLoadedState.dataSet.b, { 318 | title: 'Changed Title' 319 | }) 320 | }), 321 | displayedItems: ['a', 'c', 'b'] 322 | }) 323 | //console.log(JSON.stringify(state, null, 2)) 324 | //console.log(JSON.stringify(expected, null, 2)) 325 | expect(state).toEqual(expected) 326 | }) 327 | 328 | it('should not update the image title for an unknown id', () => { 329 | let state = imageData(initialLoadedState, { 330 | type: CHANGE_IMAGE_TITLE, 331 | payload: { 332 | imageId: 'unknown', 333 | title: 'Changed Title' 334 | } 335 | }) 336 | let expected = initialLoadedState 337 | expect(state).toEqual(expected) 338 | }) 339 | 340 | it('should not update the image title for an empty title', () => { 341 | let state = imageData(initialLoadedState, { 342 | type: CHANGE_IMAGE_TITLE, 343 | payload: { 344 | imageId: 'unknown', 345 | title: '' 346 | } 347 | }) 348 | let expected = initialLoadedState 349 | expect(state).toEqual(expected) 350 | }) 351 | 352 | }) 353 | 354 | describe('UPDATE_IMAGE_TAGS action', () => { 355 | 356 | it('should update the image tags', () => { 357 | let state = imageData(initialLoadedState, { 358 | type: UPDATE_IMAGE_TAGS, 359 | payload: { 360 | imageId: 'b', 361 | tags: ['x', 'y', 'z'] 362 | } 363 | }) 364 | let expected = Object.assign({}, initialLoadedState, { 365 | dataSet: Object.assign({}, initialLoadedState.dataSet, { 366 | b: Object.assign({}, initialLoadedState.dataSet.b, { 367 | tags: ['x', 'y', 'z'] 368 | }) 369 | }) 370 | }) 371 | expect(state).toEqual(expected) 372 | }) 373 | 374 | it('should not update the image tags for an unknown id', () => { 375 | let state = imageData(initialLoadedState, { 376 | type: UPDATE_IMAGE_TAGS, 377 | payload: { 378 | imageId: 'unknown', 379 | tags: ['x', 'y', 'z'] 380 | } 381 | }) 382 | let expected = initialLoadedState 383 | expect(state).toEqual(expected) 384 | }) 385 | 386 | it('should clear the image tags for an empty tag list', () => { 387 | let state = imageData(initialLoadedState, { 388 | type: UPDATE_IMAGE_TAGS, 389 | payload: { 390 | imageId: 'b', 391 | tags: null 392 | } 393 | }) 394 | let expected = Object.assign({}, initialLoadedState, { 395 | dataSet: Object.assign({}, initialLoadedState.dataSet, { 396 | b: Object.assign({}, initialLoadedState.dataSet.b, { 397 | tags: [] 398 | }) 399 | }) 400 | }) 401 | expect(state).toEqual(expected) 402 | }) 403 | 404 | }) 405 | 406 | }) 407 | -------------------------------------------------------------------------------- /tests/specs/utils/tag-utils.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | areAllTagsExcluded, 4 | isTagIncludedInList, 5 | isMatchingTag, 6 | tagCompareValue, 7 | getSelectedTagsList, 8 | getExcludedTagsFromSelectedTagsList 9 | } from '../../../src/app/utils/tag-utils' 10 | 11 | describe('Tag utils', () => { 12 | 13 | describe('areAllTagsExcluded', () => { 14 | 15 | it('should handled undefined input', () => { 16 | expect(areAllTagsExcluded(undefined, undefined)).toBe(false) 17 | expect(areAllTagsExcluded([], undefined)).toBe(false) 18 | expect(areAllTagsExcluded(undefined, [])).toBe(false) 19 | }) 20 | 21 | it('should return false if some tags are in list of excluded tags', () => { 22 | expect(areAllTagsExcluded(['x', 'b', 'z'], ['a', 'b', 'c'])).toBe(false) 23 | }) 24 | 25 | it('should return true if all tags are in list of excluded tags', () => { 26 | expect(areAllTagsExcluded(['c', 'b', 'a'], ['a', 'b', 'c'])).toBe(true) 27 | }) 28 | 29 | it('should return true if no tags are in list of excluded tags', () => { 30 | expect(areAllTagsExcluded(['x', 'y', 'z'], ['a', 'b', 'c'])).toBe(false) 31 | }) 32 | 33 | }) 34 | 35 | describe('isTagIncludedInList', () => { 36 | 37 | it('should handled undefined input', () => { 38 | expect(isTagIncludedInList(undefined, undefined)).toBe(false) 39 | expect(isTagIncludedInList('abc', undefined)).toBe(false) 40 | }) 41 | 42 | it('should return true if tag is in list', () => { 43 | expect(isTagIncludedInList('b', ['a', 'b', 'c'])).toBe(true) 44 | }) 45 | 46 | it('should return false if tag is not in list', () => { 47 | expect(isTagIncludedInList('x', ['a', 'b', 'c'])).toBe(false) 48 | }) 49 | 50 | }) 51 | 52 | describe('isMatchingTag', () => { 53 | 54 | it('should do case-insensitive compare', () => { 55 | expect(isMatchingTag('ABC', 'ABC')).toBe(true) 56 | expect(isMatchingTag('AbC', 'aBc')).toBe(true) 57 | expect(isMatchingTag('abc', 'abc')).toBe(true) 58 | }) 59 | 60 | }) 61 | 62 | describe('tagCompareValue', () => { 63 | 64 | it('should provide lower-case value', () => { 65 | expect(tagCompareValue('ABC')).toBe('abc') 66 | expect(tagCompareValue('AbC')).toBe('abc') 67 | expect(tagCompareValue('abc')).toBe('abc') 68 | }) 69 | 70 | }) 71 | 72 | describe('getSelectedTagsList', () => { 73 | 74 | it('should extract, sort, and select all unique tags', () => { 75 | let testData = { 76 | a: { tags: ['xxx', 'bbb', 'ccc' ]}, 77 | b: { tags: ['aaa', 'yyy', 'ccc' ]}, 78 | c: { tags: ['aaa', 'bbb', 'zzz' ]} 79 | } 80 | expect(getSelectedTagsList(testData, [])).toEqual([ 81 | { tag: 'aaa', isSelected: true }, 82 | { tag: 'bbb', isSelected: true }, 83 | { tag: 'ccc', isSelected: true }, 84 | { tag: 'xxx', isSelected: true }, 85 | { tag: 'yyy', isSelected: true }, 86 | { tag: 'zzz', isSelected: true } 87 | ]) 88 | }) 89 | 90 | it('should extract, sort unique tags and exclude some', () => { 91 | let testData = { 92 | a: { tags: ['xxx', 'bbb', 'ccc' ]}, 93 | b: { tags: ['aaa', 'yyy', 'ccc' ]}, 94 | c: { tags: ['aaa', 'bbb', 'zzz' ]} 95 | } 96 | expect(getSelectedTagsList(testData, ['bbb', 'zzz'])).toEqual([ 97 | { tag: 'aaa', isSelected: true }, 98 | { tag: 'bbb', isSelected: false }, 99 | { tag: 'ccc', isSelected: true }, 100 | { tag: 'xxx', isSelected: true }, 101 | { tag: 'yyy', isSelected: true }, 102 | { tag: 'zzz', isSelected: false } 103 | ]) 104 | }) 105 | 106 | it('should extract, sort, and select all unique tags, ignoring case', () => { 107 | let testData = { 108 | a: { tags: ['xxx', 'bbb', 'CCC' ]}, 109 | b: { tags: ['AAA', 'yyy', 'ccc' ]}, 110 | c: { tags: ['aaa', 'BBB', 'zzz' ]} 111 | } 112 | expect(getSelectedTagsList(testData, [])).toEqual([ 113 | { tag: 'aaa', isSelected: true }, 114 | { tag: 'bbb', isSelected: true }, 115 | { tag: 'ccc', isSelected: true }, 116 | { tag: 'xxx', isSelected: true }, 117 | { tag: 'yyy', isSelected: true }, 118 | { tag: 'zzz', isSelected: true } 119 | ]) 120 | }) 121 | 122 | }) 123 | 124 | describe('getExcludedTagsFromSelectedTagsList', () => { 125 | 126 | it('should provide list of excluded tags', () => { 127 | let testData = [ 128 | { tag: 'aaa', isSelected: true }, 129 | { tag: 'bbb', isSelected: false }, 130 | { tag: 'ccc', isSelected: true }, 131 | { tag: 'xxx', isSelected: true }, 132 | { tag: 'yyy', isSelected: true }, 133 | { tag: 'zzz', isSelected: false } 134 | ] 135 | expect(getExcludedTagsFromSelectedTagsList(testData)).toEqual([ 136 | 'bbb', 137 | 'zzz' 138 | ]) 139 | }) 140 | 141 | }) 142 | 143 | }) 144 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | } 12 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "comment-format": [ 12 | false, 13 | "check-space", 14 | "check-lowercase" 15 | ], 16 | "curly": true, 17 | "eofline": true, 18 | "forin": true, 19 | "indent": [ 20 | true, 21 | "spaces" 22 | ], 23 | "interface-name": false, 24 | "jsdoc-format": true, 25 | "label-position": true, 26 | "label-undefined": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": true, 32 | "member-ordering": [ 33 | true, 34 | "public-before-private", 35 | "static-before-instance", 36 | "variables-before-functions" 37 | ], 38 | "no-any": false, 39 | "no-arg": true, 40 | "no-bitwise": true, 41 | "no-conditional-assignment": true, 42 | "no-consecutive-blank-lines": false, 43 | "no-console": [ 44 | true, 45 | "debug", 46 | "info", 47 | "time", 48 | "timeEnd", 49 | "trace" 50 | ], 51 | "no-construct": true, 52 | "no-constructor-vars": false, 53 | "no-debugger": true, 54 | "no-duplicate-key": true, 55 | "no-duplicate-variable": true, 56 | "no-empty": false, 57 | "no-eval": true, 58 | "no-inferrable-types": false, 59 | "no-internal-module": true, 60 | "no-null-keyword": false, 61 | "no-require-imports": true, 62 | "no-shadowed-variable": true, 63 | "no-string-literal": true, 64 | "no-switch-case-fall-through": true, 65 | "no-trailing-whitespace": false, 66 | "no-unreachable": true, 67 | "no-unused-expression": true, 68 | "no-unused-variable": true, 69 | "no-use-before-declare": true, 70 | "no-var-keyword": true, 71 | "no-var-requires": true, 72 | "object-literal-sort-keys": false, 73 | "one-line": [ 74 | true, 75 | "check-open-brace", 76 | "check-catch", 77 | "check-else", 78 | "check-finally", 79 | "check-whitespace" 80 | ], 81 | "quotemark": [ 82 | true, 83 | "single", 84 | "avoid-escape" 85 | ], 86 | "radix": true, 87 | "semicolon": false, 88 | "switch-default": true, 89 | "trailing-comma": [ 90 | true, 91 | { 92 | "multiline": "never", 93 | "singleline": "never" 94 | } 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef": [ 101 | false, 102 | "call-signature", 103 | "parameter", 104 | "arrow-parameter", 105 | "property-declaration", 106 | "variable-declaration", 107 | "member-variable-declaration" 108 | ], 109 | "typedef-whitespace": [ 110 | true, 111 | { 112 | "call-signature": "nospace", 113 | "index-signature": "nospace", 114 | "parameter": "nospace", 115 | "property-declaration": "nospace", 116 | "variable-declaration": "nospace" 117 | } 118 | ], 119 | "use-strict": [ 120 | false, 121 | "check-module", 122 | "check-function" 123 | ], 124 | "variable-name": [ 125 | true, 126 | "check-format", 127 | "allow-leading-underscore", 128 | "ban-keywords" 129 | ], 130 | "whitespace": [ 131 | true, 132 | "check-branch", 133 | "check-decl", 134 | "check-operator", 135 | "check-separator", 136 | "check-type" 137 | ] 138 | } 139 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-redux-complex-ui", 3 | "dependencies": { 4 | "lodash": "registry:npm/lodash#4.0.0+20160723033700", 5 | "redux-thunk": "registry:npm/redux-thunk#2.0.0+20160525185520" 6 | }, 7 | "globalDependencies": { 8 | "core-js": "registry:dt/core-js#0.0.0+20160725163759", 9 | "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", 10 | "node": "registry:dt/node#6.0.0+20160909174046" 11 | } 12 | } 13 | --------------------------------------------------------------------------------