Start working at using grunt/npm/bower for everything.
Showing
6 changed files
with
651 additions
and
0 deletions
.gitignore
0 → 100644
Gruntfile.js
0 → 100644
1 | // Generated on 2014-02-06 using generator-webapp 0.4.7 | ||
2 | /* global module */ | ||
3 | |||
4 | // # Globbing | ||
5 | // for performance reasons we're only matching one level down: | ||
6 | // 'test/spec/{,*/}*.js' | ||
7 | // use this if you want to recursively match all subfolders: | ||
8 | // 'test/spec/**/*.js' | ||
9 | |||
10 | module.exports = function (grunt) { | ||
11 | /* global require */ | ||
12 | 'use strict'; | ||
13 | |||
14 | var jasmineRequirejsTemplateOptions = function(withInstanbul) { | ||
15 | /* global requirejs */ | ||
16 | var callback; | ||
17 | if (withInstanbul) { | ||
18 | callback = function() { | ||
19 | var oldLoad = requirejs.load; | ||
20 | requirejs.load = function (context, moduleName, url) { | ||
21 | //console.log('context=' + JSON.stringify(arguments), 'moduleName=' + moduleName, 'url=' + url); | ||
22 | var parts = url.split('/'); | ||
23 | for (var i = 0; i < parts.length; ) { | ||
24 | var part = parts[i]; | ||
25 | if (part === '.') { | ||
26 | parts.splice(i, 1); | ||
27 | } else if (part === '') { | ||
28 | parts.splice(i, 1); | ||
29 | } else if (part === '..') { | ||
30 | if (i > 0) { | ||
31 | i--; | ||
32 | parts.splice(i, 2); | ||
33 | } else { | ||
34 | parts.splice(i, 1); | ||
35 | } | ||
36 | } else { | ||
37 | i++; | ||
38 | } | ||
39 | } | ||
40 | url = parts.join('/'); | ||
41 | if (url.indexOf('src/scripts/') === 0) { | ||
42 | url = './.grunt/grunt-contrib-jasmine/' + url; | ||
43 | } | ||
44 | if (url.indexOf('test/specs/') === 0) { | ||
45 | url = './.grunt/grunt-contrib-jasmine/' + url; | ||
46 | } | ||
47 | //console.log('url=' + url); | ||
48 | return oldLoad.apply(this, [context, moduleName, url]); | ||
49 | }; | ||
50 | }; | ||
51 | } | ||
52 | return { | ||
53 | requireConfigFile: '<%= yeoman.src %>/scripts/config.js', | ||
54 | requireConfig: { | ||
55 | baseUrl: '<%= yeoman.src %>/scripts', | ||
56 | callback: callback | ||
57 | } | ||
58 | }; | ||
59 | }; | ||
60 | |||
61 | var jasmineInstanbulTemplateOptions = function(nestedTemplate, nestedOptions) { | ||
62 | return { | ||
63 | coverage: 'bin/coverage/coverage.json', | ||
64 | report: 'bin/coverage', | ||
65 | replace: false, | ||
66 | template: require(nestedTemplate), | ||
67 | templateOptions: nestedOptions | ||
68 | }; | ||
69 | }; | ||
70 | |||
71 | // Load grunt tasks automatically | ||
72 | require('load-grunt-tasks')(grunt); | ||
73 | |||
74 | // Time how long tasks take. Can help when optimizing build times | ||
75 | require('time-grunt')(grunt); | ||
76 | |||
77 | // Define the configuration for all the tasks | ||
78 | grunt.initConfig({ | ||
79 | bower: { | ||
80 | target: { | ||
81 | options: { | ||
82 | exclude: [ | ||
83 | 'requirejs', | ||
84 | ], | ||
85 | transitive: true, | ||
86 | }, | ||
87 | rjsConfig: '<%= yeoman.src %>/scripts/config.js' | ||
88 | } | ||
89 | }, | ||
90 | |||
91 | // Project settings | ||
92 | yeoman: { | ||
93 | // Configurable paths | ||
94 | app: 'app', | ||
95 | dist: 'dist', | ||
96 | src: 'src', | ||
97 | }, | ||
98 | |||
99 | // Watches files for changes and runs tasks based on the changed files | ||
100 | watch: { | ||
101 | js: { | ||
102 | files: ['<%= yeoman.src %>/scripts/{,*/}*.js'], | ||
103 | tasks: ['jshint'], | ||
104 | }, | ||
105 | jstest: { | ||
106 | files: ['test/spec/{,*/}*.js'], | ||
107 | tasks: ['test:watch'] | ||
108 | }, | ||
109 | gruntfile: { | ||
110 | files: ['Gruntfile.js'] | ||
111 | }, | ||
112 | styles: { | ||
113 | files: ['<%= yeoman.src %>/styles/{,*/}*.css'], | ||
114 | tasks: ['newer:copy:styles', 'autoprefixer'] | ||
115 | } | ||
116 | }, | ||
117 | |||
118 | // The actual grunt server settings | ||
119 | connect: { | ||
120 | options: { | ||
121 | port: 9000, | ||
122 | // Change this to '0.0.0.0' to access the server from outside | ||
123 | hostname: 'localhost' | ||
124 | }, | ||
125 | app: { | ||
126 | options: { | ||
127 | open: false, | ||
128 | base: [ | ||
129 | '.tmp', | ||
130 | '<%= yeoman.src %>' | ||
131 | ] | ||
132 | } | ||
133 | }, | ||
134 | test: { | ||
135 | options: { | ||
136 | port: 9001, | ||
137 | base: [ | ||
138 | '.tmp', | ||
139 | 'test', | ||
140 | '<%= yeoman.src %>' | ||
141 | ] | ||
142 | } | ||
143 | }, | ||
144 | dist: { | ||
145 | options: { | ||
146 | open: false, | ||
147 | base: '<%= yeoman.dist %>', | ||
148 | } | ||
149 | } | ||
150 | }, | ||
151 | |||
152 | // Empties folders to start fresh | ||
153 | clean: { | ||
154 | dist: { | ||
155 | files: [{ | ||
156 | dot: true, | ||
157 | src: [ | ||
158 | '.tmp', | ||
159 | '<%= yeoman.dist %>/*', | ||
160 | '!<%= yeoman.dist %>/.git*' | ||
161 | ] | ||
162 | }] | ||
163 | }, | ||
164 | server: '.tmp' | ||
165 | }, | ||
166 | |||
167 | // Make sure code styles are up to par and there are no obvious mistakes | ||
168 | jshint: { | ||
169 | options: { | ||
170 | browser: true, | ||
171 | esnext: true, | ||
172 | bitwise: true, | ||
173 | camelcase: true, | ||
174 | curly: true, | ||
175 | eqeqeq: true, | ||
176 | immed: true, | ||
177 | indent: 4, | ||
178 | latedef: true, | ||
179 | newcap: true, | ||
180 | noarg: true, | ||
181 | quotmark: 'single', | ||
182 | undef: true, | ||
183 | unused: true, | ||
184 | strict: true, | ||
185 | trailing: true, | ||
186 | smarttabs: true, | ||
187 | jquery: true, | ||
188 | reporter: require('jshint-stylish') | ||
189 | }, | ||
190 | all: [ | ||
191 | 'Gruntfile.js', | ||
192 | ], | ||
193 | scripts: { | ||
194 | options: { | ||
195 | globals: { | ||
196 | define: false, | ||
197 | } | ||
198 | }, | ||
199 | files: { | ||
200 | src: [ | ||
201 | '<%= yeoman.src %>/scripts/**/*.js', | ||
202 | '!<%= yeoman.src %>/scripts/vendor/*', | ||
203 | ] | ||
204 | } | ||
205 | }, | ||
206 | specs: { | ||
207 | options: { | ||
208 | globals: { | ||
209 | beforeEach: false, | ||
210 | define: false, | ||
211 | describe: false, | ||
212 | expect: false, | ||
213 | it: false, | ||
214 | jasmine: false, | ||
215 | } | ||
216 | }, | ||
217 | files: { | ||
218 | src: [ | ||
219 | 'test/specs/**/*.spec.js' | ||
220 | ] | ||
221 | } | ||
222 | } | ||
223 | }, | ||
224 | |||
225 | jasmine: { | ||
226 | all: { | ||
227 | src: [ | ||
228 | '<%= yeoman.src %>/scripts/{,**/}*.js', | ||
229 | 'test/specs/**/*.spec.js', | ||
230 | ], | ||
231 | options: { | ||
232 | specs: 'test/specs/**/*.spec.js', | ||
233 | template: require('grunt-template-jasmine-istanbul'), | ||
234 | templateOptions: jasmineInstanbulTemplateOptions('grunt-template-jasmine-requirejs', jasmineRequirejsTemplateOptions(true)) | ||
235 | } | ||
236 | } | ||
237 | }, | ||
238 | |||
239 | // Mocha testing framework configuration options | ||
240 | mocha: { | ||
241 | all: { | ||
242 | options: { | ||
243 | run: true, | ||
244 | urls: ['http://<%= connect.test.options.hostname %>:<%= connect.test.options.port %>/index.html'] | ||
245 | } | ||
246 | } | ||
247 | }, | ||
248 | |||
249 | // Add vendor prefixed styles | ||
250 | autoprefixer: { | ||
251 | options: { | ||
252 | browsers: ['last 1 version'] | ||
253 | }, | ||
254 | dist: { | ||
255 | files: [{ | ||
256 | expand: true, | ||
257 | cwd: '.tmp/styles/', | ||
258 | src: '{,*/}*.css', | ||
259 | dest: '.tmp/styles/' | ||
260 | }] | ||
261 | } | ||
262 | }, | ||
263 | |||
264 | // Automatically inject Bower components into the HTML file | ||
265 | 'bower-install': { | ||
266 | app: { | ||
267 | html: '<%= yeoman.src %>/index.html', | ||
268 | ignorePath: '<%= yeoman.src %>/' | ||
269 | } | ||
270 | }, | ||
271 | |||
272 | // Renames files for browser caching purposes | ||
273 | rev: { | ||
274 | dist: { | ||
275 | files: { | ||
276 | src: [ | ||
277 | '<%= yeoman.dist %>/scripts/*/**/*.js', | ||
278 | '<%= yeoman.dist %>/scripts/!(config)*.js', | ||
279 | '<%= yeoman.dist %>/styles/{,*/}*.css', | ||
280 | '<%= yeoman.dist %>/images/{,*/}*.{gif,jpeg,jpg,png,webp}', | ||
281 | '<%= yeoman.dist %>/styles/fonts/{,*/}*.*' | ||
282 | ] | ||
283 | } | ||
284 | }, | ||
285 | requireconfig: { | ||
286 | files: { | ||
287 | src: [ | ||
288 | '<%= yeoman.dist %>/scripts/config.js' | ||
289 | ] | ||
290 | } | ||
291 | } | ||
292 | }, | ||
293 | |||
294 | requirejs: { | ||
295 | dist: { | ||
296 | options: { | ||
297 | done: function(done) { | ||
298 | var requireModules = grunt.config('requireModules') || {}; | ||
299 | var lines = [ | ||
300 | 'require.bundles = (function(bundles) {', | ||
301 | ]; | ||
302 | for (var key in requireModules) { | ||
303 | var keyS = JSON.stringify(key); | ||
304 | var value = requireModules[key]; | ||
305 | var included = []; | ||
306 | for (var i = 0; i < value.included.length; i++) { | ||
307 | var file = value.included[i]; | ||
308 | if (file.match(/\.js$/)) { | ||
309 | included.push(file.substring(0, file.length - 3)); | ||
310 | } | ||
311 | } | ||
312 | lines.push('bundles[' + keyS + '] = ' + JSON.stringify(included) + ';'); | ||
313 | } | ||
314 | lines.push('return bundles;'); | ||
315 | lines.push('})(require.bundles || {});'); | ||
316 | grunt.file.write('.tmp/scripts/bundles.js', lines.join('\n')); | ||
317 | done(); | ||
318 | }, | ||
319 | baseUrl: '<%= yeoman.src %>/scripts', | ||
320 | mainConfigFile: '<%= yeoman.src %>/scripts/config.js', | ||
321 | wrapShim: true, | ||
322 | dir: '<%= yeoman.dist %>/scripts', | ||
323 | optimize: 'none', | ||
324 | removeCombined: true, | ||
325 | onModuleBundleComplete: function(data) { | ||
326 | if (data.name.slice(0, 'bundles/'.length) === 'bundles/') { | ||
327 | var requireModules = grunt.config('requireModules') || {}; | ||
328 | requireModules[data.name] = data; | ||
329 | grunt.config('requireModules', requireModules); | ||
330 | } | ||
331 | }, | ||
332 | } | ||
333 | }, | ||
334 | }, | ||
335 | |||
336 | // Reads HTML for usemin blocks to enable smart builds that automatically | ||
337 | // concat, minify and revision files. Creates configurations in memory so | ||
338 | // additional tasks can operate on them | ||
339 | useminPrepare: { | ||
340 | options: { | ||
341 | dest: '<%= yeoman.dist %>' | ||
342 | }, | ||
343 | html: '<%= yeoman.src %>/index.html' | ||
344 | }, | ||
345 | |||
346 | // Performs rewrites based on rev and the useminPrepare configuration | ||
347 | usemin: { | ||
348 | options: { | ||
349 | assetsDirs: ['<%= yeoman.dist %>'] | ||
350 | }, | ||
351 | html: ['<%= yeoman.dist %>/{,*/}*.html'], | ||
352 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'] | ||
353 | }, | ||
354 | |||
355 | // The following *-min tasks produce minified files in the dist folder | ||
356 | imagemin: { | ||
357 | dist: { | ||
358 | files: [{ | ||
359 | expand: true, | ||
360 | cwd: '<%= yeoman.src %>/images', | ||
361 | src: '{,*/}*.{gif,jpeg,jpg,png}', | ||
362 | dest: '<%= yeoman.dist %>/images' | ||
363 | }] | ||
364 | } | ||
365 | }, | ||
366 | svgmin: { | ||
367 | dist: { | ||
368 | files: [{ | ||
369 | expand: true, | ||
370 | cwd: '<%= yeoman.src %>/images', | ||
371 | src: '{,*/}*.svg', | ||
372 | dest: '<%= yeoman.dist %>/images' | ||
373 | }] | ||
374 | } | ||
375 | }, | ||
376 | htmlmin: { | ||
377 | dist: { | ||
378 | options: { | ||
379 | collapseBooleanAttributes: true, | ||
380 | collapseWhitespace: true, | ||
381 | removeAttributeQuotes: true, | ||
382 | removeCommentsFromCDATA: true, | ||
383 | removeEmptyAttributes: true, | ||
384 | removeOptionalTags: true, | ||
385 | removeRedundantAttributes: true, | ||
386 | useShortDoctype: true | ||
387 | }, | ||
388 | files: [{ | ||
389 | expand: true, | ||
390 | cwd: '<%= yeoman.dist %>', | ||
391 | src: '{,*/}*.html', | ||
392 | dest: '<%= yeoman.dist %>' | ||
393 | }] | ||
394 | } | ||
395 | }, | ||
396 | |||
397 | // By default, your `index.html`'s <!-- Usemin block --> will take care of | ||
398 | // minification. These next options are pre-configured if you do not wish | ||
399 | // to use the Usemin blocks. | ||
400 | // cssmin: { | ||
401 | // dist: { | ||
402 | // files: { | ||
403 | // '<%= yeoman.dist %>/styles/main.css': [ | ||
404 | // '.tmp/styles/{,*/}*.css', | ||
405 | // '<%= yeoman.src %>/styles/{,*/}*.css' | ||
406 | // ] | ||
407 | // } | ||
408 | // } | ||
409 | // }, | ||
410 | // uglify: { | ||
411 | // dist: { | ||
412 | // files: { | ||
413 | // '<%= yeoman.dist %>/scripts/scripts.js': [ | ||
414 | // '<%= yeoman.dist %>/scripts/scripts.js' | ||
415 | // ] | ||
416 | // } | ||
417 | // } | ||
418 | // }, | ||
419 | // concat: { | ||
420 | // dist: {} | ||
421 | // }, | ||
422 | |||
423 | concat: { | ||
424 | requireconfig: { | ||
425 | } | ||
426 | }, | ||
427 | |||
428 | uglify: { | ||
429 | dist: { | ||
430 | }, | ||
431 | requireconfig: { | ||
432 | files: { | ||
433 | '<%= yeoman.dist %>/scripts/config.js': [ | ||
434 | '<%= yeoman.dist %>/scripts/config.js', | ||
435 | '.tmp/scripts/config.js', | ||
436 | ], | ||
437 | } | ||
438 | } | ||
439 | }, | ||
440 | |||
441 | // Copies remaining files to places other tasks can use | ||
442 | copy: { | ||
443 | dist: { | ||
444 | files: [{ | ||
445 | expand: true, | ||
446 | dot: true, | ||
447 | cwd: '<%= yeoman.src %>', | ||
448 | dest: '<%= yeoman.dist %>', | ||
449 | src: [ | ||
450 | '*.{ico,png,txt}', | ||
451 | '.htaccess', | ||
452 | 'images/{,*/}*.webp', | ||
453 | '{,*/}*.html', | ||
454 | 'styles/fonts/{,*/}*.*' | ||
455 | ] | ||
456 | }] | ||
457 | }, | ||
458 | styles: { | ||
459 | expand: true, | ||
460 | dot: true, | ||
461 | cwd: '<%= yeoman.src %>/styles', | ||
462 | dest: '.tmp/styles/', | ||
463 | src: '{,*/}*.css' | ||
464 | } | ||
465 | }, | ||
466 | |||
467 | |||
468 | // Run some tasks in parallel to speed up build process | ||
469 | concurrent: { | ||
470 | server: [ | ||
471 | 'copy:styles' | ||
472 | ], | ||
473 | test: [ | ||
474 | 'copy:styles' | ||
475 | ], | ||
476 | dist: [ | ||
477 | 'copy:styles', | ||
478 | 'imagemin', | ||
479 | 'svgmin' | ||
480 | ] | ||
481 | } | ||
482 | }); | ||
483 | |||
484 | grunt.loadNpmTasks('grunt-bower-requirejs'); | ||
485 | |||
486 | grunt.registerTask('revconfig', function () { | ||
487 | var prefix = grunt.template.process('<%= yeoman.dist %>/scripts/'); | ||
488 | var pattern = prefix + '**/*.{js,html}'; | ||
489 | var files = grunt.file.expand(pattern); | ||
490 | var lines = []; | ||
491 | grunt.util._.each(files, function(file) { | ||
492 | file = file.substring(prefix.length); | ||
493 | var res = file.match(/^(.*\/)?([0-9a-f]+)\.([^\/]+)\.([^\.]+)$/); | ||
494 | if (!res) { | ||
495 | return; | ||
496 | } | ||
497 | //grunt.log.oklns(JSON.stringify(res)); | ||
498 | var dir = res[1] || ''; | ||
499 | //var hash = res[2]; | ||
500 | var base = res[3]; | ||
501 | var ext = res[4]; | ||
502 | var id; | ||
503 | if (ext === 'js') { | ||
504 | id = dir + base; | ||
505 | file = file.substring(0, file.length - ext.length - 1); | ||
506 | } else if (ext === 'html') { | ||
507 | id = 'text!' + dir + base + '.' + ext; | ||
508 | } | ||
509 | grunt.log.oklns('map: ' + id + ' -> ' + file); | ||
510 | lines.push('require.paths[' + JSON.stringify(id) + ']=' + JSON.stringify(file) + ';\n'); | ||
511 | }); | ||
512 | grunt.file.write('.tmp/scripts/config.js', lines.join('')); | ||
513 | }); | ||
514 | |||
515 | grunt.registerTask('serve', function (target) { | ||
516 | if (target === 'dist') { | ||
517 | return grunt.task.run(['build', 'connect:dist:keepalive']); | ||
518 | } | ||
519 | |||
520 | grunt.task.run([ | ||
521 | 'clean:server', | ||
522 | 'concurrent:server', | ||
523 | 'autoprefixer', | ||
524 | 'connect:app', | ||
525 | 'watch' | ||
526 | ]); | ||
527 | }); | ||
528 | |||
529 | grunt.registerTask('server', function () { | ||
530 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); | ||
531 | grunt.task.run(['serve']); | ||
532 | }); | ||
533 | |||
534 | grunt.registerTask('test', function(target) { | ||
535 | if (target !== 'watch') { | ||
536 | grunt.task.run([ | ||
537 | 'clean:server', | ||
538 | 'concurrent:test', | ||
539 | 'autoprefixer', | ||
540 | ]); | ||
541 | } | ||
542 | |||
543 | grunt.task.run([ | ||
544 | 'connect:test', | ||
545 | 'mocha' | ||
546 | ]); | ||
547 | }); | ||
548 | |||
549 | grunt.registerTask('build', [ | ||
550 | 'clean:dist', | ||
551 | 'useminPrepare', | ||
552 | 'requirejs', | ||
553 | 'concurrent:dist', | ||
554 | 'autoprefixer', | ||
555 | 'concat', | ||
556 | // 'cssmin', | ||
557 | 'uglify:dist', | ||
558 | 'copy:dist', | ||
559 | // 'rev:dist', | ||
560 | 'revconfig', | ||
561 | 'uglify:requireconfig', | ||
562 | // 'rev:requireconfig', | ||
563 | 'usemin', | ||
564 | 'htmlmin' | ||
565 | ]); | ||
566 | |||
567 | grunt.registerTask('dist', [ | ||
568 | 'bower', | ||
569 | 'newer:jshint', | ||
570 | // 'test', | ||
571 | 'build' | ||
572 | ]); | ||
573 | grunt.registerTask('default', []); | ||
574 | }; |
bower.json
0 → 100644
1 | { | ||
2 | "name": "rivets-error-binder", | ||
3 | "version": "0.0.0", | ||
4 | "authors": [ | ||
5 | "Adam Heath <doogie@brainfood.com>" | ||
6 | ], | ||
7 | "main": [ | ||
8 | "src/scripts/rivets-error-binder.js" | ||
9 | ], | ||
10 | "private": true, | ||
11 | "ignore": [ | ||
12 | "**/.*", | ||
13 | "node_modules", | ||
14 | "src/lib", | ||
15 | "test" | ||
16 | ], | ||
17 | "dependencies": { | ||
18 | "rivets": "~0.6.6", | ||
19 | "requirejs": "~2.1.10" | ||
20 | } | ||
21 | } |
package.json
0 → 100644
1 | { | ||
2 | "name": "rivets-error-binder", | ||
3 | "version": "0.0.0", | ||
4 | "main": [ | ||
5 | "src/scripts/rivets-error-binder.js" | ||
6 | ], | ||
7 | "dependencies": { | ||
8 | "rivets": "~0.6.6", | ||
9 | "requirejs": "~2.1.10" | ||
10 | }, | ||
11 | "devDependencies": { | ||
12 | "bower-requirejs": "~0.9.2", | ||
13 | "grunt": "~0.4.1", | ||
14 | "grunt-contrib-copy": "~0.4.1", | ||
15 | "grunt-contrib-concat": "~0.3.0", | ||
16 | "grunt-contrib-uglify": "~0.2.0", | ||
17 | "grunt-contrib-jshint": "~0.7.0", | ||
18 | "grunt-contrib-cssmin": "~0.7.0", | ||
19 | "grunt-contrib-connect": "~0.5.0", | ||
20 | "grunt-contrib-clean": "~0.5.0", | ||
21 | "grunt-contrib-htmlmin": "~0.1.3", | ||
22 | "grunt-bower-install": "~0.7.0", | ||
23 | "grunt-contrib-imagemin": "~0.2.0", | ||
24 | "grunt-contrib-watch": "~0.5.2", | ||
25 | "grunt-rev": "~0.1.0", | ||
26 | "grunt-autoprefixer": "~0.5.0", | ||
27 | "grunt-usemin": "~0.1.10", | ||
28 | "grunt-mocha": "~0.4.0", | ||
29 | "grunt-newer": "~0.6.0", | ||
30 | "grunt-svgmin": "~0.2.0", | ||
31 | "grunt-concurrent": "~0.4.0", | ||
32 | "load-grunt-tasks": "~0.2.0", | ||
33 | "time-grunt": "~0.2.0", | ||
34 | "jshint-stylish": "~0.1.3", | ||
35 | "grunt-contrib-requirejs": "~0.4.0", | ||
36 | "grunt-bower-requirejs": "~0.8.4", | ||
37 | "grunt-template-jasmine-istanbul": "~0.2.6", | ||
38 | "grunt-template-jasmine-requirejs": "~0.1.10", | ||
39 | "grunt-contrib-jasmine": "~0.5.3" | ||
40 | }, | ||
41 | "engines": { | ||
42 | "node": ">=0.8.0" | ||
43 | } | ||
44 | } | ||
45 |
-
Please register or sign in to post a comment