1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 09:23:13 +01:00

Merge remote-tracking branch 'origin/master' into AD-756-branding-for-sluiceascribeio-brow

Conflicts:
	js/components/header.js
	js/utils/head_setter.js
This commit is contained in:
Tim Daubenschütz 2015-10-22 12:00:59 +02:00
commit 68b608763a
198 changed files with 4211 additions and 2074 deletions

View File

@ -22,7 +22,7 @@
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-mount-set-state": [1, "allow-in-func"],
"react/no-did-update-set-state": 1,
"react/no-multi-comp": 0,
"react/no-unknown-property": 1,
@ -58,4 +58,4 @@
"superInFunctions": 1,
"templateStrings": 1
}
}
}

BIN
fonts/ascribe-logo.eot Normal file

Binary file not shown.

19
fonts/ascribe-logo.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="ascribe-logo" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe800;" glyph-name="add" d="M1024 482.684h-477.316v477.316h-69.368v-477.316h-477.316v-69.368h477.316v-477.316h69.368v477.316h477.316z" />
<glyph unicode="&#xe801;" glyph-name="sort" d="M429.733 809.882l-139.567 150.118-139.6-150.118 25-23.283 97.5 104.887v-955.486h34.133v955.553l97.533-104.954zM848.433 109.401l-97.5-104.887v955.486h-34.133v-955.553l-97.533 104.954-25-23.283 139.567-150.118 139.6 150.118z" />
<glyph unicode="&#xe802;" glyph-name="search" d="M1021.1-36.711l-305.914 305.583c67.615 73.406 108.95 171.391 108.95 279.060 0 227.579-184.49 412.068-412.068 412.068s-412.068-184.489-412.068-412.068c0-227.578 184.489-412.068 412.068-412.068 107.625 0 205.576 41.3 278.972 108.866l305.927-305.597 24.133 24.156zM412.068 169.997c-208.394 0-377.935 169.541-377.935 377.935s169.541 377.935 377.935 377.935 377.935-169.54 377.935-377.935c0-208.394-169.541-377.935-377.935-377.935z" />
<glyph unicode="&#xe803;" glyph-name="filter" d="M0 960l384.89-534.756 8.722-489.244 184.119 174.521 0.324 314.724 445.947 534.756h-1024zM551.839 447.106c-5.109-6.126-7.91-13.849-7.919-21.826l-0.308-300.068-117.253-111.141-7.341 411.782c-0.124 6.948-2.365 13.692-6.424 19.331l-345.97 480.683h884.467l-399.251-478.761z" />
<glyph unicode="&#xe804;" glyph-name="add-white" d="M510.103 923.822c263.415 0 477.719-214.304 477.719-477.719s-214.303-477.718-477.719-477.718c-263.415 0-477.718 214.304-477.718 477.718s214.303 477.719 477.718 477.719zM510.103 957.955c-282.688 0-511.851-229.164-511.851-511.852s229.163-511.851 511.851-511.851 511.852 229.164 511.852 511.851-229.164 511.852-511.852 511.852v0zM796.444 459.378h-273.067v273.067h-34.133v-273.067h-261.689v-34.133h261.689v-261.689h34.133v261.689h273.067z" />
<glyph unicode="&#xe805;" d="M512.148 959.852c-282.688 0-511.851-229.164-511.851-511.852s229.164-511.851 511.851-511.851 511.852 229.163 511.852 511.851-229.164 511.852-511.852 511.852z" />
<glyph unicode="&#xe806;" d="M796.444 459.378h-273.067v273.067h-34.133v-273.067h-261.689v-34.133h261.689v-261.689h34.133v261.689h273.067z" />
<glyph unicode="&#xe807;" glyph-name="icon" d="M550.306 782.458h-75.373l-249.184-613.64h90.453l62.951 159.627h262.477l62.974-159.627h95.755l-250.053 613.64zM403.098 400.255l107.305 274.897 107.28-274.897h-214.586zM1024 448c0 286.204-225.796 512-511.999 512s-512.001-225.796-512.001-512c0-286.204 225.797-512 512.001-512s511.999 225.797 511.999 512v0zM962.165 448c0-245.94-204.249-450.164-450.164-450.164-245.941 0-450.161 204.224-450.161 450.164s204.221 450.164 450.161 450.164c245.915 0 450.164-204.224 450.164-450.164v0z" />
<glyph unicode="&#xe808;" glyph-name="logo" horiz-adv-x="4195" d="M499.718 326.19c0 109.528-24.641 157.448-61.607 198.517-38.336 41.077-95.832 71.191-191.673 71.191-95.832 0-171.135-36.957-212.212-64.34l27.382-52.031c13.695 10.954 88.998 54.764 187.571 54.764 99.943 0 177.978-57.505 177.978-173.876v-34.225l-173.876-6.843c-171.135-6.852-253.281-82.146-253.281-191.673s88.989-191.674 212.212-191.674c123.214 0 191.674 75.294 214.944 102.676v-88.989h72.562v376.503zM427.156 113.978c-30.114-47.92-98.573-116.371-201.258-116.371-102.676 0-154.707 61.607-154.707 130.066 0 68.45 42.448 125.955 175.246 132.798l180.719 10.955v-157.448zM1063.784 123.562c0 120.482-119.12 161.551-198.525 188.933-78.035 27.382-146.494 56.134-146.494 121.853s50.661 101.314 121.853 101.314c71.191 0 115.001-24.641 158.819-72.561l43.809 43.809c-49.291 56.134-106.795 88.989-199.887 88.989-93.1 0-193.044-54.764-193.044-169.765s119.112-156.078 173.876-175.246c54.764-19.168 168.394-47.92 168.394-130.066s-69.821-120.482-149.226-120.482c-79.413 0-142.391 39.707-183.46 99.952l-50.661-41.077c39.707-65.718 113.639-123.215 231.38-123.215s223.166 68.451 223.166 187.562v0zM1679.873 93.44c0 0-68.451-93.1-212.212-93.1-143.753 0-242.326 113.639-242.326 271.087s112.268 260.132 239.594 260.132c125.955 0 180.719-61.616 208.101-93.1l45.18 47.912c-13.687 17.798-82.146 109.528-250.548 109.528-167.024 0-317.628-132.798-317.628-328.583 0-194.406 139.65-331.315 310.785-331.315s236.853 82.146 260.133 109.528l-41.077 47.912zM2142.607 586.323c0 0-27.382 9.576-68.45 9.576-68.459 0-138.28-43.81-161.551-130.058v119.112h-71.2v-635.266h71.2v342.278c0 109.528 34.225 162.921 45.18 177.978 10.946 15.066 47.912 57.505 109.528 57.505 34.225 0 53.394-5.473 68.451-10.955l6.843 69.83zM2353.432 810.851c0 32.855-26.012 58.875-58.867 58.875-32.863 0-58.875-26.020-58.875-58.875s26.011-58.875 58.875-58.875c32.855 0 58.867 26.020 58.867 58.875v0zM2330.161 584.953h-71.191v-635.266h71.191v635.266zM3144.767 267.315c0 188.941-123.223 328.583-312.155 328.583-91.73 0-177.987-36.957-239.594-132.798v431.267h-71.191v-944.68h71.191v120.482c53.394-93.1 141.012-134.169 236.853-134.169 191.673 0 314.896 143.753 314.896 331.315v0zM3069.464 267.315c0-146.494-86.257-266.976-239.594-266.976-154.707 0-239.594 120.482-239.594 266.976 0 147.864 84.887 266.976 239.594 266.976 153.337 0 239.594-119.112 239.594-266.976v0zM3836.158 298.808c0 171.135-120.482 297.090-287.514 297.090-168.402 0-308.044-132.798-308.044-328.583 0-194.406 119.112-331.315 303.933-331.315 184.83 0 264.244 95.833 264.244 95.833l-34.234 50.661c0 0-79.405-82.154-223.158-82.154-143.761 0-228.64 99.952-235.491 250.548h516.154c0 0 4.111 27.382 4.111 47.92v0zM3318.633 312.495c4.111 88.998 68.459 221.796 225.899 221.796 157.449 0 219.065-139.65 219.065-221.796h-444.963zM4027.755 897.194h-26.663l-88.15-217.077h31.998l22.269 56.468h92.852l22.277-56.468h33.874l-88.457 217.077zM3975.68 761.989l37.96 97.245 37.951-97.245h-75.91zM4195.326 778.878c0 101.246-79.876 181.122-181.121 181.122s-181.122-79.876-181.122-181.122c0-101.245 79.876-181.122 181.122-181.122s181.121 79.876 181.121 181.122v0zM4173.452 778.878c0-87.002-72.254-159.247-159.247-159.247-87.002 0-159.246 72.245-159.246 159.247s72.244 159.247 159.246 159.247c86.993 0 159.247-72.245 159.247-159.247v0z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
fonts/ascribe-logo.ttf Normal file

Binary file not shown.

BIN
fonts/ascribe-logo.woff Normal file

Binary file not shown.

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="595.28px" height="419.53px" viewBox="0 0 595.28 419.53" enable-background="new 0 0 595.28 419.53" xml:space="preserve">
<g>
<path d="M362.878,195.61c-1.647-6.692-4.142-13.088-7.412-19.008c-3.401-6.166-7.549-11.797-12.319-16.725
c-4.951-5.127-10.511-9.553-16.519-13.149c-6.202-3.728-12.833-6.607-19.722-8.564c-7.088-2.003-14.352-3.075-21.602-3.188
c-7.417-0.083-14.843,0.769-22.059,2.592c-4.731,1.212-9.302,2.843-13.703,4.791l7.269,14.875
c3.387-1.463,6.895-2.699,10.517-3.627c5.788-1.463,11.74-2.16,17.735-2.082c5.8,0.09,11.635,0.954,17.334,2.562
c5.471,1.554,10.754,3.852,15.717,6.833c4.77,2.856,9.184,6.372,13.131,10.46c3.762,3.886,7.035,8.333,9.73,13.219
c2.57,4.651,4.53,9.687,5.832,14.971c1.237,5.021,1.809,10.269,1.693,15.6c-0.105,5.053-0.9,10.154-2.362,15.145
c-1.409,4.783-3.46,9.395-6.094,13.703c-2.521,4.121-5.625,7.939-9.224,11.348c-3.437,3.256-7.326,6.064-11.562,8.352
c-4.053,2.188-8.428,3.838-13.016,4.914c-4.344,1.023-8.87,1.465-13.477,1.32c-4.341-0.141-8.718-0.875-12.994-2.176
c-4.068-1.244-7.992-3.043-11.661-5.348c-3.473-2.182-6.704-4.871-9.592-7.98c-2.712-2.932-5.052-6.266-6.955-9.906
c-1.799-3.445-3.146-7.162-4.008-11.059c-0.815-3.658-1.137-7.471-0.957-11.338c0.167-3.615,0.837-7.264,1.997-10.867
c1.049-3.288,2.634-6.606,4.587-9.602c1.839-2.828,4.165-5.532,6.732-7.829c2.425-2.172,5.2-4.05,8.23-5.574
c2.81-1.405,5.947-2.48,9.076-3.11c2.926-0.591,6.12-0.8,9.219-0.598c2.85,0.188,5.858,0.804,8.694,1.777
c2.586,0.892,5.217,2.22,7.612,3.845c2.159,1.464,4.247,3.347,6.052,5.468c1.635,1.903,3.084,4.162,4.205,6.548
c1.023,2.166,1.791,4.617,2.225,7.114c0.392,2.204,0.48,4.645,0.258,7.045c-0.201,2.135-0.735,4.404-1.526,6.51
c-0.75,1.959-1.806,3.893-3.056,5.598c-1.149,1.572-2.592,3.064-4.174,4.32c-1.398,1.107-3.125,2.129-4.843,2.861
c-1.534,0.648-3.352,1.137-5.082,1.369c-1.563,0.199-3.315,0.184-4.947-0.047c-1.428-0.201-2.979-0.645-4.357-1.248
c-1.193-0.523-2.489-1.326-3.561-2.209c-0.942-0.771-1.886-1.797-2.605-2.832c-0.609-0.871-1.173-1.992-1.563-3.123
c-0.299-0.855-0.512-1.979-0.568-3.033c-0.045-0.795,0.052-1.85,0.234-2.666c0.16-0.684,0.506-1.557,0.893-2.246
c0.262-0.471,0.75-1.094,1.267-1.594c0.28-0.279,0.826-0.666,1.228-0.875c0.257-0.135,0.845-0.338,1.163-0.402
c0.182-0.035,0.657-0.037,0.931-0.008c4.538,0.547,8.644-2.734,9.167-7.275c0.521-4.541-2.736-8.646-7.277-9.168
c-0.898-0.103-3.262-0.293-5.914,0.191c-1.95,0.389-4.019,1.104-5.693,1.97c-1.114,0.58-3.28,1.848-5.187,3.743
c-1.614,1.559-3.085,3.463-4.134,5.345c-1.135,2.027-2.045,4.35-2.577,6.625c-0.535,2.393-0.756,4.984-0.627,7.271
c0.099,1.828,0.434,4.629,1.461,7.555c0.879,2.555,2.137,5.037,3.623,7.16c1.583,2.275,3.546,4.402,5.667,6.146
c2.256,1.855,4.823,3.439,7.435,4.586c2.768,1.209,5.775,2.064,8.678,2.473c3.095,0.443,6.339,0.465,9.418,0.068
c3.234-0.432,6.479-1.309,9.398-2.545c3.105-1.326,6.101-3.1,8.656-5.127c2.727-2.164,5.233-4.762,7.242-7.508
c2.105-2.873,3.891-6.145,5.178-9.51c1.316-3.506,2.19-7.246,2.53-10.838c0.363-3.928,0.214-7.788-0.437-11.451
c-0.68-3.911-1.91-7.825-3.553-11.298c-1.761-3.754-3.986-7.211-6.595-10.25c-2.778-3.264-5.925-6.097-9.351-8.415
c-3.629-2.464-7.502-4.413-11.51-5.795c-4.27-1.468-8.64-2.356-13.008-2.646c-4.605-0.289-9.173,0.005-13.562,0.892
c-4.606,0.927-9.049,2.45-13.225,4.54c-4.354,2.188-8.342,4.896-11.849,8.036c-3.687,3.298-6.906,7.043-9.564,11.13
c-2.787,4.276-4.968,8.853-6.477,13.58c-1.604,4.977-2.535,10.077-2.771,15.155c-0.247,5.332,0.203,10.613,1.333,15.689
c1.174,5.307,3.022,10.398,5.496,15.137c2.583,4.941,5.772,9.477,9.487,13.492c3.88,4.18,8.228,7.793,12.928,10.746
c4.905,3.08,10.16,5.488,15.634,7.164c5.678,1.727,11.493,2.695,17.286,2.887c0.755,0.023,1.51,0.035,2.263,0.035
c5.287,0,10.509-0.602,15.541-1.789c5.996-1.406,11.744-3.578,17.084-6.457c5.519-2.977,10.595-6.646,15.089-10.902
c4.653-4.41,8.678-9.365,11.959-14.727c3.39-5.545,6.03-11.486,7.856-17.676c1.873-6.408,2.892-12.957,3.026-19.457
C365.195,208.741,364.465,202.041,362.878,195.61"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -111,7 +111,11 @@ gulp.task('sass:build', function () {
]
}).on('error', sass.logError))
.pipe(gulpif(!argv.production, sourcemaps.write('./maps')))
.pipe(gulpif(argv.production, minifyCss()))
// We need to set `advanced` to false, as it merges
// some of the styles wrongly
.pipe(gulpif(argv.production, minifyCss({
advanced: false
})))
.pipe(gulp.dest('./build/css'))
.pipe(browserSync.stream());
});

View File

@ -2,17 +2,8 @@
<html>
<head>
<meta name="msapplication-config" content="<%= BASE_URL %>static/img/hq-favicons/browserconfig.xml">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#48DACB">
<title>ascribe</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//brick.a.ssl.fastly.net/Source+Sans+Pro:400,600,700,900">
<link rel="stylesheet" href="<%= BASE_URL %>static/css/main.css">
<% DEBUG && print('<link rel="stylesheet" href="' + BASE_URL + 'static/css/maps/main.css.map">') %>
@ -25,6 +16,12 @@
<% DEBUG && print('window.DEBUG = true'); %>
<% DEBUG && print('window.CREDENTIALS = \'' + CREDENTIALS + '\''); %>
</script>
<!-- Typekit gibson font -->
<script src="https://use.typekit.net/gma2yhj.js"></script>
<script>
try {Typekit.load({ async: true });}
catch(e){}
</script>
</head>
<body>
<div id="main"></div>

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import ApplicationFetcher from '../fetchers/application_fetcher';

View File

@ -1,8 +1,9 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import CoaFetcher from '../fetchers/coa_fetcher';
import Q from 'q';
class CoaActions {
constructor() {
@ -12,23 +13,38 @@ class CoaActions {
);
}
fetchOne(id) {
CoaFetcher.fetchOne(id)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.logGlobal(err);
});
fetchOrCreate(id, bitcoinId) {
return Q.Promise((resolve, reject) => {
CoaFetcher.fetchOne(id)
.then((res) => {
if (res.coa) {
this.actions.updateCoa(res.coa);
resolve(res.coa);
}
else {
this.actions.create(bitcoinId);
}
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCoa(null);
reject(err);
});
});
}
create(edition) {
CoaFetcher.create(edition.bitcoin_id)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.logGlobal(err);
});
create(bitcoinId) {
return Q.Promise((resolve, reject) => {
CoaFetcher.create(bitcoinId)
.then((res) => {
this.actions.updateCoa(res.coa);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCoa(null);
reject(err);
});
});
}
}

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import Q from 'q';
import OwnershipFetcher from '../fetchers/ownership_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';
import Q from 'q';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import EditionFetcher from '../fetchers/edition_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import Q from 'q';
import EditionListFetcher from '../fetchers/edition_list_fetcher.js';
@ -33,6 +33,10 @@ class EditionListActions {
EditionListFetcher
.fetch(pieceId, page, pageSize, orderBy, orderAsc, filterBy)
.then((res) => {
if(res && !res.editions) {
throw new Error('Piece has no editions to fetch.');
}
this.actions.updateEditionList({
pieceId,
page,
@ -46,6 +50,7 @@ class EditionListActions {
resolve(res);
})
.catch((err) => {
console.logGlobal(err);
reject(err);
});
});

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { altThirdParty } from '../alt';
class EventActions {
@ -16,4 +16,4 @@ class EventActions {
}
}
export default alt.createActions(EventActions);
export default altThirdParty.createActions(EventActions);

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
class GlobalNotificationActions {

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import LicenseFetcher from '../fetchers/license_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import Q from 'q';
import NotificationFetcher from '../fetchers/notification_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import OwnershipFetcher from '../fetchers/ownership_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import PieceFetcher from '../fetchers/piece_fetcher';

View File

@ -1,10 +1,11 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import Q from 'q';
import PieceListFetcher from '../fetchers/piece_list_fetcher';
class PieceListActions {
constructor() {
this.generateActions(
@ -21,17 +22,16 @@ class PieceListActions {
this.actions.updatePieceList({
page,
pageSize,
search,
orderBy,
orderAsc,
filterBy,
search: '',
pieceList: [],
pieceListCount: -1,
unfilteredPieceListCount: -1
});
// afterwards, we can load the list
return Q.Promise((resolve, reject) => {
PieceListFetcher
.fetch(page, pageSize, search, orderBy, orderAsc, filterBy)

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import Q from 'q';
import PrizeListFetcher from '../fetchers/prize_list_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { altUser } from '../alt';
import UserFetcher from '../fetchers/user_fetcher';
@ -13,7 +13,7 @@ class UserActions {
}
fetchCurrentUser() {
return UserFetcher.fetchOne()
UserFetcher.fetchOne()
.then((res) => {
this.actions.updateCurrentUser(res.users[0]);
})
@ -24,7 +24,7 @@ class UserActions {
}
logoutCurrentUser() {
return UserFetcher.logout()
UserFetcher.logout()
.then(() => {
this.actions.deleteCurrentUser();
})
@ -34,4 +34,4 @@ class UserActions {
}
}
export default alt.createActions(UserActions);
export default altUser.createActions(UserActions);

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { alt } from '../alt';
import WalletSettingsFetcher from '../fetchers/wallet_settings_fetcher';

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../alt';
import { altWhitelabel } from '../alt';
import WhitelabelFetcher from '../fetchers/whitelabel_fetcher';
@ -26,4 +26,4 @@ class WhitelabelActions {
}
}
export default alt.createActions(WhitelabelActions);
export default altWhitelabel.createActions(WhitelabelActions);

View File

@ -2,4 +2,7 @@
import Alt from 'alt';
export default new Alt();
export let alt = new Alt();
export let altThirdParty = new Alt();
export let altUser = new Alt();
export let altWhitelabel = new Alt();

View File

@ -3,20 +3,23 @@
require('babel/polyfill');
import React from 'react';
import Router from 'react-router';
import { Router, Redirect } from 'react-router';
import history from './history';
/* eslint-disable */
import fetch from 'isomorphic-fetch';
/* eslint-enable */
import ApiUrls from './constants/api_urls';
import { updateApiUrls } from './constants/api_urls';
import appConstants from './constants/application_constants';
import AppConstants from './constants/application_constants';
import getRoutes from './routes';
import requests from './utils/requests';
import { updateApiUrls } from './constants/api_urls';
import { getSubdomainSettings } from './utils/constants_utils';
import { initLogging } from './utils/error_utils';
import { getSubdomain } from './utils/general_utils';
import EventActions from './actions/event_actions';
@ -44,15 +47,14 @@ requests.defaults({
}
});
class AppGateway {
start() {
let settings;
let subdomain = window.location.host.split('.')[0];
let subdomain = getSubdomain();
try {
settings = getSubdomainSettings(subdomain);
appConstants.whitelabel = settings;
AppConstants.whitelabel = settings;
updateApiUrls(settings.type, subdomain);
this.load(settings);
} catch(err) {
@ -66,22 +68,36 @@ class AppGateway {
load(settings) {
let type = 'default';
let subdomain = 'www';
let redirectRoute = (<Redirect from="/" to="/collection" />);
if (settings) {
type = settings.type;
subdomain = settings.subdomain;
}
// www and cc do not have a landing page
if(subdomain && subdomain !== 'cc') {
redirectRoute = null;
}
// Adds a client specific class to the body for whitelabel styling
window.document.body.classList.add('client--' + subdomain);
// Send the applicationWillBoot event to the third-party stores
EventActions.applicationWillBoot(settings);
window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => {
React.render(
<App />,
document.getElementById('main')
);
EventActions.routeDidChange();
});
// `history.listen` is called on every route change, which is perfect for
// us in that case.
history.listen(EventActions.routeDidChange);
React.render((
<Router history={history}>
{redirectRoute}
{getRoutes(type, subdomain)}
</Router>
), document.getElementById('main'));
// Send the applicationDidBoot event to the third-party stores
EventActions.applicationDidBoot(settings);
}
}

View File

@ -9,21 +9,49 @@ let AccordionList = React.createClass({
className: React.PropTypes.string,
children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired,
loadingElement: React.PropTypes.element,
count: React.PropTypes.number
count: React.PropTypes.number,
itemList: React.PropTypes.arrayOf(React.PropTypes.object),
search: React.PropTypes.string,
searchFor: React.PropTypes.func
},
clearSearch() {
this.props.searchFor('');
},
render() {
const { search } = this.props;
if(this.props.itemList && this.props.itemList.length > 0) {
return (
<div className={this.props.className}>
{this.props.children}
</div>
);
} else if(this.props.count === 0) {
} else if(this.props.count === 0 && !search) {
return (
<div className="ascribe-accordion-list-placeholder">
<p className="text-center">{getLangText('We could not find any works related to you...')}</p>
<p className="text-center">{getLangText('To register one, click')} <a href="register_piece">{getLangText('here')}</a>!</p>
<p className="text-center">
{getLangText('We could not find any works related to you...')}
</p>
<p className="text-center">
{getLangText('To register one, click')}&nbsp;
<a href="register_piece">{getLangText('here')}</a>!
</p>
</div>
);
} else if(this.props.count === 0 && search) {
return (
<div className="ascribe-accordion-list-placeholder">
<p className="text-center">
{getLangText('We could not find any works related to you...')}
</p>
<p className="text-center">
{getLangText('You\'re filtering by the search keyword: \'%s\' ', search)}
</p>
<p className="text-center">
<button className="btn btn-sm btn-default" onClick={this.clearSearch}>{getLangText('Clear search')}</button>
</p>
</div>
);
} else {

View File

@ -1,7 +1,8 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
let AccordionListItem = React.createClass({
propTypes: {
@ -12,39 +13,57 @@ let AccordionListItem = React.createClass({
subheading: React.PropTypes.object,
subsubheading: React.PropTypes.object,
buttons: React.PropTypes.object,
linkData: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
mixins: [Router.Navigation],
render() {
const { linkData,
className,
thumbnail,
heading,
subheading,
subsubheading,
buttons,
badge,
children } = this.props;
return (
<div className="row">
<div className={this.props.className}>
<div className={className}>
<div className="wrapper">
<div className="col-xs-4 col-sm-3 col-md-2 col-lg-2 clear-paddings">
<div className="thumbnail-wrapper">
{this.props.thumbnail}
<div className="pull-left">
<Link to={linkData}>
<div className="thumbnail-wrapper">
{thumbnail}
</div>
</Link>
</div>
</div>
<div className="col-xs-8 col-sm-9 col-md-9 col-lg-9 col-md-offset-1 col-lg-offset-1 accordion-list-item-header">
{this.props.heading}
{this.props.subheading}
{this.props.subsubheading}
{this.props.buttons}
</div>
<div className="accordion-list-item-header">
<Link to={linkData}>
{heading}
</Link>
<Link to={linkData}>
{subheading}
{subsubheading}
</Link>
<div className="accordion-list-item-buttons">
{buttons}
</div>
</div>
<span style={{'clear': 'both'}}></span>
<div className="request-action-badge">
{this.props.badge}
{badge}
</div>
</div>
</div>
{this.props.children}
{children}
</div>
);
}

View File

@ -11,6 +11,7 @@ import PieceListStore from '../../stores/piece_list_store';
import Button from 'react-bootstrap/lib/Button';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
@ -75,7 +76,10 @@ let AccordionListItemEditionWidget = React.createClass({
// PLEASE FUTURE TIM, DO NOT FUCKING REMOVE IT AGAIN!
if(typeof this.state.editionList[pieceId] === 'undefined') {
return (
<span className="glyph-ascribe-spool-chunked ascribe-color spin"/>
<AscribeSpinner
size='sm'
color='white'
classNames='pull-right margin-left-2px'/>
);
} else {
return (
@ -98,7 +102,7 @@ let AccordionListItemEditionWidget = React.createClass({
return (
<CreateEditionsButton
label={getLangText('Create editions')}
className="btn-xs pull-right"
className="btn-secondary btn-sm pull-right"
piece={piece}
toggleCreateEditionsDialog={this.props.toggleCreateEditionsDialog}
onPollingSuccess={this.props.onPollingSuccess}/>
@ -112,12 +116,12 @@ let AccordionListItemEditionWidget = React.createClass({
if(piece.first_edition === null) {
// user has deleted all his editions and only the piece is showing
return (
<Button
<button
disabled
title={getLangText('All editions for this have been deleted already.')}
className={classNames('btn', 'btn-default', 'btn-xs', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
className={classNames('btn', 'btn-default', 'btn-secondary', 'btn-sm', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
{'0 ' + getLangText('Editions')}
</Button>
</button>
);
} else {
let editionMapping = piece && piece.first_edition ? piece.first_edition.num_editions_available + '/' + piece.num_editions : '';
@ -125,7 +129,7 @@ let AccordionListItemEditionWidget = React.createClass({
return (
<button
onClick={this.toggleTable}
className={classNames('btn', 'btn-default', 'btn-xs', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
className={classNames('btn', 'btn-secondary', 'btn-sm', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
{editionMapping + ' ' + getLangText('Editions')} {this.getGlyphicon()}
</button>
);

View File

@ -1,14 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import AccordionListItem from './accordion_list_item';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let AccordionListItemPiece = React.createClass({
propTypes: {
@ -24,51 +22,54 @@ let AccordionListItemPiece = React.createClass({
badge: React.PropTypes.object
},
mixins: [Router.Navigation],
getLinkData() {
let { piece } = this.props;
if(piece && piece.first_edition) {
return `/editions/${piece.first_edition.bitcoin_id}`;
if(this.props.piece && this.props.piece.first_edition) {
return {
to: 'edition',
params: {
editionId: this.props.piece.first_edition.bitcoin_id
}
};
} else {
return {
to: 'piece',
params: {
pieceId: this.props.piece.id
}
};
return `/pieces/${piece.id}`;
}
},
render() {
const { className, piece, artistName, buttons, badge, children, subsubheading } = this.props;
const { url, url_safe } = piece.thumbnail;
let thumbnail;
// Since we're going to refactor the thumbnail generation anyway at one point,
// for not use the annoying ascribe_spiral.png, we're matching the url against
// this name and replace it with a CSS version of the new logo.
if(url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) {
thumbnail = (
<span className="ascribe-logo-circle">
<span>A</span>
</span>
);
} else {
thumbnail = (
<div style={{backgroundImage: 'url("' + url_safe + '")'}}/>
);
}
return (
<AccordionListItem
className={this.props.className}
thumbnail={
<Link {...this.getLinkData()}>
<img src={this.props.piece.thumbnail.url_safe}/>
</Link>}
heading={
<Link {...this.getLinkData()}>
<h1>{this.props.piece.title}</h1>
</Link>}
className={className}
thumbnail={thumbnail}
heading={<h1>{piece.title}</h1>}
subheading={
<h3>
{getLangText('by ')}
{this.props.artistName ? this.props.artistName : this.props.piece.artist_name}
{artistName ? artistName : piece.artist_name}
</h3>
}
subsubheading={this.props.subsubheading}
buttons={this.props.buttons}
badge={this.props.badge}
subsubheading={subsubheading}
buttons={buttons}
badge={badge}
linkData={this.getLinkData()}
>
{this.props.children}
{children}
</AccordionListItem>
);
}

View File

@ -110,7 +110,7 @@ let AccordionListItemTableEditions = React.createClass({
showExpandOption = true;
}
let transition = new TransitionModel('edition', 'editionId', 'bitcoin_id', (e) => e.stopPropagation() );
let transition = new TransitionModel('editions', 'editionId', 'bitcoin_id', (e) => e.stopPropagation() );
let columnList = [
new ColumnModel(

View File

@ -1,22 +1,31 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Header from '../components/header';
import Footer from '../components/footer';
import GlobalNotification from './global_notification';
import getRoutes from '../routes';
let RouteHandler = Router.RouteHandler;
let AscribeApp = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
render() {
let { children, routes } = this.props;
return (
<div className="container ascribe-default-app">
<Header routes={getRoutes()} />
<RouteHandler />
<Header routes={routes} />
{/* Routes are injected here */}
<div className="ascribe-body">
{children}
</div>
<Footer />
<GlobalNotification />
<div id="modal" className="container"></div>

View File

@ -5,6 +5,8 @@ import React from 'react';
import EditionListActions from '../../actions/edition_list_actions';
import EditionListStore from '../../stores/edition_list_store';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import classNames from 'classnames';
@ -75,14 +77,17 @@ let CreateEditionsButton = React.createClass({
return (
<button
disabled
className={classNames('btn', 'btn-default', this.props.className)}>
{getLangText('Creating editions')} <span className="glyph-ascribe-spool-chunked spin"/>
className={classNames('btn', this.props.className)}>
{getLangText('Creating editions')} <AscribeSpinner
size='sm'
color='white'
classNames='pull-right margin-left-2px'/>
</button>
);
} else {
return (
<button
className={classNames('btn', 'btn-default', this.props.className)}
className={classNames('btn', this.props.className)}
onClick={this.props.toggleCreateEditionsDialog}>
{this.props.label}
</button>

View File

@ -1,7 +1,6 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Button from 'react-bootstrap/lib/Button';
@ -24,8 +23,6 @@ let DeleteButton = React.createClass({
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation],
render() {
let availableAcls;
let btnDelete;

View File

@ -1,20 +1,16 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link, History } from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import Button from 'react-bootstrap/lib/Button';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import CoaActions from '../../actions/coa_actions';
import CoaStore from '../../stores/coa_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions';
import HistoryIterator from './history_iterator';
@ -26,46 +22,37 @@ import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_property';
import LicenseDetail from './license_detail';
import EditionFurtherDetails from './further_details';
import FurtherDetails from './further_details';
import ListRequestActions from './../ascribe_forms/list_form_request_actions';
import AclButtonList from './../ascribe_buttons/acl_button_list';
import UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import EditionActionPanel from './edition_action_panel';
import Note from './note';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let Link = Router.Link;
/**
* This is the component that implements display-specific functionality
*/
let Edition = React.createClass({
propTypes: {
edition: React.PropTypes.object,
loadEdition: React.PropTypes.func
loadEdition: React.PropTypes.func,
location: React.PropTypes.object
},
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return mergeOptions(
UserStore.getState(),
PieceListStore.getState()
);
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
@ -80,31 +67,12 @@ let Edition = React.createClass({
CoaActions.flushCoa();
UserStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleDeleteSuccess(response) {
this.refreshCollection();
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
refreshCollection() {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.refreshEditionList({pieceId: this.props.edition.parent});
},
render() {
return (
<Row>
@ -114,19 +82,16 @@ let Edition = React.createClass({
</Col>
<Col md={6} className="ascribe-edition-details">
<div className="ascribe-detail-header">
<hr/>
<hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1>
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.edition.date_created.slice(0, 4) } />
<hr/>
</div>
<EditionSummary
handleSuccess={this.props.loadEdition}
refreshCollection={this.refreshCollection}
currentUser={this.state.currentUser}
edition={this.props.edition}
handleDeleteSuccess={this.handleDeleteSuccess}/>
currentUser={this.state.currentUser}
handleSuccess={this.props.loadEdition}/>
<CollapsibleParagraph
title={getLangText('Certificate of Authenticity')}
show={this.props.edition.acl.acl_coa === true}>
@ -186,12 +151,13 @@ let Edition = React.createClass({
show={this.props.edition.acl.acl_edit
|| Object.keys(this.props.edition.extra_data).length > 0
|| this.props.edition.other_data.length > 0}>
<EditionFurtherDetails
<FurtherDetails
editable={this.props.edition.acl.acl_edit}
pieceId={this.props.edition.parent}
extraData={this.props.edition.extra_data}
otherData={this.props.edition.other_data}
handleSuccess={this.props.loadEdition}/>
handleSuccess={this.props.loadEdition}
location={this.props.location}/>
</CollapsibleParagraph>
<CollapsibleParagraph
@ -209,29 +175,14 @@ let Edition = React.createClass({
let EditionSummary = React.createClass({
propTypes: {
edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
currentUser: React.PropTypes.object,
handleDeleteSuccess: React.PropTypes.func,
refreshCollection: React.PropTypes.func
},
getTransferWithdrawData(){
return {'bitcoin_id': this.props.edition.bitcoin_id};
handleSuccess: React.PropTypes.func
},
handleSuccess() {
this.props.refreshCollection();
this.props.handleSuccess();
},
showNotification(response){
this.props.handleSuccess();
if (response){
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
getStatus(){
let status = null;
if (this.props.edition.status.length > 0){
@ -246,79 +197,26 @@ let EditionSummary = React.createClass({
return status;
},
getActions(){
let actions = null;
if (this.props.edition &&
this.props.edition.notifications &&
this.props.edition.notifications.length > 0){
actions = (
<ListRequestActions
pieceOrEditions={[this.props.edition]}
currentUser={this.props.currentUser}
handleSuccess={this.showNotification}
notifications={this.props.edition.notifications}/>);
}
else {
let withdrawButton = null;
if (this.props.edition.status.length > 0 && this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer) {
withdrawButton = (
<Form
url={ApiUrls.ownership_transfers_withdraw}
getFormData={this.getTransferWithdrawData}
handleSuccess={this.showNotification}
className='inline'
isInline={true}>
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
WITHDRAW TRANSFER
</Button>
</Form>
);
}
let unconsignRequestButton = null;
if (this.props.edition.acl.acl_request_unconsign) {
unconsignRequestButton = (
<UnConsignRequestButton
currentUser={this.props.currentUser}
edition={this.props.edition}
handleSuccess={this.props.handleSuccess} />
);
}
actions = (
<Row>
<Col md={12}>
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.props.edition.acl}
editions={[this.props.edition]}
handleSuccess={this.handleSuccess}>
{withdrawButton}
<DeleteButton
handleSuccess={this.props.handleDeleteSuccess}
editions={[this.props.edition]}/>
{unconsignRequestButton}
</AclButtonList>
</Col>
</Row>);
}
return actions;
},
render() {
let { edition, currentUser } = this.props;
return (
<div className="ascribe-detail-header">
<EditionDetailProperty
label={getLangText('EDITION')}
value={this.props.edition.edition_number + ' ' + getLangText('of') + ' ' + this.props.edition.num_editions} />
value={ edition.edition_number + ' ' + getLangText('of') + ' ' + edition.num_editions} />
<EditionDetailProperty
label={getLangText('ID')}
value={ this.props.edition.bitcoin_id }
value={ edition.bitcoin_id }
ellipsis={true} />
<EditionDetailProperty
label={getLangText('OWNER')}
value={ this.props.edition.owner } />
<LicenseDetail license={this.props.edition.license_type}/>
value={ edition.owner } />
<LicenseDetail license={edition.license_type}/>
{this.getStatus()}
{this.getActions()}
<EditionActionPanel
edition={edition}
currentUser={currentUser}
handleSuccess={this.handleSuccess} />
<hr/>
</div>
);
@ -336,12 +234,13 @@ let CoaDetails = React.createClass({
},
componentDidMount() {
let { edition } = this.props;
CoaStore.listen(this.onChange);
if(this.props.edition.coa) {
CoaActions.fetchOne(this.props.edition.coa);
if(edition.coa) {
CoaActions.fetchOrCreate(edition.coa, edition.bitcoin_id);
}
else {
CoaActions.create(this.props.edition);
CoaActions.create(edition.bitcoin_id);
}
},
@ -363,7 +262,7 @@ let CoaDetails = React.createClass({
{getLangText('Download')} <Glyphicon glyph="cloud-download"/>
</button>
</a>
<Link to="coa_verify">
<Link to="/coa_verify">
<button className="btn btn-default btn-xs">
{getLangText('Verify')} <Glyphicon glyph="check"/>
</button>
@ -381,7 +280,7 @@ let CoaDetails = React.createClass({
}
return (
<div className="text-center">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
<AscribeSpinner color='dark-blue' size='lg'/>
</div>
);
}

View File

@ -0,0 +1,169 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import Button from 'react-bootstrap/lib/Button';
import EditionListActions from '../../actions/edition_list_actions';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store';
import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import ListRequestActions from './../ascribe_forms/list_form_request_actions';
import AclButtonList from './../ascribe_buttons/acl_button_list';
import UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AclProxy from '../acl_proxy';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
/*
A component that handles all the actions inside of the edition detail
handleSuccess requires a loadEdition action (could be refactored)
*/
let EditionActionPanel = React.createClass({
propTypes: {
edition: React.PropTypes.object,
currentUser: React.PropTypes.object,
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation],
getInitialState() {
return PieceListStore.getState();
},
componentDidMount() {
PieceListStore.listen(this.onChange);
},
componentWillUnmount() {
PieceListStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
handleDeleteSuccess(response) {
this.refreshCollection();
EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections();
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
},
refreshCollection() {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.refreshEditionList({pieceId: this.props.edition.parent});
},
handleSuccess(response){
this.refreshCollection();
this.props.handleSuccess();
if (response){
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
render(){
let {edition, currentUser} = this.props;
if (edition &&
edition.notifications &&
edition.notifications.length > 0){
return (
<ListRequestActions
pieceOrEditions={[edition]}
currentUser={currentUser}
handleSuccess={this.handleSuccess}
notifications={edition.notifications}/>);
}
else {
return (
<Row>
<Col md={12}>
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={edition.acl}
editions={[edition]}
handleSuccess={this.handleSuccess}>
<AclProxy
aclObject={edition.acl}
aclName="acl_withdraw_transfer">
<Form
url={ApiUrls.ownership_transfers_withdraw}
handleSuccess={this.handleSuccess}
className='inline'
isInline={true}>
<Property
name="bitcoin_id"
hidden={true}>
<input
type="text"
value={edition.bitcoin_id} />
</Property>
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
{getLangText('WITHDRAW TRANSFER')}
</Button>
</Form>
</AclProxy>
<AclProxy
aclObject={edition.acl}
aclName="acl_withdraw_consign">
<Form
url={ApiUrls.ownership_consigns_withdraw}
handleSuccess={this.handleSuccess}
className='inline'
isInline={true}>
<Property
name="bitcoin_id"
hidden={true}>
<input
type="text"
value={edition.bitcoin_id} />
</Property>
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
{getLangText('WITHDRAW CONSIGN')}
</Button>
</Form>
</AclProxy>
<AclProxy
aclObject={edition.acl}
aclName="acl_request_unconsign">
<UnConsignRequestButton
currentUser={currentUser}
edition={edition}
handleSuccess={this.handleSuccess} />
</AclProxy>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
editions={[edition]}/>
</AclButtonList>
</Col>
</Row>
);
}
}
});
export default EditionActionPanel;

View File

@ -7,14 +7,19 @@ import EditionStore from '../../stores/edition_store';
import Edition from './edition';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { setDocumentTitle } from '../../utils/dom_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let EditionContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return EditionStore.getState();
},
@ -62,15 +67,18 @@ let EditionContainer = React.createClass({
render() {
if(this.state.edition && this.state.edition.title) {
setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', '));
return (
<Edition
edition={this.state.edition}
loadEdition={this.loadEdition}/>
loadEdition={this.loadEdition}
location={this.props.location}/>
);
} else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
<AscribeSpinner color='dark-blue' size='lg'/>
</div>
);
}

View File

@ -9,7 +9,6 @@ import Form from './../ascribe_forms/form';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -17,13 +16,15 @@ import FurtherDetailsFileuploader from './further_details_fileuploader';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let FurtherDetails = React.createClass({
propTypes: {
editable: React.PropTypes.bool,
pieceId: React.PropTypes.number,
extraData: React.PropTypes.object,
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
handleSuccess: React.PropTypes.func
handleSuccess: React.PropTypes.func,
location: React.PropTypes.object
},
getInitialState() {
@ -85,7 +86,8 @@ let FurtherDetails = React.createClass({
overrideForm={true}
pieceId={this.props.pieceId}
otherData={this.props.otherData}
multiple={true}/>
multiple={true}
location={this.props.location}/>
</Form>
</Col>
</Row>

View File

@ -20,7 +20,8 @@ let FurtherDetailsFileuploader = React.createClass({
submitFile: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool,
multiple: React.PropTypes.bool
multiple: React.PropTypes.bool,
location: React.PropTypes.object
},
getDefaultProps() {
@ -43,7 +44,7 @@ let FurtherDetailsFileuploader = React.createClass({
return (
<Property
label="Additional files (max. 50MB per file)">
label="Additional files">
<ReactS3FineUploader
uploadStarted={this.props.uploadStarted}
keyRoutine={{
@ -88,7 +89,8 @@ let FurtherDetailsFileuploader = React.createClass({
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}
multiple={this.props.multiple}/>
multiple={this.props.multiple}
location={this.props.location}/>
</Property>
);
}

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store';
@ -35,16 +35,21 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import Note from './note';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -132,7 +137,7 @@ let PieceContainer = React.createClass({
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
getCreateEditionsDialog() {
@ -207,6 +212,8 @@ let PieceContainer = React.createClass({
render() {
if(this.state.piece && this.state.piece.title) {
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
return (
<Piece
piece={this.state.piece}
@ -261,7 +268,8 @@ let PieceContainer = React.createClass({
pieceId={this.state.piece.id}
extraData={this.state.piece.extra_data}
otherData={this.state.piece.other_data}
handleSuccess={this.loadPiece}/>
handleSuccess={this.loadPiece}
location={this.props.location}/>
</CollapsibleParagraph>
</Piece>
@ -269,7 +277,7 @@ let PieceContainer = React.createClass({
} else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
<AscribeSpinner color='dark-blue' size='lg'/>
</div>
);
}

View File

@ -8,6 +8,7 @@ import Property from '../ascribe_forms/property';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AscribeSpinner from '../ascribe_spinner';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
@ -43,12 +44,12 @@ let CreateEditionsForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Create editions')}
</button>}
spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<button className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</button>
}>
<Property

View File

@ -8,7 +8,7 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -43,7 +43,9 @@ let ConsignForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='consignee'

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import ContractListActions from '../../actions/contract_list_actions';
import ContractListStore from '../../stores/contract_list_store';
@ -15,6 +15,7 @@ import PropertyCollapsible from './property_collapsible';
import InputTextAreaToggable from './input_textarea_toggable';
import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
@ -25,7 +26,7 @@ let ContractAgreementForm = React.createClass({
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getInitialState() {
return mergeOptions(
@ -57,7 +58,8 @@ let ContractAgreementForm = React.createClass({
let notification = 'Contract agreement send';
notification = new GlobalNotificationModel(notification, 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
getFormData(){
@ -100,12 +102,12 @@ let ContractAgreementForm = React.createClass({
handleSuccess={this.handleSubmitSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Send contract')}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">
@ -138,7 +140,7 @@ let ContractAgreementForm = React.createClass({
<div>
<p className="text-center">
{getLangText('No contracts uploaded yet, please go to the ')}
<a href="settings">{getLangText('settings page')}</a>
<a href="contract_settings">{getLangText('contract settings page')}</a>
{getLangText(' and create them.')}
</p>
</div>

View File

@ -47,7 +47,7 @@ let CopyrightAssociationForm = React.createClass({
handleSuccess={this.handleSubmitSuccess}>
<Property
name="copyright_association"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
label={getLangText('Copyright Association')}
style={{paddingBottom: 0}}>
<select defaultValue={selectedState} name="contract">

View File

@ -28,7 +28,8 @@ let CreateContractForm = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
}),
location: React.PropTypes.object
},
getInitialState() {
@ -86,7 +87,8 @@ let CreateContractForm = React.createClass({
areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload}/>
fileClassToUpload={this.props.fileClassToUpload}
location={this.props.location}/>
</Property>
<Property
name='name'

View File

@ -5,7 +5,7 @@ import React from 'react';
import Form from './form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -55,7 +55,9 @@ let EditionDeleteForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<p>{getLangText('Are you sure you would like to permanently delete this edition')}&#63;</p>

View File

@ -5,7 +5,7 @@ import React from 'react';
import Form from '../ascribe_forms/form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -46,7 +46,9 @@ let PieceDeleteForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<p>{getLangText('Are you sure you would like to permanently delete this piece')}&#63;</p>

View File

@ -15,7 +15,7 @@ import InputCheckbox from './input_checkbox';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
@ -130,6 +130,9 @@ let LoanForm = React.createClass({
src={contract.blob.url_safe}
alt="pdf"
pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
<a href={contract.blob.url_safe} target="_blank">
<span className="glyphicon glyphicon-download" aria-hidden="true"></span> {getLangText('Download contract')}
</a>
{/* We still need to send the server information that we're accepting */}
<InputCheckbox
style={{'display': 'none'}}
@ -141,7 +144,7 @@ let LoanForm = React.createClass({
return (
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
key="terms_explicitly"
@ -191,7 +194,7 @@ let LoanForm = React.createClass({
return (
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Finish process')}
</button>
);
@ -222,7 +225,9 @@ let LoanForm = React.createClass({
buttons={this.getButtons()}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<div className={classnames({'ascribe-form-header': true, 'hidden': !this.props.loanHeading})}>
<h3>{this.props.loanHeading}</h3>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -14,6 +14,7 @@ import Property from './property';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -24,10 +25,10 @@ let LoginForm = React.createClass({
submitMessage: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool,
onLogin: React.PropTypes.func
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getDefaultProps() {
return {
@ -52,50 +53,19 @@ let LoginForm = React.createClass({
onChange(state) {
this.setState(state);
// if user is already logged in, redirect him to piece list
if(this.state.currentUser && this.state.currentUser.email && this.props.redirectOnLoggedIn) {
// FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.transitionTo('pieces'), 0);
}
},
handleSuccess(){
handleSuccess({ success }){
let notification = new GlobalNotificationModel('Login successful', 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
// register_piece is waiting for a login success as login_container and it is wrapped
// in a slides_container component.
// The easiest way to check if the user was successfully logged in is to fetch the user
// in the user store (which is obviously only possible if the user is logged in), since
// register_piece is listening to the changes of the user_store.
UserActions.fetchCurrentUser()
.then(() => {
if(this.props.redirectOnLoginSuccess) {
/* Taken from http://stackoverflow.com/a/14916411 */
/*
We actually have to trick the Browser into showing the "save password" dialog
as Chrome expects the login page to be reloaded after the login.
Users on Stack Overflow claim this is a bug in chrome and should be fixed in the future.
Until then, we redirect the HARD way, but reloading the whole page using window.location
*/
window.location = AppConstants.baseUrl + 'collection';
} else if(this.props.onLogin) {
// In some instances we want to give a callback to an outer container,
// to show that the one login action the user triggered actually went through.
// We can not do this by listening on a store's state as it wouldn't really tell us
// if the user did log in or was just fetching the user's data again
this.props.onLogin();
}
})
.catch((err) => {
console.logGlobal(err);
});
if(success) {
UserActions.fetchCurrentUser();
}
},
render() {
let email = this.getQuery().email || null;
let email = this.props.location.query.email || null;
return (
<Form
className="ascribe-form-bordered"
@ -106,12 +76,12 @@ let LoginForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{this.props.submitMessage}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">

View File

@ -11,6 +11,7 @@ import InputFineUploader from './input_fineuploader';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
@ -29,7 +30,8 @@ let RegisterPieceForm = React.createClass({
onLoggedOut: React.PropTypes.func,
// For this form to work with SlideContainer, we sometimes have to disable it
disabled: React.PropTypes.bool
disabled: React.PropTypes.bool,
location: React.PropTypes.object
},
getDefaultProps() {
@ -83,14 +85,14 @@ let RegisterPieceForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
className="btn btn-default btn-wide"
disabled={!this.state.isUploadReady || this.props.disabled}>
{this.props.submitMessage}
</button>
}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">
@ -113,7 +115,8 @@ let RegisterPieceForm = React.createClass({
isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut}
disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}/>
enableLocalHashing={enableLocalHashing}
location={this.props.location}/>
</Property>
<Property
name='artist_name'

View File

@ -5,7 +5,7 @@ import React from 'react';
import Form from './form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -53,7 +53,9 @@ let EditionRemoveFromCollectionForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<p>{getLangText('Are you sure you would like to remove these editions from your collection')}&#63;</p>

View File

@ -5,7 +5,7 @@ import React from 'react';
import Form from './form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -46,7 +46,9 @@ let PieceRemoveFromCollectionForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<p>{getLangText('Are you sure you would like to remove this piece from your collection')}&#63;</p>

View File

@ -8,7 +8,7 @@ import InputTextAreaToggable from './input_textarea_toggable';
import Button from 'react-bootstrap/lib/Button';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -47,7 +47,9 @@ let ShareForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='share_emails'

View File

@ -1,11 +1,10 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { getLangText } from '../../utils/lang_utils';
import { History } from 'react-router';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -15,18 +14,21 @@ import Property from './property';
import InputCheckbox from './input_checkbox';
import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
let SignupForm = React.createClass({
propTypes: {
headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func,
children: React.PropTypes.element
children: React.PropTypes.element,
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getDefaultProps() {
return {
@ -34,6 +36,7 @@ let SignupForm = React.createClass({
submitMessage: getLangText('Sign up')
};
},
getInitialState() {
return UserStore.getState();
},
@ -48,27 +51,23 @@ let SignupForm = React.createClass({
onChange(state) {
this.setState(state);
// if user is already logged in, redirect him to piece list
if(this.state.currentUser && this.state.currentUser.email) {
this.transitionTo('pieces');
}
},
handleSuccess(response){
handleSuccess(response) {
if (response.user) {
let notification = new GlobalNotificationModel(getLangText('Sign up successful'), 'success', 50000);
GlobalNotificationActions.appendGlobalNotification(notification);
// Refactor this to its own component
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.');
}
else if (response.redirect) {
this.transitionTo('pieces');
} else {
UserActions.fetchCurrentUser();
}
},
getFormData() {
if (this.getQuery().token){
return {token: this.getQuery().token};
if (this.props.location.query.token){
return {token: this.props.location.query.token};
}
return null;
},
@ -77,7 +76,9 @@ let SignupForm = React.createClass({
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' +
getLangText('This password is securing your digital property like a bank account') + '.\n ' +
getLangText('Store it in a safe place') + '!';
let email = this.getQuery().email || null;
let email = this.props.location.query.email || null;
return (
<Form
className="ascribe-form-bordered"
@ -86,12 +87,12 @@ let SignupForm = React.createClass({
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
<button type="submit" className="btn btn-default btn-wide">
{this.props.submitMessage}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">
@ -130,7 +131,7 @@ let SignupForm = React.createClass({
{this.props.children}
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>

View File

@ -9,7 +9,7 @@ import InputCheckbox from './input_checkbox';
import Alert from 'react-bootstrap/lib/Alert';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils.js';
@ -40,7 +40,9 @@ let PieceSubmitToPrizeForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='artist_statement'
@ -64,7 +66,7 @@ let PieceSubmitToPrizeForm = React.createClass({
</Property>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>

View File

@ -9,8 +9,8 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AscribeSpinner from '../ascribe_spinner';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
@ -48,7 +48,9 @@ let TransferForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='transferee'

View File

@ -8,7 +8,7 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -45,7 +45,9 @@ let UnConsignForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='unconsign_message'

View File

@ -8,8 +8,7 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -45,7 +44,9 @@ let UnConsignRequestForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='unconsign_request_message'

View File

@ -22,6 +22,7 @@ let InputCheckbox = React.createClass({
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
]),
name: React.PropTypes.string,
// provided by Property
disabled: React.PropTypes.bool,
@ -50,7 +51,7 @@ let InputCheckbox = React.createClass({
// Developer's are used to define defaultValues for inputs via defaultValue, but since this is a
// input of type checkbox we warn the dev to not do that.
if(this.props.defaultValue) {
if(this.props.defaultValue) { //eslint-disable-line react/prop-types
console.warn('InputCheckbox is of type checkbox. Therefore its value is represented by checked and defaultChecked. defaultValue will do nothing!');
}
@ -102,8 +103,10 @@ let InputCheckbox = React.createClass({
return (
<span
style={this.props.style}
onClick={this.onChange}>
onClick={this.onChange}
name={this.props.name}>
<input
name={this.props.name}
type="checkbox"
ref="checkbox"
onChange={this.onChange}
@ -119,4 +122,4 @@ let InputCheckbox = React.createClass({
}
});
export default InputCheckbox;
export default InputCheckbox;

View File

@ -46,7 +46,8 @@ let InputFineUploader = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
}),
location: React.PropTypes.object
},
getInitialState() {
@ -106,7 +107,8 @@ let InputFineUploader = React.createClass({
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload}/>
fileClassToUpload={this.props.fileClassToUpload}
location={this.props.location}/>
);
}
});

View File

@ -31,6 +31,7 @@ let Property = React.createClass({
footer: React.PropTypes.element,
handleChange: React.PropTypes.func,
ignoreFocus: React.PropTypes.bool,
name: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
onClick: React.PropTypes.func,
@ -146,7 +147,12 @@ let Property = React.createClass({
if(typeof this.props.onClick === 'function') {
this.props.onClick();
}
// skip the focus of non-input elements
let nonInputHTMLElements = ['pre', 'div'];
if (this.refs.input &&
nonInputHTMLElements.indexOf(this.refs.input.getDOMNode().nodeName.toLowerCase()) > -1 ) {
return;
}
this.refs.input.getDOMNode().focus();
this.setState({
isFocused: true
@ -210,7 +216,8 @@ let Property = React.createClass({
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled: !this.props.editable,
ref: 'input'
ref: 'input',
name: this.props.name
});
});
},
@ -240,15 +247,14 @@ let Property = React.createClass({
return (
<div
className={'ascribe-settings-wrapper ' + this.getClassName()}
className={'ascribe-property-wrapper ' + this.getClassName()}
onClick={this.handleFocus}
onFocus={this.handleFocus}
style={style}>
<OverlayTrigger
delay={500}
placement="top"
overlay={tooltip}>
<div className={'ascribe-settings-property ' + this.props.className}>
<div className={'ascribe-property ' + this.props.className}>
{this.state.errors}
<span>{this.props.label}</span>
{this.renderChildren(style)}
@ -260,4 +266,4 @@ let Property = React.createClass({
}
});
export default Property;
export default Property;

View File

@ -62,14 +62,14 @@ let PropertyCollapsile = React.createClass({
return (
<div
className={'ascribe-settings-wrapper'}
className={'ascribe-property-wrapper'}
style={style}>
<OverlayTrigger
delay={500}
placement="top"
overlay={tooltip}>
<div
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
onClick={this.handleFocus}
onFocus={this.handleFocus}>
<input
@ -84,7 +84,7 @@ let PropertyCollapsile = React.createClass({
collapsible
expanded={this.state.show}
className="bs-custom-panel">
<div className="ascribe-settings-property">
<div className="ascribe-property">
{this.renderChildren()}
</div>
</Panel>

View File

@ -103,14 +103,17 @@ let Video = React.createClass({
* ReactJS is responsible for DOM manipulation but VideoJS updates the DOM
* to install itself to display the video, therefore ReactJS complains that we are
* changing the DOM under its feet.
* The component supports a fall-back to HTML5 video tag.
*
* What we do is the following:
* 1) set `state.ready = false`
* 2) render the cover using the `<Image />` component (because ready is false)
* 1) set `state.libraryLoaded = null` (state.libraryLoaded can be in three states: `null`
* if we don't know anything about it, `true` if the external library has been loaded,
* `false` if we failed to load the external library)
* 2) render the cover using the `<Image />` component (because libraryLoaded is null)
* 3) on `componentDidMount`, we load the external `css` and `js` resources using
* the `InjectInHeadMixin`, attaching a function to `Promise.then` to change
* `state.ready` to true
* 4) when the promise is succesfully resolved, we change `state.ready` triggering
* `state.libraryLoaded` to true
* 4) when the promise is succesfully resolved, we change `state.libraryLoaded` triggering
* a re-render
* 5) the new render calls `prepareVideoHTML` to get the raw HTML of the video tag
* (that will be later processed and expanded by VideoJS)
@ -129,18 +132,19 @@ let Video = React.createClass({
mixins: [InjectInHeadMixin],
getInitialState() {
return { ready: false, videoMounted: false };
return { libraryLoaded: null, videoMounted: false };
},
componentDidMount() {
Q.all([
this.inject('//vjs.zencdn.net/4.12/video-js.css'),
this.inject('//vjs.zencdn.net/4.12/video.js')
]).then(this.ready);
this.inject('//vjs.zencdn.net/4.12/video.js')])
.then(() => this.setState({libraryLoaded: true}))
.fail(() => this.setState({libraryLoaded: false}));
},
componentDidUpdate() {
if (this.state.ready && !this.state.videoMounted) {
if (this.state.libraryLoaded && !this.state.videoMounted) {
window.videojs('#mainvideo');
/* eslint-disable */
this.setState({videoMounted: true});
@ -149,11 +153,9 @@ let Video = React.createClass({
},
componentWillUnmount() {
window.videojs('#mainvideo').dispose();
},
ready() {
this.setState({ready: true, videoMounted: false});
if (this.state.videoMounted) {
window.videojs('#mainvideo').dispose();
}
},
prepareVideoHTML() {
@ -171,7 +173,7 @@ let Video = React.createClass({
},
render() {
if (this.state.ready) {
if (this.state.libraryLoaded !== null) {
return (
<div dangerouslySetInnerHTML={{__html: this.prepareVideoHTML() }}/>
);

View File

@ -1,11 +1,11 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { getLangText } from '../../utils/lang_utils';
let Link = Router.Link;
let PaginationButton = React.createClass({
propTypes: {
@ -29,21 +29,21 @@ let PaginationButton = React.createClass({
page -= 1;
directionDisplay = (
<span>
<span aria-hidden="true">&larr;</span> {getLangText('Previous')}
<span aria-hidden="true"><Glyphicon glyph='chevron-left'/></span> {getLangText('Previous')}
</span>
);
} else {
page += 1;
directionDisplay = (
<span>
{getLangText('Next')} <span aria-hidden="true">&rarr;</span>
{getLangText('Next')} <span aria-hidden="true"><Glyphicon glyph='chevron-right'/></span>
</span>
);
}
if (this.isInRange(page)) {
anchor = (
<Link to="pieces"
<Link to="/collection"
query={{page}}
onClick={this.props.goToPage(page)}>
{directionDisplay}

View File

@ -33,21 +33,28 @@ let PieceListBulkModal = React.createClass({
);
},
onChange(state) {
this.setState(state);
},
componentDidMount() {
EditionListStore.listen(this.onChange);
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search,
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
},
componentWillUnmount() {
EditionListStore.unlisten(this.onChange);
PieceListStore.unlisten(this.onChange);
UserStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
fetchSelectedPieceEditionList() {
let filteredPieceIdList = Object.keys(this.state.editionList)
.filter((pieceId) => {

View File

@ -4,16 +4,16 @@ import React from 'react';
import PieceListToolbarFilterWidget from './piece_list_toolbar_filter_widget';
import PieceListToolbarOrderWidget from './piece_list_toolbar_order_widget';
import SearchBar from '../search_bar';
import AppConstants from '../../constants/application_constants';
import Input from 'react-bootstrap/lib/Input';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { getLangText } from '../../utils/lang_utils';
let PieceListToolbar = React.createClass({
propTypes: {
className: React.PropTypes.string,
searchFor: React.PropTypes.func,
searchQuery: React.PropTypes.string,
filterParams: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string,
@ -39,11 +39,6 @@ let PieceListToolbar = React.createClass({
])
},
searchFor() {
let searchTerm = this.refs.search.getInputDOMNode().value;
this.props.searchFor(searchTerm);
},
getFilterWidget(){
if (this.props.filterParams){
return (
@ -55,6 +50,7 @@ let PieceListToolbar = React.createClass({
}
return null;
},
getOrderWidget(){
if (this.props.orderParams){
return (
@ -68,28 +64,25 @@ let PieceListToolbar = React.createClass({
},
render() {
let searchIcon = <Glyphicon glyph='search' className="filter-glyph"/>;
const { className, children, searchFor, searchQuery } = this.props;
return (
<div className={this.props.className}>
<div className={className}>
<div className="row">
<div className="col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<div className="row">
<span className="pull-left">
{this.props.children}
</span>
<span className="pull-right search-bar ascribe-input-glyph">
<Input
type='text'
ref="search"
placeholder={getLangText('Search%s', '...')}
onChange={this.searchFor}
addonAfter={searchIcon} />
{children}
</span>
<span className="pull-right">
{this.getOrderWidget()}
{this.getFilterWidget()}
</span>
<SearchBar
className="pull-right search-bar ascribe-input-glyph"
searchFor={searchFor}
searchQuery={searchQuery}
threshold={AppConstants.searchThreshold}/>
</div>
</div>
</div>

View File

@ -76,7 +76,7 @@ let PieceListToolbarFilterWidgetFilter = React.createClass({
render() {
let filterIcon = (
<span>
<span className="glyphicon glyphicon-filter" aria-hidden="true"></span>
<span className="ascribe-icon icon-ascribe-filter" aria-hidden="true"></span>
<span style={this.isFilterActive()}>*</span>
</span>
);

View File

@ -47,8 +47,8 @@ let PieceListToolbarOrderWidget = React.createClass({
render() {
let filterIcon = (
<span>
<span className="glyphicon glyphicon-sort-by-alphabet" aria-hidden="true"></span>
<span style={this.isOrderActive()}>*</span>
<span className="ascribe-icon icon-ascribe-sort" aria-hidden="true"></span>
<span style={this.isOrderActive()}>&middot;</span>
</span>
);
return (

View File

@ -0,0 +1,107 @@
'use strict';
import React from 'react';
import { History } from 'react-router';
import UserStore from '../../../stores/user_store';
import UserActions from '../../../actions/user_actions';
import AppConstants from '../../../constants/application_constants';
const { object } = React.PropTypes;
const WHEN_ENUM = ['loggedIn', 'loggedOut'];
/**
* Can be used in combination with `Route` as an intermediate Handler
* between the actual component we want to display dependent on a certain state
* that is required to display that component.
*
* @param {string} options.to Any type of route path that is defined in routes.js
* @param {enum/string} options.when ('loggedIn' || 'loggedOut')
*/
export default function AuthProxyHandler({to, when}) {
// validate `when`, must be contained in `WHEN_ENUM`.
// Throw an error otherwise.
if(WHEN_ENUM.indexOf(when) === -1) {
let whenValues = WHEN_ENUM.join(', ');
throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`);
}
return (Component) => {
return React.createClass({
propTypes: {
location: object
},
mixins: [History],
getInitialState() {
return UserStore.getState();
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
},
componentDidUpdate() {
this.redirectConditionally();
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
},
redirectConditionally() {
const { query } = this.props.location;
const { redirectAuthenticated, redirect } = query;
// The user of this handler specifies with `when`, what kind of status
// needs to be checked to conditionally do - if that state is `true` -
// a redirect.
//
// So if when === 'loggedIn', we're checking if the user is logged in (and
// vice versa)
let exprToValidate = when === 'loggedIn' ?
this.state.currentUser && this.state.currentUser.email :
this.state.currentUser && !this.state.currentUser.email;
// and redirect if `true`.
if(exprToValidate) {
window.setTimeout(() => this.history.replaceState(null, to, query));
// Otherwise there can also be the case that the backend
// wants to redirect the user to a specific route when the user is logged out already
} else if(!exprToValidate && when === 'loggedIn' && redirect) {
delete query.redirect;
window.setTimeout(() => this.history.replaceState(null, '/' + redirect, query));
} else if(!exprToValidate && when === 'loggedOut' && redirectAuthenticated) {
/*
* redirectAuthenticated contains an arbirary path
* eg pieces/<id>, editions/<bitcoin_id>, collection, settings, ...
* hence transitionTo cannot be used directly.
*
* While we're getting rid of `query.redirect` explicitly in the
* above `else if` statement, here it's sufficient to just call
* `baseUrl` + `redirectAuthenticated`, as it gets rid of queries as well.
*/
window.location = AppConstants.baseUrl + redirectAuthenticated;
}
},
onChange(state) {
this.setState(state);
},
render() {
return (
<Component {...this.props}/>
);
}
});
};
}

View File

@ -15,7 +15,7 @@ import AclProxy from '../acl_proxy';
import CopyrightAssociationForm from '../ascribe_forms/form_copyright_association';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -37,7 +37,7 @@ let AccountSettings = React.createClass({
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
let profile = null;
if (this.props.currentUser.username) {
@ -78,7 +78,7 @@ let AccountSettings = React.createClass({
getFormData={this.getFormDataProfile}>
<Property
name="hash_locally"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
defaultChecked={this.props.currentUser.profile.hash_locally}>
@ -96,11 +96,15 @@ let AccountSettings = React.createClass({
title={getLangText('Account')}
defaultExpanded={true}>
{content}
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
<AclProxy
aclObject={this.props.whitelabel}
aclName="acl_view_settings_copyright_association">
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
</AclProxy>
{profile}
</CollapsibleParagraph>
);
}
});
export default AccountSettings;
export default AccountSettings;

View File

@ -15,7 +15,7 @@ import ActionPanel from '../ascribe_panel/action_panel';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -57,7 +57,7 @@ let APISettings = React.createClass({
},
getApplications(){
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app, i) {

View File

@ -10,7 +10,7 @@ import Property from '../ascribe_forms/property';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -38,7 +38,7 @@ let BitcoinWalletSettings = React.createClass({
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.walletSettings.btc_public_key) {
content = (

View File

@ -23,10 +23,15 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils';
let ContractSettings = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState(){
return mergeOptions(
ContractListStore.getState(),
@ -82,6 +87,8 @@ let ContractSettings = React.createClass({
let privateContracts = this.getPrivateContracts();
let createPublicContractForm = null;
setDocumentTitle(getLangText('Contracts settings'));
if(publicContracts.length === 0) {
createPublicContractForm = (
<CreateContractForm
@ -89,7 +96,8 @@ let ContractSettings = React.createClass({
fileClassToUpload={{
singular: 'new contract',
plural: 'new contracts'
}}/>
}}
location={this.props.location}/>
);
}
@ -114,7 +122,9 @@ let ContractSettings = React.createClass({
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_public_contract">
<ContractSettingsUpdateButton contract={contract}/>
<ContractSettingsUpdateButton
contract={contract}
location={this.props.location}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
@ -144,7 +154,8 @@ let ContractSettings = React.createClass({
fileClassToUpload={{
singular: getLangText('new contract'),
plural: getLangText('new contracts')
}}/>
}}
location={this.props.location}/>
{privateContracts.map((contract, i) => {
return (
<ActionPanel
@ -156,7 +167,9 @@ let ContractSettings = React.createClass({
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_private_contract">
<ContractSettingsUpdateButton contract={contract}/>
<ContractSettingsUpdateButton
contract={contract}
location={this.props.location}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
@ -183,4 +196,4 @@ let ContractSettings = React.createClass({
}
});
export default ContractSettings;
export default ContractSettings;

View File

@ -20,7 +20,8 @@ import { getLangText } from '../../utils/lang_utils';
let ContractSettingsUpdateButton = React.createClass({
propTypes: {
contract: React.PropTypes.object
contract: React.PropTypes.object,
location: React.PropTypes.object
},
submitFile(file) {
@ -90,7 +91,7 @@ let ContractSettingsUpdateButton = React.createClass({
}}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile}
/>
location={this.props.location}/>
);
}
});

View File

@ -1,7 +1,6 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
@ -16,6 +15,8 @@ import APISettings from './api_settings';
import AclProxy from '../acl_proxy';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
let SettingsContainer = React.createClass({
@ -25,8 +26,6 @@ let SettingsContainer = React.createClass({
React.PropTypes.element])
},
mixins: [Router.Navigation],
getInitialState() {
return mergeOptions(
UserStore.getState(),
@ -56,6 +55,8 @@ let SettingsContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Account settings'));
if (this.state.currentUser && this.state.currentUser.username) {
return (
<div className="settings-container">

View File

@ -1,96 +1,40 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import ReactAddons from 'react/addons';
import React from 'react/addons';
import { History } from 'react-router';
import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs';
let State = Router.State;
let Navigation = Router.Navigation;
const { arrayOf, element, bool, shape, string, object } = React.PropTypes;
let SlidesContainer = React.createClass({
const SlidesContainer = React.createClass({
propTypes: {
children: React.PropTypes.arrayOf(React.PropTypes.element),
forwardProcess: React.PropTypes.bool.isRequired,
children: arrayOf(element),
forwardProcess: bool.isRequired,
glyphiconClassNames: React.PropTypes.shape({
pending: React.PropTypes.string,
complete: React.PropTypes.string
})
glyphiconClassNames: shape({
pending: string,
complete: string
}),
location: object
},
mixins: [State, Navigation],
mixins: [History],
getInitialState() {
// handle queryParameters
let queryParams = this.getQuery();
let slideNum = -1;
let startFrom = -1;
// We can actually need to check if slide_num is present as a key in queryParams.
// We do not really care about its value though...
if(queryParams && 'slide_num' in queryParams) {
slideNum = parseInt(queryParams.slide_num, 10);
}
// if slide_num is not set, this will be done in componentDidMount
// the query param 'start_from' removes all slide children before the respective number
// Also, we use the 'in' keyword for the same reason as above in 'slide_num'
if(queryParams && 'start_from' in queryParams) {
startFrom = parseInt(queryParams.start_from, 10);
}
return {
slideNum,
startFrom,
containerWidth: 0,
historyLength: window.history.length
containerWidth: 0
};
},
componentDidMount() {
// check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery();
// We use 'in' to check if the key is present in the user's browser url bar,
// we do not really care about its value at this point
if(!('slide_num' in queryParams)) {
// we're first requiring all the other possible queryParams and then set
// the specific one we need instead of overwriting them
queryParams.slide_num = 0;
this.replaceWith(this.getPathname(), null, queryParams);
}
// init container width
this.handleContainerResize();
// we're using an event listener on window here,
// as it was not possible to listen to the resize events of a dom node
window.addEventListener('resize', this.handleContainerResize);
},
componentWillReceiveProps() {
let queryParams = this.getQuery();
// also check if start_from was updated
// This applies for example when the user tries to submit a already existing piece
// (starting from slide 1 for example) and then clicking on + NEW WORK
if(queryParams && !('start_from' in queryParams)) {
this.setState({
startFrom: -1
});
}
},
componentDidUpdate() {
let queryParams = this.getQuery();
// check if slide_num was defined, and if not then default to 0
this.setSlideNum(queryParams.slide_num);
// Initially, we need to dispatch 'resize' once to render correctly
window.dispatchEvent(new Event('resize'));
},
componentWillUnmount() {
@ -105,80 +49,26 @@ let SlidesContainer = React.createClass({
},
// When the start_from parameter is used, this.setSlideNum can not simply be used anymore.
nextSlide() {
let nextSlide = this.state.slideNum + 1;
this.setSlideNum(nextSlide);
nextSlide(additionalQueryParams) {
const slideNum = parseInt(this.props.location.query.slide_num, 10) || 0;
let nextSlide = slideNum + 1;
this.setSlideNum(nextSlide, additionalQueryParams);
},
// We let every one from the outsite set the page number of the slider,
// though only if the slideNum is actually in the range of our children-list.
setSlideNum(slideNum) {
// we do not want to overwrite other queryParams
let queryParams = this.getQuery();
// slideNum can in some instances be not a number,
// therefore we have to parse it to one and make sure that its not NaN
slideNum = parseInt(slideNum, 10);
// if slideNum is not a number (even after we parsed it to one) and there has
// never been a transition to another slide (this.state.slideNum ==== -1 indicates that)
// then we want to "replace" (in this case append) the current url with ?slide_num=0
if(isNaN(slideNum) && this.state.slideNum === -1) {
slideNum = 0;
queryParams.slide_num = slideNum;
this.replaceWith(this.getPathname(), null, queryParams);
this.setState({slideNum: slideNum});
return;
// slideNum always represents the future state. So if slideNum and
// this.state.slideNum are equal, there is no sense in redirecting
} else if(slideNum === this.state.slideNum) {
return;
// if slideNum is within the range of slides and none of the previous cases
// where matched, we can actually do transitions
} else if(slideNum >= 0 || slideNum < this.customChildrenCount()) {
if(slideNum !== this.state.slideNum - 1) {
// Bootstrapping the component, getInitialState is called once to save
// the tabs history length.
// In order to know if we already pushed a new state on the history stack or not,
// we're comparing the old history length with the new one and if it didn't change then
// we push a new state on it ONCE (ever).
// Otherwise, we're able to use the browsers history.forward() method
// to keep the stack clean
if(this.props.forwardProcess) {
queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams);
} else {
if(this.state.historyLength === window.history.length) {
queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams);
} else {
window.history.forward();
}
}
}
this.setState({
slideNum: slideNum
});
} else {
throw new Error('You\'re calling a page number that is out of range.');
}
setSlideNum(nextSlideNum, additionalQueryParams = {}) {
let queryParams = Object.assign(this.props.location.query, additionalQueryParams);
queryParams.slide_num = nextSlideNum;
this.history.pushState(null, this.props.location.pathname, queryParams);
},
// breadcrumbs are defined as attributes of the slides.
// To extract them we have to read the DOM element's attributes
extractBreadcrumbs() {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
let breadcrumbs = [];
ReactAddons.Children.map(this.props.children, (child, i) => {
if(child && i >= this.state.startFrom && child.props['data-slide-title']) {
React.Children.map(this.props.children, (child, i) => {
if(child && i >= startFrom && child.props['data-slide-title']) {
breadcrumbs.push(child.props['data-slide-title']);
}
});
@ -191,9 +81,11 @@ let SlidesContainer = React.createClass({
// Therefore React.Children.count does not work anymore and we
// need to implement our own method.
customChildrenCount() {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
let count = 0;
React.Children.forEach(this.props.children, (child, i) => {
if(i >= this.state.startFrom) {
if(i >= startFrom) {
count++;
}
});
@ -212,7 +104,7 @@ let SlidesContainer = React.createClass({
return (
<SlidesContainerBreadcrumbs
breadcrumbs={breadcrumbs}
slideNum={this.state.slideNum}
slideNum={parseInt(this.props.location.query.slide_num, 10) || 0}
numOfSlides={breadcrumbs.length}
containerWidth={this.state.containerWidth}
glyphiconClassNames={this.props.glyphiconClassNames}/>
@ -225,12 +117,13 @@ let SlidesContainer = React.createClass({
// Since we need to give the slides a width, we need to call ReactAddons.addons.cloneWithProps
// Also, a key is nice to have!
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child, i) => {
const startFrom = parseInt(this.props.location.query.start_from, 10) || -1;
return React.Children.map(this.props.children, (child, i) => {
// since the default parameter of startFrom is -1, we do not need to check
// if its actually present in the url bar, as it will just not match
if(child && i >= this.state.startFrom) {
return ReactAddons.addons.cloneWithProps(child, {
if(child && i >= startFrom) {
return React.addons.cloneWithProps(child, {
className: 'ascribe-slide',
style: {
width: this.state.containerWidth
@ -246,7 +139,7 @@ let SlidesContainer = React.createClass({
},
render() {
let spacing = this.state.containerWidth * this.state.slideNum;
let spacing = this.state.containerWidth * parseInt(this.props.location.query.slide_num, 10) || 0;
let translateXValue = 'translateX(' + (-1) * spacing + 'px)';
/*
@ -280,4 +173,4 @@ let SlidesContainer = React.createClass({
}
});
export default SlidesContainer;
export default SlidesContainer;

View File

@ -0,0 +1,34 @@
'use strict';
import React from 'react';
import classNames from 'classnames';
let AscribeSpinner = React.createClass({
propTypes: {
classNames: React.PropTypes.string,
size: React.PropTypes.oneOf(['sm', 'md', 'lg']),
color: React.PropTypes.oneOf(['blue', 'dark-blue', 'light-blue', 'pink', 'black', 'loop'])
},
getDefaultProps() {
return {
inline: false,
size: 'md',
color: 'loop'
};
},
render() {
return (
<div className={
classNames('spinner-wrapper-' + this.props.size,
'spinner-wrapper-' + this.props.color,
this.props.classNames)}>
<div className={classNames('spinner-circle')}></div>
<div className={classNames('spinner-inner')}>A</div>
</div>
);
}
});
export default AscribeSpinner;

View File

@ -35,14 +35,7 @@ export class TransitionModel {
this.callback = callback;
}
toReactRouterLinkProps(queryValue) {
let props = {
to: this.to,
params: {}
};
props.params[this.queryKey] = queryValue;
return props;
toReactRouterLink(queryValue) {
return '/' + this.to + '/' + queryValue;
}
}

View File

@ -1,11 +1,10 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import { ColumnModel } from './models/table_models';
let Link = Router.Link;
let TableItemWrapper = React.createClass({
propTypes: {
@ -15,8 +14,6 @@ let TableItemWrapper = React.createClass({
onClick: React.PropTypes.func
},
mixins: [Router.Navigation],
render() {
return (
<tr onClick={this.props.onClick}>
@ -35,18 +32,13 @@ let TableItemWrapper = React.createClass({
);
} else {
let linkProps = column.transition.toReactRouterLinkProps(this.props.columnContent[column.transition.valueKey]);
/**
* If a transition is defined in columnContent, then we can use
* Router.Navigation.transitionTo to redirect the user
* programmatically
*/
let linkString = column.transition.toReactRouterLink(this.props.columnContent[column.transition.valueKey]);
return (
<td key={i} className={column.className}>
<Link
to={linkString}
className={'ascribe-table-item-column'}
onClick={column.transition.callback}
{...linkProps}>
onClick={column.transition.callback}>
<TypeElement {...typeElementProps} />
</Link>
</td>

View File

@ -12,6 +12,7 @@ import { getLangText } from '../../../utils/lang_utils';
// Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({
propTypes: {
className: React.PropTypes.string,
onDrop: React.PropTypes.func.isRequired,
onDragOver: React.PropTypes.func,
onInactive: React.PropTypes.func,
@ -40,7 +41,8 @@ let FileDragAndDrop = React.createClass({
plural: React.PropTypes.string
}),
allowedExtensions: React.PropTypes.string
allowedExtensions: React.PropTypes.string,
location: React.PropTypes.object
},
handleDragOver(event) {
@ -107,6 +109,7 @@ let FileDragAndDrop = React.createClass({
},
handleOnClick() {
let evt;
// when multiple is set to false and the user already uploaded a piece,
// do not propagate event
if(this.props.dropzoneInactive) {
@ -118,16 +121,18 @@ let FileDragAndDrop = React.createClass({
return;
}
// Firefox only recognizes the simulated mouse click if bubbles is set to true,
// but since Google Chrome propagates the event much further than needed, we
// need to stop propagation as soon as the event is created
var evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
try {
evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
} catch(e) {
// For browsers that do not support the new MouseEvent syntax
evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
}
evt.stopPropagation();
this.refs.fileinput.getDOMNode().dispatchEvent(evt);
},
@ -142,7 +147,8 @@ let FileDragAndDrop = React.createClass({
fileClassToUpload,
areAssetsDownloadable,
areAssetsEditable,
allowedExtensions
allowedExtensions,
location
} = this.props;
// has files only is true if there are files that do not have the status deleted or canceled
@ -158,10 +164,10 @@ let FileDragAndDrop = React.createClass({
<div className="file-drag-and-drop-hashing-dialog">
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
<p>
<a onClick={this.props.handleCancelHashing}> {getLangText('Cancel hashing')}</a>
<a onClick={handleCancelHashing}> {getLangText('Cancel hashing')}</a>
</p>
<ProgressBar
now={Math.ceil(this.props.hashingProgress)}
now={Math.ceil(hashingProgress)}
label="%(percent)s%"
className="ascribe-progress-bar"/>
</div>
@ -179,7 +185,8 @@ let FileDragAndDrop = React.createClass({
hasFiles={hasFiles}
onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload}/>
fileClassToUpload={fileClassToUpload}
location={location}/>
<FileDragAndDropPreviewIterator
files={filesToUpload}
handleDeleteFile={this.handleDeleteFile}
@ -188,12 +195,23 @@ let FileDragAndDrop = React.createClass({
handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={areAssetsEditable}/>
{/*
Opera doesn't trigger simulated click events
if the targeted input has `display:none` set.
Which means we need to set its visibility to hidden
instead of using `display:none`.
See:
- http://stackoverflow.com/questions/12880604/jquery-triggerclick-not-working-on-opera-if-the-element-is-not-displayed
*/}
<input
multiple={multiple}
ref="fileinput"
type="file"
style={{
display: 'none',
visibility: 'hidden',
position: 'absolute',
top: 0,
height: 0,
width: 0
}}

View File

@ -1,11 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import { getLangText } from '../../../utils/lang_utils';
import { dragAndDropAvailable } from '../../../utils/feature_detection_utils';
let Link = Router.Link;
let FileDragAndDropDialog = React.createClass({
propTypes: {
@ -19,13 +20,24 @@ let FileDragAndDropDialog = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
})
}),
location: React.PropTypes.object
},
mixins: [Router.State],
getDragDialog(fileClass) {
if(dragAndDropAvailable) {
return [
<p>{getLangText('Drag %s here', fileClass)}</p>,
<p>{getLangText('or')}</p>
];
} else {
return null;
}
},
render() {
const queryParams = this.getQuery();
const queryParams = this.props.location.query;
if(this.props.hasFiles) {
return null;
@ -38,11 +50,13 @@ let FileDragAndDropDialog = React.createClass({
let queryParamsUpload = Object.assign({}, queryParams);
queryParamsUpload.method = 'upload';
let { location } = this.props;
return (
<div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p>
<Link
to={this.getPath()}
to={location.pathname}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
@ -52,7 +66,7 @@ let FileDragAndDropDialog = React.createClass({
<span> or </span>
<Link
to={this.getPath()}
to={location.pathname}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
@ -64,8 +78,7 @@ let FileDragAndDropDialog = React.createClass({
if(this.props.multipleFiles) {
return (
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag %s here', this.props.fileClassToUpload.plural)}</p>
<p>{getLangText('or')}</p>
{this.getDragDialog(this.props.fileClassToUpload.plural)}
<span
className="btn btn-default"
onClick={this.props.onClick}>
@ -78,8 +91,7 @@ let FileDragAndDropDialog = React.createClass({
return (
<span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a %s here', this.props.fileClassToUpload.singular)}</p>
<p>{getLangText('or')}</p>
{this.getDragDialog(this.props.fileClassToUpload.singular)}
<span
className="btn btn-default"
onClick={this.props.onClick}>

View File

@ -3,7 +3,7 @@
import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../../constants/application_constants';
import AscribeSpinner from '../../ascribe_spinner';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewImage = React.createClass({
@ -53,7 +53,11 @@ let FileDragAndDropPreviewImage = React.createClass({
}
} else {
actionSymbol = <img height={35} className="action-file" src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
actionSymbol = (
<div className="spinner-file">
<AscribeSpinner color='dark-blue' size='md' />
</div>
);
}
return (

View File

@ -3,7 +3,7 @@
import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../../constants/application_constants';
import AscribeSpinner from '../../ascribe_spinner';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewOther = React.createClass({
@ -49,7 +49,11 @@ let FileDragAndDropPreviewOther = React.createClass({
}
} else {
actionSymbol = <img height={35} src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
actionSymbol = (
<div className="spinner-file">
<AscribeSpinner color='dark-blue' size='md' />
</div>
);
}
return (

View File

@ -2,7 +2,6 @@
import React from 'react/addons';
import fineUploader from 'fineUploader';
import Router from 'react-router';
import Q from 'q';
import S3Fetcher from '../../fetchers/s3_fetcher';
@ -127,10 +126,10 @@ let ReactS3FineUploader = React.createClass({
fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.element
])
},
]),
mixins: [Router.State],
location: React.PropTypes.object
},
getDefaultProps() {
return {
@ -327,7 +326,7 @@ let ReactS3FineUploader = React.createClass({
.then((res) => {
return res.json();
})
.then((res) =>{
.then((res) => {
if(res.otherdata) {
file.s3Url = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe;
@ -649,7 +648,7 @@ let ReactS3FineUploader = React.createClass({
//
// In the view this only happens when the user is allowed to do local hashing as well
// as when the correct query parameter is present in the url ('hash' and not 'upload')
let queryParams = this.getQuery();
let queryParams = this.props.location.query;
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') {
let convertedFilePromises = [];
@ -830,7 +829,7 @@ let ReactS3FineUploader = React.createClass({
isDropzoneInactive() {
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
let queryParams = this.getQuery();
let queryParams = this.props.location.query;
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
return true;
@ -859,12 +858,20 @@ let ReactS3FineUploader = React.createClass({
enableLocalHashing,
fileClassToUpload,
validation,
fileInputElement
fileInputElement,
location
} = this.props;
// Here we initialize the template that has been either provided from the outside
// or the default input that is FileDragAndDrop.
return React.createElement(fileInputElement, {
multiple,
areAssetsDownloadable,
areAssetsEditable,
onInactive,
enableLocalHashing,
fileClassToUpload,
location,
onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile,
@ -872,14 +879,8 @@ let ReactS3FineUploader = React.createClass({
handlePauseFile: this.handlePauseFile,
handleResumeFile: this.handleResumeFile,
handleCancelHashing: this.handleCancelHashing,
multiple: multiple,
areAssetsDownloadable: areAssetsDownloadable,
areAssetsEditable: areAssetsEditable,
onInactive: onInactive,
dropzoneInactive: this.isDropzoneInactive(),
hashingProgress: this.state.hashingProgress,
enableLocalHashing: enableLocalHashing,
fileClassToUpload: fileClassToUpload,
allowedExtensions: this.getAllowedExtensions()
});
}

View File

@ -1,7 +1,6 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
@ -10,14 +9,17 @@ import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable';
import AscribeSpinner from './ascribe_spinner';
import ApiUrls from '../constants/api_urls';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let CoaVerifyContainer = React.createClass({
mixins: [Router.Navigation],
render() {
setDocumentTitle(getLangText('Verify your Certificate of Authenticity'));
return (
<div className="ascribe-login-wrapper">
<br/>
@ -45,8 +47,6 @@ let CoaVerifyContainer = React.createClass({
let CoaVerifyForm = React.createClass({
mixins: [Router.Navigation],
handleSuccess(response){
let notification = null;
if (response.verdict) {
@ -64,12 +64,12 @@ let CoaVerifyForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Verify your Certificate of Authenticity')}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<Property

View File

@ -0,0 +1,25 @@
'use strict';
import React from 'react';
import { getLangText } from '../utils/lang_utils';
let ErrorNotFoundPage = React.createClass({
render() {
return (
<div className="row">
<div className="col-md-12">
<div className="error-wrapper">
<h1>404</h1>
<p>
{getLangText('Ups, the page you are looking for does not exist.')}
</p>
</div>
</div>
</div>
);
}
});
export default ErrorNotFoundPage;

View File

@ -8,10 +8,9 @@ let Footer = React.createClass({
render() {
return (
<div className="ascribe-footer">
<hr />
<p className="ascribe-sub-sub-statement">
<br />
<a href="https://github.com/ascribe/REST-main/" target="_blank">api</a> |
<a href="http://docs.ascribe.apiary.io/" target="_blank">api</a> |
<a href="https://www.ascribe.io/impressum/" target="_blank"> impressum</a> |
<a href="https://www.ascribe.io/terms/" target="_blank"> {getLangText('terms of service')}</a> |
<a href="https://www.ascribe.io/privacy/" target="_blank"> {getLangText('privacy')}</a>

View File

@ -1,15 +1,15 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import Nav from 'react-bootstrap/lib/Nav';
import Navbar from 'react-bootstrap/lib/Navbar';
import CollapsibleNav from 'react-bootstrap/lib/CollapsibleNav';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import NavItem from 'react-bootstrap/lib/NavItem';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
import AclProxy from './acl_proxy';
@ -31,14 +31,13 @@ import { getLangText } from '../utils/lang_utils';
import {constructHead} from '../utils/head_setter';
let Header = React.createClass({
propTypes: {
showAddWork: React.PropTypes.bool,
routes: React.PropTypes.element
routes: React.PropTypes.arrayOf(React.PropTypes.object)
},
mixins: [Router.State],
getDefaultProps() {
return {
showAddWork: true
@ -63,24 +62,23 @@ let Header = React.createClass({
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
},
getLogo(){
let whitelabel = this.state.whitelabel;
getLogo() {
let { whitelabel } = this.state;
if (whitelabel.head) {
constructHead(whitelabel.head);
}
else{
setTitle('ascribe');
}
if (whitelabel.subdomain !== 'www'){
if (whitelabel.subdomain && whitelabel.subdomain !== 'www' && whitelabel.logo){
return (<img className="img-brand" src={whitelabel.logo}/>);
}
else {
return (
<span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
</span>);
}
return (
<span>
<span className="icon-ascribe-logo"></span>
</span>
);
},
getPoweredBy(){
@ -89,10 +87,9 @@ let Header = React.createClass({
aclObject={this.state.whitelabel}
aclName="acl_view_powered_by">
<li>
<a className="pull-right" href="https://www.ascribe.io/" target="_blank">
<a className="pull-right ascribe-powered-by" href="https://www.ascribe.io/" target="_blank">
<span id="powered">{getLangText('powered by')} </span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
<span className="icon-ascribe-logo"></span>
</a>
</li>
</AclProxy>
@ -101,6 +98,7 @@ let Header = React.createClass({
onChange(state) {
this.setState(state);
if(this.state.currentUser && this.state.currentUser.email) {
EventActions.profileDidLoad.defer(this.state.currentUser);
}
@ -141,38 +139,61 @@ let Header = React.createClass({
ref='dropdownbutton'
eventKey="1"
title={this.state.currentUser.username}>
<MenuItemLink
eventKey="2"
to="settings"
<LinkContainer
to="/settings"
onClick={this.onMenuItemClick}>
{getLangText('Account Settings')}
</MenuItemLink>
<MenuItem
eventKey="2">
{getLangText('Account Settings')}
</MenuItem>
</LinkContainer>
<AclProxy
aclObject={this.state.currentUser.acl}
aclName="acl_view_settings_contract">
<MenuItemLink
to="contract_settings"
<LinkContainer
to="/contract_settings"
onClick={this.onMenuItemClick}>
{getLangText('Contract Settings')}
</MenuItemLink>
<MenuItem
eventKey="2">
{getLangText('Contract Settings')}
</MenuItem>
</LinkContainer>
</AclProxy>
<MenuItem divider />
<MenuItemLink eventKey="3" to="logout">{getLangText('Log out')}</MenuItemLink>
<LinkContainer
to="/logout">
<MenuItem
eventKey="3">
{getLangText('Log out')}
</MenuItem>
</LinkContainer>
</DropdownButton>
);
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
}
else {
account = <NavItemLink to="login">{getLangText('LOGIN')}</NavItemLink>;
signup = <NavItemLink to="signup">{getLangText('SIGNUP')}</NavItemLink>;
account = (
<LinkContainer
to="/login">
<NavItem>
{getLangText('LOGIN')}
</NavItem>
</LinkContainer>
);
signup = (
<LinkContainer
to="/signup">
<NavItem>
{getLangText('SIGNUP')}
</NavItem>
</LinkContainer>
);
}
return (
<div>
<Navbar
brand={
this.getLogo()
}
brand={this.getLogo()}
toggleNavKey={0}
fixedTop={true}>
<CollapsibleNav eventKey={0}>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import MenuItem from 'react-bootstrap/lib/MenuItem';
@ -14,8 +14,6 @@ import NotificationStore from '../stores/notification_store';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
let HeaderNotifications = React.createClass({
@ -39,7 +37,7 @@ let HeaderNotifications = React.createClass({
this.setState(state);
},
onMenuItemClick(event) {
onMenuItemClick() {
/*
This is a hack to make the dropdown close after clicking on an item
The function just need to be defined
@ -158,23 +156,13 @@ let NotificationListItem = React.createClass({
},
getLinkData() {
let { pieceOrEdition } = this.props;
if (this.isPiece()) {
return {
to: 'piece',
params: {
pieceId: this.props.pieceOrEdition.id
}
};
return `/pieces/${pieceOrEdition.id}`;
} else {
return {
to: 'edition',
params: {
editionId: this.props.pieceOrEdition.bitcoin_id
}
};
return `/editions/${pieceOrEdition.bitcoin_id}`;
}
},
onClick(event){
@ -184,7 +172,7 @@ let NotificationListItem = React.createClass({
getNotificationText(){
let numNotifications = null;
if (this.props.notification.length > 1){
numNotifications = <div>+ {this.props.notification.length - 1} more...</div>;
numNotifications = <div>+ {this.props.notification.length - 1} {getLangText('more...')}</div>;
}
return (
<div className="notification-action">
@ -196,7 +184,7 @@ let NotificationListItem = React.createClass({
render() {
if (this.props.pieceOrEdition) {
return (
<Link {...this.getLinkData()} onClick={this.onClick}>
<Link to={this.getLinkData()} onClick={this.onClick}>
<div className="row notification-wrapper">
<div className="col-xs-4 clear-paddings">
<div className="thumbnail-wrapper">

View File

@ -1,13 +1,12 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import LoginForm from './ascribe_forms/form_login';
import { getLangText } from '../utils/lang_utils';
let Link = Router.Link;
import { setDocumentTitle } from '../utils/dom_utils';
let LoginContainer = React.createClass({
@ -15,7 +14,8 @@ let LoginContainer = React.createClass({
message: React.PropTypes.string,
redirectOnLoggedIn: React.PropTypes.bool,
redirectOnLoginSuccess: React.PropTypes.bool,
onLogin: React.PropTypes.func
onLogin: React.PropTypes.func,
location: React.PropTypes.object
},
getDefaultProps() {
@ -27,16 +27,19 @@ let LoginContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Log in'));
return (
<div className="ascribe-login-wrapper">
<LoginForm
redirectOnLoggedIn={this.props.redirectOnLoggedIn}
redirectOnLoginSuccess={this.props.redirectOnLoginSuccess}
message={this.props.message}
onLogin={this.props.onLogin}/>
onLogin={this.props.onLogin}
location={this.props.location}/>
<div className="ascribe-login-text">
{getLangText('Not an ascribe user')}&#63; <Link to="signup">{getLangText('Sign up')}...</Link><br/>
{getLangText('Forgot my password')}&#63; <Link to="password_reset">{getLangText('Rescue me')}...</Link>
{getLangText('Not an ascribe user')}&#63; <Link to="/signup">{getLangText('Sign up')}...</Link><br/>
{getLangText('Forgot my password')}&#63; <Link to="/password_reset">{getLangText('Rescue me')}...</Link>
</div>
</div>
);

View File

@ -1,35 +1,42 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import AscribeSpinner from './ascribe_spinner';
import UserActions from '../actions/user_actions';
import Alt from '../alt';
import { alt, altWhitelabel, altUser, altThirdParty } from '../alt';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
import AppConstants from '../constants/application_constants';
let baseUrl = AppConstants.baseUrl;
let LogoutContainer = React.createClass({
mixins: [Router.Navigation, Router.State],
mixins: [History],
componentDidMount() {
UserActions.logoutCurrentUser()
.then(() => {
Alt.flush();
// kill intercom (with fire)
window.Intercom('shutdown');
this.replaceWith(baseUrl);
})
.catch((err) => {
console.logGlobal(err);
});
UserActions.logoutCurrentUser();
alt.flush();
altWhitelabel.flush();
altUser.flush();
altThirdParty.flush();
// kill intercom (with fire)
window.Intercom('shutdown');
},
render() {
return null;
}
setDocumentTitle(getLangText('Log out'));
return (
<div className="ascribe-loading-position">
<AscribeSpinner color='dark-blue' size='lg'/>
<h3 className="text-center">
{getLangText('Just a sec, we\'re logging you out...')}
</h3>
</div>
);
}
});

View File

@ -13,7 +13,7 @@ import { sanitizeList } from '../utils/general_utils';
let NavRoutesLinks = React.createClass({
propTypes: {
routes: React.PropTypes.element,
routes: React.PropTypes.arrayOf(React.PropTypes.object),
userAcl: React.PropTypes.object
},
@ -33,15 +33,15 @@ let NavRoutesLinks = React.createClass({
return;
}
let links = node.props.children.map((child, j) => {
let links = node.childRoutes.map((child, j) => {
let childrenFn = null;
let { aclName, headerTitle, name, children } = child.props;
let { aclName, headerTitle, path, childRoutes } = child;
// If the node has children that could be rendered, then we want
// to execute this function again with the child as the root
//
// Otherwise we'll just pass childrenFn as false
if(child.props.children && child.props.children.length > 0) {
if(child.childRoutes && child.childRoutes.length > 0) {
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
}
@ -58,7 +58,7 @@ let NavRoutesLinks = React.createClass({
aclObject={this.props.userAcl}>
<NavRoutesLinksLink
headerTitle={headerTitle}
routeName={name}
routePath={'/' + path}
depth={i}
children={childrenFn}/>
</AclProxy>
@ -68,7 +68,7 @@ let NavRoutesLinks = React.createClass({
<NavRoutesLinksLink
key={j}
headerTitle={headerTitle}
routeName={name}
routePath={'/' + path}
depth={i}
children={childrenFn}/>
);
@ -88,7 +88,7 @@ let NavRoutesLinks = React.createClass({
return (
<Nav {...this.props}>
{this.extractLinksFromRoutes(routes, userAcl, 0)}
{this.extractLinksFromRoutes(routes[0], userAcl, 0)}
</Nav>
);
}

View File

@ -3,13 +3,16 @@
import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItemLink from 'react-router-bootstrap/lib/MenuItemLink';
import NavItemLink from 'react-router-bootstrap/lib/NavItemLink';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import NavItem from 'react-bootstrap/lib/NavItem';
import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
let NavRoutesLinksLink = React.createClass({
propTypes: {
headerTitle: React.PropTypes.string,
routeName: React.PropTypes.string,
routePath: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
@ -20,10 +23,10 @@ let NavRoutesLinksLink = React.createClass({
},
render() {
let { children, headerTitle, depth, routeName } = this.props;
let { children, headerTitle, depth, routePath } = this.props;
// if the route has children, we're returning a DropdownButton that will get filled
// with MenuItemLinks
// with MenuItems
if(children) {
return (
<DropdownButton title={headerTitle}>
@ -33,13 +36,17 @@ let NavRoutesLinksLink = React.createClass({
} else {
if(depth === 1) {
// if the node's child is actually a node of level one (a child of a node), we're
// returning a DropdownButton matching MenuItemLink
// returning a DropdownButton matching MenuItem
return (
<MenuItemLink to={routeName}>{headerTitle}</MenuItemLink>
<LinkContainer to={routePath}>
<MenuItem>{headerTitle}</MenuItem>
</LinkContainer>
);
} else if(depth === 0) {
return (
<NavItemLink to={routeName}>{headerTitle}</NavItemLink>
<LinkContainer to={routePath}>
<NavItem>{headerTitle}</NavItem>
</LinkContainer>
);
} else {
return null;

View File

@ -1,19 +1,23 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import ApiUrls from '../constants/api_urls';
import AscribeSpinner from './ascribe_spinner';
import GlobalNotificationModel from '../models/global_notification_model';
import GlobalNotificationActions from '../actions/global_notification_actions';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let PasswordResetContainer = React.createClass({
mixins: [Router.Navigation],
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return {isRequested: false};
@ -24,12 +28,14 @@ let PasswordResetContainer = React.createClass({
},
render() {
if (this.props.query.email && this.props.query.token) {
let { location } = this.props;
if (location.query.email && location.query.token) {
return (
<div>
<PasswordResetForm
email={this.props.query.email}
token={this.props.query.token}/>
email={location.query.email}
token={location.query.token}/>
</div>
);
}
@ -71,6 +77,8 @@ let PasswordRequestResetForm = React.createClass({
},
render() {
setDocumentTitle(getLangText('Reset your password'));
return (
<Form
ref="form"
@ -80,12 +88,12 @@ let PasswordRequestResetForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Reset your password')}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">
@ -112,7 +120,7 @@ let PasswordResetForm = React.createClass({
token: React.PropTypes.string
},
mixins: [Router.Navigation],
mixins: [History],
getFormData() {
return {
@ -122,7 +130,7 @@ let PasswordResetForm = React.createClass({
},
handleSuccess() {
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
let notification = new GlobalNotificationModel(getLangText('password successfully updated'), 'success', 10000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
@ -138,12 +146,12 @@ let PasswordResetForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Reset your password')}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
<span className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import PieceListStore from '../stores/piece_list_store';
import PieceListActions from '../actions/piece_list_actions';
@ -20,10 +20,13 @@ import PieceListFilterDisplay from './piece_list_filter_display';
import PieceListBulkModal from './ascribe_piece_list_bulk_modal/piece_list_bulk_modal';
import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar';
import AscribeSpinner from './ascribe_spinner';
import AppConstants from '../constants/application_constants';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let PieceList = React.createClass({
@ -33,10 +36,11 @@ let PieceList = React.createClass({
customSubmitButton: React.PropTypes.element,
filterParams: React.PropTypes.array,
orderParams: React.PropTypes.array,
orderBy: React.PropTypes.string
orderBy: React.PropTypes.string,
location: React.PropTypes.object
},
mixins: [Router.Navigation, Router.State],
mixins: [History],
getDefaultProps() {
return {
@ -60,7 +64,7 @@ let PieceList = React.createClass({
},
componentDidMount() {
let page = this.getQuery().page || 1;
let page = this.props.location.query.page || 1;
PieceListStore.listen(this.onChange);
EditionListStore.listen(this.onChange);
@ -75,7 +79,7 @@ let PieceList = React.createClass({
componentDidUpdate() {
if (this.props.redirectTo && this.state.unfilteredPieceListCount === 0) {
// FIXME: hack to redirect out of the dispatch cycle
window.setTimeout(() => this.transitionTo(this.props.redirectTo, this.getQuery()));
window.setTimeout(() => this.history.pushState(null, this.props.redirectTo, this.props.location.query), 0);
}
},
@ -100,10 +104,10 @@ let PieceList = React.createClass({
},
getPagination() {
let currentPage = parseInt(this.getQuery().page, 10) || 1;
let currentPage = parseInt(this.props.location.query.page, 10) || 1;
let totalPages = Math.ceil(this.state.pieceListCount / this.state.pageSize);
if (this.state.pieceListCount > 10) {
if (this.state.pieceListCount > 20) {
return (
<Pagination
currentPage={currentPage}
@ -116,7 +120,7 @@ let PieceList = React.createClass({
searchFor(searchTerm) {
PieceListActions.fetchPieceList(1, this.state.pageSize, searchTerm, this.state.orderBy,
this.state.orderAsc, this.state.filterBy);
this.transitionTo(this.getPathname(), {page: 1});
this.history.pushState(null, this.props.location.pathname, {page: 1});
},
applyFilterBy(filterBy){
@ -140,7 +144,7 @@ let PieceList = React.createClass({
// we have to redirect the user always to page one as it could be that there is no page two
// for filtered pieces
this.transitionTo(this.getPathname(), {page: 1});
this.history.pushState(null, this.props.location.pathname, {page: 1});
},
applyOrderBy(orderBy) {
@ -149,21 +153,29 @@ let PieceList = React.createClass({
},
render() {
let loadingElement = (<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />);
let loadingElement = <AscribeSpinner color='dark-blue' size='lg'/>;
let AccordionListItemType = this.props.accordionListItemType;
setDocumentTitle(getLangText('Collection'));
return (
<div>
<PieceListToolbar
className="ascribe-piece-list-toolbar"
searchFor={this.searchFor}
searchQuery={this.state.search}
filterParams={this.props.filterParams}
orderParams={this.props.orderParams}
filterBy={this.state.filterBy}
orderBy={this.state.orderBy}
applyFilterBy={this.applyFilterBy}
applyOrderBy={this.applyOrderBy}>
{this.props.customSubmitButton}
{this.props.customSubmitButton ?
this.props.customSubmitButton :
<button className="btn btn-default btn-ascribe-add">
<span className="icon-ascribe icon-ascribe-add" />
</button>
}
</PieceListToolbar>
<PieceListBulkModal className="ascribe-piece-list-bulk-modal" />
<PieceListFilterDisplay
@ -177,6 +189,7 @@ let PieceList = React.createClass({
orderBy={this.state.orderBy}
orderAsc={this.state.orderAsc}
search={this.state.search}
searchFor={this.searchFor}
page={this.state.page}
pageSize={this.state.pageSize}
loadingElement={loadingElement}>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
@ -20,12 +20,9 @@ import GlobalNotificationActions from '../actions/global_notification_actions';
import PropertyCollapsible from './ascribe_forms/property_collapsible';
import RegisterPieceForm from './ascribe_forms/form_register_piece';
import LoginContainer from './login_container';
import SlidesContainer from './ascribe_slides_container/slides_container';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let RegisterPiece = React.createClass( {
@ -37,10 +34,11 @@ let RegisterPiece = React.createClass( {
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element,
React.PropTypes.string
])
]),
location: React.PropTypes.object
},
mixins: [Router.Navigation],
mixins: [History],
getDefaultProps() {
return {
@ -60,10 +58,10 @@ let RegisterPiece = React.createClass( {
},
componentDidMount() {
WhitelabelActions.fetchWhitelabel();
PieceListStore.listen(this.onChange);
UserStore.listen(this.onChange);
WhitelabelStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
},
componentWillUnmount() {
@ -98,7 +96,7 @@ let RegisterPiece = React.createClass( {
this.state.filterBy
);
this.transitionTo('piece', {pieceId: response.piece.id});
this.history.pushState(null, `/pieces/${response.piece.id}`);
},
getSpecifyEditions() {
@ -117,53 +115,22 @@ let RegisterPiece = React.createClass( {
}
},
// basically redirects to the second slide (index: 1), when the user is not logged in
onLoggedOut() {
// only transition to the login store, if user is not logged in
// ergo the currentUser object is not properly defined
if(this.state.currentUser && !this.state.currentUser.email) {
this.refs.slidesContainer.setSlideNum(1);
}
},
onLogin() {
// once the currentUser object from UserStore is defined (eventually the user was transitioned
// to the login form via the slider and successfully logged in), we can direct him back to the
// register_piece slide
if(this.state.currentUser && this.state.currentUser.email) {
window.history.back();
}
},
render() {
setDocumentTitle(getLangText('Register a new piece'));
return (
<SlidesContainer
ref="slidesContainer"
forwardProcess={false}>
<div
onClick={this.onLoggedOut}
onFocus={this.onLoggedOut}>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
<RegisterPieceForm
{...this.props}
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleSuccess}
onLoggedOut={this.onLoggedOut}>
{this.props.children}
{this.getSpecifyEditions()}
</RegisterPieceForm>
</Col>
</Row>
</div>
<div>
<LoginContainer
message={getLangText('Please login before ascribing your work%s', '...')}
redirectOnLoggedIn={false}
redirectOnLoginSuccess={false}
onLogin={this.onLogin}/>
</div>
</SlidesContainer>
<Row className="no-margin">
<Col xs={12} sm={10} md={8} smOffset={1} mdOffset={2}>
<RegisterPieceForm
{...this.props}
isFineUploaderActive={this.state.isFineUploaderActive}
handleSuccess={this.handleSuccess}
location={this.props.location}>
{this.props.children}
{this.getSpecifyEditions()}
</RegisterPieceForm>
</Col>
</Row>
);
}
});

143
js/components/search_bar.js Normal file
View File

@ -0,0 +1,143 @@
'use strict';
import React from 'react';
import Input from 'react-bootstrap/lib/Input';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import AscribeSpinner from './ascribe_spinner';
import { getLangText } from '../utils/lang_utils';
const { func, string, number } = React.PropTypes;
const SearchBar = React.createClass({
propTypes: {
// a function that accepts a string as a search query and updates the
// propagated `searchQuery` after successfully retrieving the
// request from the server
searchFor: func.isRequired,
searchQuery: string.isRequired,
className: string,
// the number of milliseconds the component
// should wait before requesting search results from the server
threshold: number.isRequired
},
getInitialState() {
return {
timer: null,
searchQuery: '',
loading: false
};
},
componentWillReceiveProps(nextProps) {
/**
* This enables the `PieceListStore` to override the state
* of that component in case someone is changing the `searchQuery` on
* another component.
*
* Like how it's being done in the 'Clear search' dialog.
*/
if(this.props.searchQuery !== nextProps.searchQuery || !this.state.searchQuery) {
this.setState({ searchQuery: nextProps.searchQuery });
}
},
componentDidUpdate(prevProps) {
const searchQueryProps = this.props.searchQuery;
const searchQueryPrevProps = prevProps.searchQuery;
const searchQueryState = this.state.searchQuery;
const { loading } = this.state;
/**
* 1. Condition: `loading` must be true, which implies that `evaluateTimer`,
* has already been called
*
* AND
*
* (
* 2. Condition: `searchQueryProps` and `searchQueryState` are true and equal
* (which means that the search query has been propagated to the inner
* fetch method of `fetchPieceList`, which in turn means that a fetch
* has completed)
*
* OR
*
* 3. Condition: `searchQueryProps` and `searchQueryState` can be any value (`true` or
* `false`, as long as they're equal). This case only occurs when the user
* has entered a `searchQuery` and deletes the query in one go, reseting
* `searchQueryProps` to empty string ('' === false) again.
* )
*/
const firstCondition = !!loading;
const secondCondition = searchQueryProps && searchQueryState && searchQueryProps === searchQueryState;
const thirdCondition = !searchQueryPrevProps && searchQueryProps === searchQueryState;
if(firstCondition && (secondCondition || thirdCondition)) {
this.setState({ loading: false });
}
},
startTimer(searchQuery) {
const { timer } = this.state;
const { threshold } = this.props;
// The timer waits for the specified threshold time in milliseconds
// and then calls `evaluateTimer`.
// If another letter has been called in the mean time (timespan < `threshold`),
// the present timer gets cleared and a new one is added to `this.state`.
// This means that `evaluateTimer`, will only be called when the threshold has actually
// passed,
clearTimeout(timer); // apparently `clearTimeout` can be called with null, without throwing errors
const newTimer = setTimeout(this.evaluateTimer(searchQuery), threshold);
this.setState({ timer: newTimer });
},
evaluateTimer(searchQuery) {
return () => {
this.setState({ timer: null, loading: true }, () => {
// search for the query
this.props.searchFor(searchQuery);
});
};
},
handleChange({ target: { value }}) {
// On each letter entry we're updating the state of the component
// and start a timer, which we're also pushing to the state
// of the component
this.startTimer(value);
this.setState({ searchQuery: value });
},
render() {
let searchIcon = <span className='ascribe-icon icon-ascribe-search'/>;
const { className } = this.props;
const { loading, searchQuery } = this.state;
if(loading) {
searchIcon = <AscribeSpinner size='sm' color='dark-blue'/>;
}
return (
<span className={className}>
<Input
type='text'
value={searchQuery}
placeholder={getLangText('Search%s', '...')}
onChange={this.handleChange}
addonAfter={searchIcon} />
</span>
);
}
});
export default SearchBar;

View File

@ -1,15 +1,19 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { Link } from 'react-router';
import SignupForm from './ascribe_forms/form_signup';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let Link = Router.Link;
let SignupContainer = React.createClass({
propTypes: {
location: React.PropTypes.object
},
getInitialState() {
return {
submitted: false,
@ -25,6 +29,8 @@ let SignupContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Sign up'));
if (this.state.submitted){
return (
<div className="ascribe-login-wrapper">
@ -37,9 +43,11 @@ let SignupContainer = React.createClass({
}
return (
<div className="ascribe-login-wrapper">
<SignupForm handleSuccess={this.handleSuccess} />
<SignupForm
handleSuccess={this.handleSuccess}
location={this.props.location}/>
<div className="ascribe-login-text">
{getLangText('Already an ascribe user')}&#63; <Link to="login">{getLangText('Log in')}...</Link><br/>
{getLangText('Already an ascribe user')}&#63; <Link to="/login">{getLangText('Log in')}...</Link><br/>
</div>
</div>

View File

@ -1,6 +1,6 @@
'use strict';
import alt from '../../../../alt';
import { alt } from '../../../../alt';
import Q from 'q';
import PrizeFetcher from '../fetchers/prize_fetcher';

Some files were not shown because too many files have changed in this diff Show More