Use Immutable, enhance i18n, pagination in the store
* Keep track of pagination in the store * Use Immutable in reducers * Finish i18n, every available strings are now translated in English and French * Add a loading indicator * Premises of API error handling * Better locale negotiation
This commit is contained in:
parent
5a9f540cc0
commit
40f6223bd0
25
TODO
25
TODO
@ -1,4 +1,4 @@
|
||||
4. Refactor API + Handle failures
|
||||
4. Refactor API
|
||||
5. Web player
|
||||
6. Homepage
|
||||
7. Settings
|
||||
@ -6,25 +6,4 @@
|
||||
9. Discover
|
||||
|
||||
# API middleware
|
||||
* https://github.com/reactjs/redux/issues/1824#issuecomment-228609501
|
||||
* https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f#.eezt3dano
|
||||
* https://github.com/reactjs/redux/issues/644
|
||||
* https://github.com/peterpme/redux-crud-api-middleware/blob/master/README.md
|
||||
* https://github.com/madou/armory-front/tree/master/src/app/reducers
|
||||
* https://github.com/erikras/multireducer
|
||||
* Immutable.js (?)
|
||||
|
||||
|
||||
## Global UI
|
||||
* Filter does not work on github.io
|
||||
* What happens when JS is off?
|
||||
=> https://www.allantatter.com/react-js-and-progressive-enhancement/
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
* Webpack chunks?
|
||||
|
||||
|
||||
* Stocker pagination dans le store
|
||||
* error / info: null
|
||||
* refondre reducers
|
||||
* Immutable.js : entities in API
|
||||
|
@ -1,16 +1,19 @@
|
||||
import humps from "humps";
|
||||
|
||||
import { CALL_API } from "../middleware/api";
|
||||
import { DEFAULT_LIMIT } from "../reducers/paginate";
|
||||
|
||||
export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */
|
||||
|
||||
export default function (action, requestType, successType, failureType) {
|
||||
const itemName = action.rstrip("s");
|
||||
const fetchItemsSuccess = function (itemsList, itemsCount) {
|
||||
const fetchItemsSuccess = function (itemsList, itemsCount, pageNumber) {
|
||||
const nPages = Math.ceil(itemsCount / DEFAULT_LIMIT);
|
||||
return {
|
||||
type: successType,
|
||||
payload: {
|
||||
items: itemsList,
|
||||
total: itemsCount
|
||||
nPages: nPages,
|
||||
currentPage: pageNumber
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -29,7 +32,8 @@ export default function (action, requestType, successType, failureType) {
|
||||
}
|
||||
};
|
||||
};
|
||||
const fetchItems = function (endpoint, username, passphrase, filter, offset, include = [], limit=DEFAULT_LIMIT) {
|
||||
const fetchItems = function (endpoint, username, passphrase, filter, pageNumber, include = [], limit=DEFAULT_LIMIT) {
|
||||
const offset = (pageNumber - 1) * DEFAULT_LIMIT;
|
||||
var extraParams = {
|
||||
offset: offset,
|
||||
limit: limit
|
||||
@ -47,7 +51,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
dispatch: [
|
||||
fetchItemsRequest,
|
||||
jsonData => dispatch => {
|
||||
dispatch(fetchItemsSuccess(jsonData[itemName], jsonData[action]));
|
||||
dispatch(fetchItemsSuccess(jsonData[itemName], jsonData[action], pageNumber));
|
||||
},
|
||||
fetchItemsFailure
|
||||
],
|
||||
@ -61,8 +65,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
const loadItems = function({ pageNumber = 1, filter = null, include = [] } = {}) {
|
||||
return (dispatch, getState) => {
|
||||
const { auth } = getState();
|
||||
const offset = (pageNumber - 1) * DEFAULT_LIMIT;
|
||||
dispatch(fetchItems(auth.endpoint, auth.username, auth.token.token, filter, offset, include));
|
||||
dispatch(fetchItems(auth.endpoint, auth.username, auth.token.token, filter, pageNumber, include));
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,8 @@ import Cookies from "js-cookie";
|
||||
|
||||
import { CALL_API } from "../middleware/api";
|
||||
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
|
||||
export const DEFAULT_SESSION_INTERVAL = 1800 * 1000; // 30 mins default
|
||||
|
||||
function _cleanEndpoint (endpoint) {
|
||||
@ -46,7 +48,7 @@ export function loginKeepAlive(username, token, endpoint) {
|
||||
null,
|
||||
null,
|
||||
error => dispatch => {
|
||||
dispatch(loginUserFailure(error || "Your session expired… =("));
|
||||
dispatch(loginUserFailure(error || new i18nRecord({ id: "app.login.expired", values: {}})));
|
||||
}
|
||||
],
|
||||
action: "ping",
|
||||
@ -141,7 +143,7 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
|
||||
loginUserRequest,
|
||||
jsonData => dispatch => {
|
||||
if (!jsonData.auth || !jsonData.sessionExpire) {
|
||||
return Promise.reject("API error.");
|
||||
return Promise.reject(new i18nRecord({ id: "app.api.error", values: {} }));
|
||||
}
|
||||
const token = {
|
||||
token: jsonData.auth,
|
||||
|
@ -16,3 +16,5 @@ export const SONGS_SUCCESS = "SONGS_SUCCESS";
|
||||
export const SONGS_REQUEST = "SONGS_REQUEST";
|
||||
export const SONGS_FAILURE = "SONGS_FAILURE";
|
||||
export var { loadSongs } = APIAction("songs", SONGS_REQUEST, SONGS_SUCCESS, SONGS_FAILURE);
|
||||
|
||||
export * from "./paginate";
|
||||
|
7
app/actions/paginate.js
Normal file
7
app/actions/paginate.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
export function goToPage(pageLocation) {
|
||||
return (dispatch) => {
|
||||
dispatch(push(pageLocation));
|
||||
};
|
||||
}
|
@ -1,19 +1,25 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import Immutable from "immutable";
|
||||
|
||||
import FilterablePaginatedGrid from "./elements/Grid";
|
||||
|
||||
export default class Albums extends Component {
|
||||
render () {
|
||||
const grid = {
|
||||
isFetching: this.props.isFetching,
|
||||
items: this.props.albums,
|
||||
itemsLabel: "app.common.album",
|
||||
subItemsType: "tracks",
|
||||
subItemsLabel: "app.common.track"
|
||||
};
|
||||
return (
|
||||
<FilterablePaginatedGrid items={this.props.albums} itemsTotalCount={this.props.albumsTotalCount} itemsPerPage={this.props.albumsPerPage} currentPage={this.props.currentPage} location={this.props.location} itemsType="albums" subItemsType="tracks" />
|
||||
<FilterablePaginatedGrid grid={grid} pagination={this.props.pagination} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Albums.propTypes = {
|
||||
albums: PropTypes.array.isRequired,
|
||||
albumsTotalCount: PropTypes.number.isRequired,
|
||||
albumsPerPage: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
albums: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
pagination: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -1,19 +1,27 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import Immutable from "immutable";
|
||||
|
||||
import FilterablePaginatedGrid from "./elements/Grid";
|
||||
|
||||
export default class Artists extends Component {
|
||||
class Artists extends Component {
|
||||
render () {
|
||||
const grid = {
|
||||
isFetching: this.props.isFetching,
|
||||
items: this.props.artists,
|
||||
itemsLabel: "app.common.artist",
|
||||
subItemsType: "albums",
|
||||
subItemsLabel: "app.common.album"
|
||||
};
|
||||
return (
|
||||
<FilterablePaginatedGrid items={this.props.artists} itemsTotalCount={this.props.artistsTotalCount} itemsPerPage={this.props.artistsPerPage} currentPage={this.props.currentPage} location={this.props.location} itemsType="artists" subItemsType="albums" />
|
||||
<FilterablePaginatedGrid grid={grid} pagination={this.props.pagination} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Artists.propTypes = {
|
||||
artists: PropTypes.array.isRequired,
|
||||
artistsTotalCount: PropTypes.number.isRequired,
|
||||
artistsPerPage: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
artists: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
pagination: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Artists;
|
||||
|
@ -2,12 +2,14 @@ import React, { Component, PropTypes } from "react";
|
||||
import CSSModules from "react-css-modules";
|
||||
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
import { messagesMap } from "../utils";
|
||||
import APIMessages from "../locales/messagesDescriptors/api";
|
||||
import messages from "../locales/messagesDescriptors/Login";
|
||||
|
||||
import css from "../styles/Login.scss";
|
||||
|
||||
const loginMessages = defineMessages(messagesMap(messages));
|
||||
const loginMessages = defineMessages(messagesMap(Array.concat([], APIMessages, messages)));
|
||||
|
||||
class LoginFormCSSIntl extends Component {
|
||||
constructor (props) {
|
||||
@ -58,13 +60,17 @@ class LoginFormCSSIntl extends Component {
|
||||
|
||||
render () {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var infoMessage = "";
|
||||
if (typeof this.props.info === "object") {
|
||||
var infoMessage = this.props.info;
|
||||
if (this.props.info && this.props.info instanceof i18nRecord) {
|
||||
infoMessage = (
|
||||
<FormattedMessage {...loginMessages[this.props.info.id]} values={ this.props.info.values} />
|
||||
);
|
||||
} else {
|
||||
infoMessage = this.props.info;
|
||||
}
|
||||
var errorMessage = this.props.error;
|
||||
if (this.props.error && this.props.error instanceof i18nRecord) {
|
||||
errorMessage = (
|
||||
<FormattedMessage {...loginMessages[this.props.error.id]} values={ this.props.error.values} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
@ -73,7 +79,7 @@ class LoginFormCSSIntl extends Component {
|
||||
<div className="row">
|
||||
<div className="alert alert-danger" id="loginFormError">
|
||||
<p>
|
||||
<span className="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> { this.props.error }
|
||||
<span className="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> { errorMessage }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -135,8 +141,8 @@ LoginFormCSSIntl.propTypes = {
|
||||
rememberMe: PropTypes.bool,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
isAuthenticating: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
info: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(i18nRecord)]),
|
||||
info: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(i18nRecord)]),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
|
||||
import { Link} from "react-router";
|
||||
import CSSModules from "react-css-modules";
|
||||
import { defineMessages, FormattedMessage } from "react-intl";
|
||||
import Immutable from "immutable";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
import FilterBar from "./elements/FilterBar";
|
||||
@ -25,7 +26,7 @@ export class SongsTableRow extends Component {
|
||||
<td></td>
|
||||
<td className="title">{this.props.song.name}</td>
|
||||
<td className="artist"><Link to={linkToArtist}>{this.props.song.artist.name}</Link></td>
|
||||
<td className="artist"><Link to={linkToAlbum}>{this.props.song.album.name}</Link></td>
|
||||
<td className="album"><Link to={linkToAlbum}>{this.props.song.album.name}</Link></td>
|
||||
<td className="genre">{this.props.song.genre}</td>
|
||||
<td className="length">{length}</td>
|
||||
</tr>
|
||||
@ -58,6 +59,15 @@ class SongsTableCSS extends Component {
|
||||
displayedSongs.forEach(function (song) {
|
||||
rows.push(<SongsTableRow song={song} key={song.id} />);
|
||||
});
|
||||
var loading = null;
|
||||
if (rows.length == 0 && this.props.isFetching) {
|
||||
// If we are fetching and there is nothing to show
|
||||
loading = (
|
||||
<p className="text-center">
|
||||
<FormattedMessage {...songsMessages["app.common.loading"]} />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-hover" styleName="songs">
|
||||
@ -67,10 +77,10 @@ class SongsTableCSS extends Component {
|
||||
<th>
|
||||
<FormattedMessage {...songsMessages["app.songs.title"]} />
|
||||
</th>
|
||||
<th>
|
||||
<th className="text-capitalize">
|
||||
<FormattedMessage {...songsMessages["app.common.artist"]} values={{itemCount: 1}} />
|
||||
</th>
|
||||
<th>
|
||||
<th className="text-capitalize">
|
||||
<FormattedMessage {...songsMessages["app.common.album"]} values={{itemCount: 1}} />
|
||||
</th>
|
||||
<th>
|
||||
@ -83,13 +93,14 @@ class SongsTableCSS extends Component {
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
{loading}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SongsTableCSS.propTypes = {
|
||||
songs: PropTypes.array.isRequired,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
filterText: PropTypes.string
|
||||
};
|
||||
|
||||
@ -113,21 +124,18 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const nPages = Math.ceil(this.props.songsTotalCount / this.props.songsPerPage);
|
||||
return (
|
||||
<div>
|
||||
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
||||
<SongsTable songs={this.props.songs} filterText={this.state.filterText} />
|
||||
<Pagination nPages={nPages} currentPage={this.props.currentPage} location={this.props.location} />
|
||||
<SongsTable isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} />
|
||||
<Pagination {...this.props.pagination} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterablePaginatedSongsTable.propTypes = {
|
||||
songs: PropTypes.array.isRequired,
|
||||
songsTotalCount: PropTypes.number.isRequired,
|
||||
songsPerPage: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
pagination: PropTypes.object.isRequired
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { Link} from "react-router";
|
||||
import CSSModules from "react-css-modules";
|
||||
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||
import Immutable from "immutable";
|
||||
import imagesLoaded from "imagesloaded";
|
||||
import Isotope from "isotope-layout";
|
||||
import Fuse from "fuse.js";
|
||||
@ -8,27 +10,30 @@ import shallowCompare from "react-addons-shallow-compare";
|
||||
|
||||
import FilterBar from "./FilterBar";
|
||||
import Pagination from "./Pagination";
|
||||
import { immutableDiff, messagesMap } from "../../utils/";
|
||||
|
||||
import commonMessages from "../../locales/messagesDescriptors/common";
|
||||
import messages from "../../locales/messagesDescriptors/grid";
|
||||
|
||||
import css from "../../styles/elements/Grid.scss";
|
||||
|
||||
class GridItemCSS extends Component {
|
||||
const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||
|
||||
class GridItemCSSIntl extends Component {
|
||||
render () {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
var nSubItems = this.props.item[this.props.subItemsType];
|
||||
if (Array.isArray(nSubItems)) {
|
||||
nSubItems = nSubItems.length;
|
||||
}
|
||||
|
||||
// TODO: i18n
|
||||
var subItemsLabel = this.props.subItemsType;
|
||||
if (nSubItems < 2) {
|
||||
subItemsLabel = subItemsLabel.rstrip("s");
|
||||
}
|
||||
var subItemsLabel = formatMessage(gridMessages[this.props.subItemsLabel], { itemCount: nSubItems });
|
||||
|
||||
const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id;
|
||||
const to = "/" + this.props.item.type + "/" + this.props.item.id;
|
||||
const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id;
|
||||
|
||||
// TODO: i18n
|
||||
const title = "Go to " + this.props.itemsType.rstrip("s") + " page";
|
||||
const title = formatMessage(gridMessages["app.grid.goTo" + this.props.item.type.capitalize() + "Page"]);
|
||||
return (
|
||||
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
|
||||
<div className="grid-item-content text-center">
|
||||
@ -41,13 +46,15 @@ class GridItemCSS extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
GridItemCSS.propTypes = {
|
||||
GridItemCSSIntl.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
itemsType: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired
|
||||
itemsLabel: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired,
|
||||
subItemsLabel: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export let GridItem = CSSModules(GridItemCSS, css);
|
||||
export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
|
||||
|
||||
|
||||
const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
|
||||
@ -150,17 +157,17 @@ export class Grid extends Component {
|
||||
(n) => "grid-item-" + n.type + "/" + n.id);
|
||||
|
||||
// Find which keys are new between the current set of keys and any new children passed to this component
|
||||
let addKeys = newKeys.diff(currentKeys);
|
||||
let addKeys = immutableDiff(newKeys, currentKeys);
|
||||
|
||||
// Find which keys have been removed between the current set of keys and any new children passed to this component
|
||||
let removeKeys = currentKeys.diff(newKeys);
|
||||
let removeKeys = immutableDiff(currentKeys, newKeys);
|
||||
|
||||
if (removeKeys.length > 0) {
|
||||
if (removeKeys.count() > 0) {
|
||||
removeKeys.forEach(removeKey => this.iso.remove(document.getElementById(removeKey)));
|
||||
this.iso.arrange();
|
||||
}
|
||||
if (addKeys.length > 0) {
|
||||
const itemsToAdd = addKeys.map((addKey) => document.getElementById(addKey));
|
||||
if (addKeys.count() > 0) {
|
||||
const itemsToAdd = addKeys.map((addKey) => document.getElementById(addKey)).toArray();
|
||||
this.iso.addItems(itemsToAdd);
|
||||
this.iso.arrange();
|
||||
}
|
||||
@ -178,12 +185,25 @@ export class Grid extends Component {
|
||||
|
||||
render () {
|
||||
var gridItems = [];
|
||||
const itemsType = this.props.itemsType;
|
||||
const itemsLabel = this.props.itemsLabel;
|
||||
const subItemsType = this.props.subItemsType;
|
||||
const subItemsLabel = this.props.subItemsLabel;
|
||||
this.props.items.forEach(function (item) {
|
||||
gridItems.push(<GridItem item={item} itemsType={itemsType} subItemsType={subItemsType} key={item.id} />);
|
||||
gridItems.push(<GridItem item={item} itemsLabel={itemsLabel} subItemsType={subItemsType} subItemsLabel={subItemsLabel} key={item.id} />);
|
||||
});
|
||||
var loading = null;
|
||||
if (gridItems.length == 0 && this.props.isFetching) {
|
||||
loading = (
|
||||
<div className="row text-center">
|
||||
<p>
|
||||
<FormattedMessage {...gridMessages["app.common.loading"]} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{ loading }
|
||||
<div className="row">
|
||||
<div className="grid" ref="grid">
|
||||
{/* Sizing element */}
|
||||
@ -192,13 +212,15 @@ export class Grid extends Component {
|
||||
{ gridItems }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Grid.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
itemsType: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
items: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
itemsLabel: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired,
|
||||
filterText: PropTypes.string
|
||||
};
|
||||
@ -220,23 +242,17 @@ export default class FilterablePaginatedGrid extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const nPages = Math.ceil(this.props.itemsTotalCount / this.props.itemsPerPage);
|
||||
return (
|
||||
<div>
|
||||
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
||||
<Grid items={this.props.items} itemsType={this.props.itemsType} subItemsType={this.props.subItemsType} filterText={this.state.filterText} />
|
||||
<Pagination nPages={nPages} currentPage={this.props.currentPage} location={this.props.location} />
|
||||
<Grid filterText={this.state.filterText} {...this.props.grid} />
|
||||
<Pagination {...this.props.pagination} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterablePaginatedGrid.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
itemsTotalCount: PropTypes.number.isRequired,
|
||||
itemsPerPage: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
itemsType: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired
|
||||
grid: PropTypes.object.isRequired,
|
||||
pagination: PropTypes.object.isRequired
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { Link, withRouter } from "react-router";
|
||||
import { Link } from "react-router";
|
||||
import CSSModules from "react-css-modules";
|
||||
import { defineMessages, injectIntl, intlShape, FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
||||
|
||||
@ -12,11 +12,6 @@ import css from "../../styles/elements/Pagination.scss";
|
||||
const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||
|
||||
class PaginationCSSIntl extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.buildLinkTo.bind(this);
|
||||
}
|
||||
|
||||
computePaginationBounds(currentPage, nPages, maxNumberPagesShown=5) {
|
||||
// Taken from http://stackoverflow.com/a/8608998/2626416
|
||||
var lowerLimit = currentPage;
|
||||
@ -39,18 +34,12 @@ class PaginationCSSIntl extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
buildLinkTo(pageNumber) {
|
||||
return {
|
||||
pathname: this.props.location.pathname,
|
||||
query: Object.assign({}, this.props.location.query, { page: pageNumber })
|
||||
};
|
||||
}
|
||||
|
||||
goToPage() {
|
||||
goToPage(ev) {
|
||||
ev.preventDefault();
|
||||
const pageNumber = parseInt(this.refs.pageInput.value);
|
||||
$(this.refs.paginationModal).modal("hide");
|
||||
if (pageNumber) {
|
||||
this.props.router.push(this.buildLinkTo(pageNumber));
|
||||
this.props.goToPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +68,7 @@ class PaginationCSSIntl extends Component {
|
||||
// Push first page
|
||||
pagesButton.push(
|
||||
<li className="page-item" key={key}>
|
||||
<Link className="page-link" title={formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: 1})} to={this.buildLinkTo(1)}>
|
||||
<Link className="page-link" title={formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: 1})} to={this.props.buildLinkToPage(1)}>
|
||||
<FormattedHTMLMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: 1 }} />
|
||||
</Link>
|
||||
</li>
|
||||
@ -106,7 +95,7 @@ class PaginationCSSIntl extends Component {
|
||||
const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: i });
|
||||
pagesButton.push(
|
||||
<li className={className} key={key}>
|
||||
<Link className="page-link" title={title} to={this.buildLinkTo(i)}>
|
||||
<Link className="page-link" title={title} to={this.props.buildLinkToPage(i)}>
|
||||
<FormattedHTMLMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: i }} />
|
||||
{currentSpan}
|
||||
</Link>
|
||||
@ -128,7 +117,7 @@ class PaginationCSSIntl extends Component {
|
||||
// Push last page
|
||||
pagesButton.push(
|
||||
<li className="page-item" key={key}>
|
||||
<Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}>
|
||||
<Link className="page-link" title={title} to={this.props.buildLinkToPage(this.props.nPages)}>
|
||||
<FormattedHTMLMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: this.props.nPages }} />
|
||||
</Link>
|
||||
</li>
|
||||
@ -152,8 +141,8 @@ class PaginationCSSIntl extends Component {
|
||||
</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form>
|
||||
<input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label={formatMessage(paginationMessages["app.pagination.pageToGoTo"])} />
|
||||
<form onSubmit={this.goToPage.bind(this)}>
|
||||
<input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label={formatMessage(paginationMessages["app.pagination.pageToGoTo"])} autoFocus />
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
@ -176,9 +165,10 @@ class PaginationCSSIntl extends Component {
|
||||
|
||||
PaginationCSSIntl.propTypes = {
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
goToPage: PropTypes.func.isRequired,
|
||||
buildLinkToPage: PropTypes.func.isRequired,
|
||||
nPages: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(injectIntl(CSSModules(PaginationCSSIntl, css)));
|
||||
export default injectIntl(CSSModules(PaginationCSSIntl, css));
|
||||
|
@ -82,10 +82,10 @@ class SidebarLayoutIntl extends Component {
|
||||
<li>
|
||||
<Link to="/artists" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseArtists"])} styleName={isActive.artists}>
|
||||
<span className="glyphicon glyphicon-user" aria-hidden="true"></span>
|
||||
<span className="sr-only">
|
||||
<span className="sr-only text-capitalize">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.artist"]} values={{itemCount: 42}} />
|
||||
</span>
|
||||
<span className="hidden-md">
|
||||
<span className="hidden-md text-capitalize">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.artist"]} values={{itemCount: 42}} />
|
||||
</span>
|
||||
</Link>
|
||||
@ -93,8 +93,10 @@ class SidebarLayoutIntl extends Component {
|
||||
<li>
|
||||
<Link to="/albums" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseAlbums"])} styleName={isActive.albums}>
|
||||
<span className="glyphicon glyphicon-cd" aria-hidden="true"></span>
|
||||
<span className="sr-only"><FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} /></span>
|
||||
<span className="hidden-md">
|
||||
<span className="sr-only text-capitalize">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} />
|
||||
</span>
|
||||
<span className="hidden-md text-capitalize">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} />
|
||||
</span>
|
||||
</Link>
|
||||
@ -102,11 +104,11 @@ class SidebarLayoutIntl extends Component {
|
||||
<li>
|
||||
<Link to="/songs" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseSongs"])} styleName={isActive.songs}>
|
||||
<span className="glyphicon glyphicon-music" aria-hidden="true"></span>
|
||||
<span className="sr-only">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.song"]} values={{itemCount: 42}} />
|
||||
<span className="sr-only text-capitalize">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.track"]} values={{itemCount: 42}} />
|
||||
</span>
|
||||
<span className="hidden-md">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.song"]} values={{itemCount: 42}} />
|
||||
<span className="hidden-md text-capitalize">
|
||||
<FormattedMessage {...sidebarLayoutMessages["app.common.track"]} values={{itemCount: 42}} />
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
6
app/dist/1.1.js
vendored
6
app/dist/1.1.js
vendored
File diff suppressed because one or more lines are too long
2
app/dist/1.1.js.map
vendored
2
app/dist/1.1.js.map
vendored
File diff suppressed because one or more lines are too long
2
app/dist/fix.ie9.js
vendored
2
app/dist/fix.ie9.js
vendored
@ -1,2 +1,2 @@
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(590);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},590:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(599);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},599:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
|
||||
//# sourceMappingURL=fix.ie9.js.map
|
2
app/dist/fix.ie9.js.map
vendored
2
app/dist/fix.ie9.js.map
vendored
File diff suppressed because one or more lines are too long
46
app/dist/index.js
vendored
46
app/dist/index.js
vendored
File diff suppressed because one or more lines are too long
2
app/dist/index.js.map
vendored
2
app/dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,15 +1,22 @@
|
||||
module.exports = {
|
||||
"app.common.album": "{itemCount, plural, one {Album} other {Albums}}", // Album
|
||||
"app.common.artist": "{itemCount, plural, one {Artist} other {Artists}}", // Artist
|
||||
"app.api.invalidResponse": "Invalid response text.", // Invalid response from the API
|
||||
"app.api.emptyResponse": "Empty response text.", // Empty response from the API
|
||||
"app.api.error": "Unknown API error.", // An unknown error occurred from the API
|
||||
"app.common.album": "{itemCount, plural, one {album} other {albums}}", // Album
|
||||
"app.common.artist": "{itemCount, plural, one {artist} other {artists}}", // Artist
|
||||
"app.common.cancel": "Cancel", // Cancel
|
||||
"app.common.close": "Close", // Close
|
||||
"app.common.go": "Go", // Go
|
||||
"app.common.song": "{itemCount, plural, one {Song} other {Songs}}", // Song
|
||||
"app.common.loading": "Loading…", // Loading indicator
|
||||
"app.common.track": "{itemCount, plural, one {track} other {tracks}}", // Track
|
||||
"app.filter.filter": "Filter…", // Filtering input placeholder
|
||||
"app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar
|
||||
"app.grid.goToArtistPage": "Go to artist page", // Artist thumbnail link title
|
||||
"app.grid.goToAlbumPage": "Go to album page", // Album thumbnail link title
|
||||
"app.login.byebye": "See you soon!", // Info message on successful logout
|
||||
"app.login.connecting": "Connecting…", // Info message while trying to connect
|
||||
"app.login.endpointInputAriaLabel": "URL of your Ampache instance (e.g. http://ampache.example.com)", // ARIA label for the endpoint input
|
||||
"app.login.expired": "Your session expired… =(", // Error message on expired session
|
||||
"app.login.greeting": "Welcome back on Ampache, let's go!", // Greeting to welcome the user to the app
|
||||
"app.login.password": "Password", // Password input placeholder
|
||||
"app.login.rememberMe": "Remember me", // Remember me checkbox label
|
||||
|
@ -1,15 +1,22 @@
|
||||
module.exports = {
|
||||
"app.common.album": "{itemCount, plural, one {Album} other {Albums}}", // Albums
|
||||
"app.common.artist": "{itemCount, plural, one {Artiste} other {Artistes}}", // Artists
|
||||
"app.api.invalidResponse": "Réponse invalide reçue.", // Invalid response from the API
|
||||
"app.api.emptyResponse": "Réponse vide reçue.", // Empty response from the API
|
||||
"app.api.error": "Erreur inconnue.", // An unknown error occurred from the API
|
||||
"app.common.album": "{itemCount, plural, one {album} other {albums}}", // Albums
|
||||
"app.common.artist": "{itemCount, plural, one {artiste} other {artistes}}", // Artists
|
||||
"app.common.cancel": "Annuler", // Cancel
|
||||
"app.common.close": "Fermer", // Close
|
||||
"app.common.go": "Aller", // Go
|
||||
"app.common.song": "{itemCount, plural, one {Piste} other {Pistes}}", // Song
|
||||
"app.common.loading": "Chargement…", // Loading indicator
|
||||
"app.common.track": "{itemCount, plural, one {piste} other {pistes}}", // Track
|
||||
"app.filter.filter": "Filtrer…", // Filtering input placeholder
|
||||
"app.filter.whatAreWeListeningToToday": "Que voulez-vous écouter aujourd'hui\u00a0?", // Description for the filter bar
|
||||
"app.grid.goToArtistPage": "Aller à la page de l'artiste", // Artist thumbnail link title
|
||||
"app.grid.goToAlbumPage": "Aller à la page de l'album", // Album thumbnail link title
|
||||
"app.login.byebye": "À bientôt\u00a0!", // Info message on successful logout
|
||||
"app.login.connecting": "Connexion…", // Info message while trying to connect
|
||||
"app.login.endpointInputAriaLabel": "URL de votre Ampache (e.g. http://ampache.example.com)", // ARIA label for the endpoint input
|
||||
"app.login.expired": "Session expirée… =(", // Error message on expired session
|
||||
"app.login.greeting": "Bon retour sur Ampache, c'est parti\u00a0!", // Greeting to welcome the user to the app
|
||||
"app.login.password": "Mot de passe", // Password input placeholder
|
||||
"app.login.rememberMe": "Se souvenir", // Remember me checkbox label
|
||||
|
@ -45,6 +45,11 @@ const messages = [
|
||||
id: "app.login.byebye",
|
||||
defaultMessage: "See you soon!",
|
||||
description: "Info message on successful logout"
|
||||
},
|
||||
{
|
||||
id: "app.login.expired",
|
||||
defaultMessage: "Your session expired… =(",
|
||||
description: "Error message on expired session"
|
||||
}
|
||||
];
|
||||
|
||||
|
19
app/locales/messagesDescriptors/api.js
Normal file
19
app/locales/messagesDescriptors/api.js
Normal file
@ -0,0 +1,19 @@
|
||||
const messages = [
|
||||
{
|
||||
id: "app.api.invalidResponse",
|
||||
defaultMessage: "Invalid response text.",
|
||||
description: "Invalid response from the API"
|
||||
},
|
||||
{
|
||||
id: "app.api.emptyResponse",
|
||||
defaultMessage: "Empty response text.",
|
||||
description: "Empty response from the API"
|
||||
},
|
||||
{
|
||||
id: "app.api.error",
|
||||
defaultMessage: "Unknown API error.",
|
||||
description: "An unknown error occurred from the API"
|
||||
}
|
||||
];
|
||||
|
||||
export default messages;
|
@ -17,17 +17,22 @@ const messages = [
|
||||
{
|
||||
id: "app.common.artist",
|
||||
description: "Artist",
|
||||
defaultMessage: "{itemCount, plural, one {Artist} other {Artists}}"
|
||||
defaultMessage: "{itemCount, plural, one {artist} other {artists}}"
|
||||
},
|
||||
{
|
||||
id: "app.common.album",
|
||||
description: "Album",
|
||||
defaultMessage: "{itemCount, plural, one {Album} other {Albums}}"
|
||||
defaultMessage: "{itemCount, plural, one {album} other {albums}}"
|
||||
},
|
||||
{
|
||||
id: "app.common.song",
|
||||
description: "Song",
|
||||
defaultMessage: "{itemCount, plural, one {Song} other {Songs}}"
|
||||
id: "app.common.track",
|
||||
description: "Track",
|
||||
defaultMessage: "{itemCount, plural, one {track} other {tracks}}"
|
||||
},
|
||||
{
|
||||
id: "app.common.loading",
|
||||
description: "Loading indicator",
|
||||
defaultMessage: "Loading…"
|
||||
},
|
||||
];
|
||||
|
||||
|
14
app/locales/messagesDescriptors/grid.js
Normal file
14
app/locales/messagesDescriptors/grid.js
Normal file
@ -0,0 +1,14 @@
|
||||
const messages = [
|
||||
{
|
||||
id: "app.grid.goToArtistPage",
|
||||
defaultMessage: "Go to artist page",
|
||||
description: "Artist thumbnail link title"
|
||||
},
|
||||
{
|
||||
id: "app.grid.goToAlbumPage",
|
||||
defaultMessage: "Go to album page",
|
||||
description: "Album thumbnail link title"
|
||||
}
|
||||
];
|
||||
|
||||
export default messages;
|
@ -3,6 +3,7 @@ import humps from "humps";
|
||||
import X2JS from "x2js";
|
||||
|
||||
import { assembleURLAndParams } from "../utils";
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
|
||||
export const API_VERSION = 350001; /** API version to use. */
|
||||
export const BASE_API_PATH = "/server/xml.server.php"; /** Base API path after endpoint. */
|
||||
@ -26,7 +27,10 @@ function _parseToJSON (responseText) {
|
||||
if (responseText) {
|
||||
return x2js.xml_str2json(responseText).root;
|
||||
}
|
||||
return Promise.reject("Invalid response text.");
|
||||
return Promise.reject(new i18nRecord({
|
||||
id: "app.api.invalidResponse",
|
||||
values: {}
|
||||
}));
|
||||
}
|
||||
|
||||
function _checkAPIErrors (jsonData) {
|
||||
@ -34,7 +38,10 @@ function _checkAPIErrors (jsonData) {
|
||||
return Promise.reject(jsonData.error.cdata + " (" + jsonData.error.code + ")");
|
||||
} else if (!jsonData) {
|
||||
// No data returned
|
||||
return Promise.reject("Empty response");
|
||||
return Promise.reject(new i18nRecord({
|
||||
id: "app.api.emptyResponse",
|
||||
values: {}
|
||||
}));
|
||||
}
|
||||
return jsonData;
|
||||
}
|
||||
@ -52,6 +59,8 @@ function _uglyFixes (endpoint, token) {
|
||||
|
||||
var _uglyFixesSongs = function (songs) {
|
||||
for (var i = 0; i < songs.length; i++) {
|
||||
// Add song type
|
||||
songs[i].type = "track";
|
||||
// Fix for name becoming title in songs objects
|
||||
songs[i].name = songs[i].title;
|
||||
// Fix for length being time in songs objects
|
||||
@ -66,6 +75,9 @@ function _uglyFixes (endpoint, token) {
|
||||
|
||||
var _uglyFixesAlbums = function (albums) {
|
||||
for (var i = 0; i < albums.length; i++) {
|
||||
// Add album type
|
||||
albums[i].type = "album";
|
||||
|
||||
// Fix for absence of distinction between disks in the same album
|
||||
if (albums[i].disk > 1) {
|
||||
albums[i].name = albums[i].name + " [Disk " + albums[i].disk + "]";
|
||||
@ -117,6 +129,9 @@ function _uglyFixes (endpoint, token) {
|
||||
|
||||
if (jsonData.artist) {
|
||||
for (var i = 0; i < jsonData.artist.length; i++) {
|
||||
// Add artist type
|
||||
jsonData.artist[i].type = "artist";
|
||||
|
||||
// Fix for artists art not included
|
||||
jsonData.artist[i].art = endpoint.replace("/server/xml.server.php", "") + "/image.php?object_id=" + jsonData.artist[i].id + "&object_type=artist&auth=" + token;
|
||||
|
||||
@ -190,8 +205,8 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
|
||||
.then(_checkHTTPStatus)
|
||||
.then (response => response.text())
|
||||
.then(_parseToJSON)
|
||||
.then(_uglyFixes(endpoint, auth))
|
||||
.then(_checkAPIErrors);
|
||||
.then(_checkAPIErrors)
|
||||
.then(_uglyFixes(endpoint, auth));
|
||||
}
|
||||
|
||||
// Action key that carries API call info interpreted by this Redux middleware.
|
||||
@ -239,7 +254,7 @@ export default store => next => reduxAction => {
|
||||
},
|
||||
error => {
|
||||
if (failureDispatch) {
|
||||
if (typeof error !== "string") {
|
||||
if (error instanceof Error) {
|
||||
error = error.message;
|
||||
}
|
||||
store.dispatch(failureDispatch(error));
|
||||
|
18
app/models/auth.js
Normal file
18
app/models/auth.js
Normal file
@ -0,0 +1,18 @@
|
||||
import Immutable from "immutable";
|
||||
|
||||
export const tokenRecord = Immutable.Record({
|
||||
token: null,
|
||||
expires: null
|
||||
});
|
||||
|
||||
export const stateRecord = new Immutable.Record({
|
||||
token: tokenRecord,
|
||||
username: null,
|
||||
endpoint: null,
|
||||
rememberMe: false,
|
||||
isAuthenticated: false,
|
||||
isAuthenticating: false,
|
||||
error: null,
|
||||
info: null,
|
||||
timerID: null
|
||||
});
|
6
app/models/i18n.js
Normal file
6
app/models/i18n.js
Normal file
@ -0,0 +1,6 @@
|
||||
import Immutable from "immutable";
|
||||
|
||||
export const i18nRecord = new Immutable.Record({
|
||||
id: null,
|
||||
values: new Immutable.Map()
|
||||
});
|
9
app/models/paginate.js
Normal file
9
app/models/paginate.js
Normal file
@ -0,0 +1,9 @@
|
||||
import Immutable from "immutable";
|
||||
|
||||
export const stateRecord = new Immutable.Record({
|
||||
isFetching: false,
|
||||
items: new Immutable.List(),
|
||||
error: null,
|
||||
currentPage: 1,
|
||||
nPages: 1
|
||||
});
|
@ -2,82 +2,82 @@ import Cookies from "js-cookie";
|
||||
|
||||
import {LOGIN_USER_REQUEST, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, LOGOUT_USER} from "../actions";
|
||||
import { createReducer } from "../utils";
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
import { tokenRecord, stateRecord } from "../models/auth";
|
||||
|
||||
var initialToken = Cookies.getJSON("token");
|
||||
/**
|
||||
* Initial state
|
||||
*/
|
||||
|
||||
var initialState = new stateRecord();
|
||||
const initialToken = Cookies.getJSON("token");
|
||||
if (initialToken) {
|
||||
initialToken.expires = new Date(initialToken.expires);
|
||||
initialState = initialState.set(
|
||||
"token",
|
||||
new tokenRecord({ token: initialToken.token, expires: new Date(initialToken.expires) })
|
||||
);
|
||||
}
|
||||
const initialUsername = Cookies.get("username");
|
||||
if (initialUsername) {
|
||||
initialState = initialState.set(
|
||||
"username",
|
||||
initialUsername
|
||||
);
|
||||
}
|
||||
const initialEndpoint = Cookies.get("endpoint");
|
||||
if (initialEndpoint) {
|
||||
initialState = initialState.set(
|
||||
"endpoint",
|
||||
initialEndpoint
|
||||
);
|
||||
}
|
||||
if (initialUsername && initialEndpoint) {
|
||||
initialState = initialState.set(
|
||||
"rememberMe",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
token: initialToken || {
|
||||
token: "",
|
||||
expires: null
|
||||
},
|
||||
username: Cookies.get("username"),
|
||||
endpoint: Cookies.get("endpoint"),
|
||||
rememberMe: Boolean(Cookies.get("username") && Cookies.get("endpoint")),
|
||||
isAuthenticated: false,
|
||||
isAuthenticating: false,
|
||||
error: "",
|
||||
info: "",
|
||||
timerID: null
|
||||
};
|
||||
/**
|
||||
* Reducers
|
||||
*/
|
||||
|
||||
export default createReducer(initialState, {
|
||||
[LOGIN_USER_REQUEST]: (state) => {
|
||||
return Object.assign({}, state, {
|
||||
[LOGIN_USER_REQUEST]: () => {
|
||||
return new stateRecord({
|
||||
isAuthenticating: true,
|
||||
info: {
|
||||
info: new i18nRecord({
|
||||
id: "app.login.connecting",
|
||||
values: {}
|
||||
},
|
||||
error: "",
|
||||
timerID: null
|
||||
})
|
||||
});
|
||||
},
|
||||
[LOGIN_USER_SUCCESS]: (state, payload) => {
|
||||
return Object.assign({}, state, {
|
||||
isAuthenticating: false,
|
||||
isAuthenticated: true,
|
||||
token: payload.token,
|
||||
username: payload.username,
|
||||
endpoint: payload.endpoint,
|
||||
rememberMe: payload.rememberMe,
|
||||
info: {
|
||||
return new stateRecord({
|
||||
"isAuthenticated": true,
|
||||
"token": new tokenRecord(payload.token),
|
||||
"username": payload.username,
|
||||
"endpoint": payload.endpoint,
|
||||
"rememberMe": payload.rememberMe,
|
||||
"info": new i18nRecord({
|
||||
id: "app.login.success",
|
||||
values: { username: payload.username}
|
||||
},
|
||||
error: "",
|
||||
timerID: payload.timerID
|
||||
values: {username: payload.username}
|
||||
}),
|
||||
"timerID": payload.timerID
|
||||
});
|
||||
|
||||
},
|
||||
[LOGIN_USER_FAILURE]: (state, payload) => {
|
||||
return Object.assign({}, state, {
|
||||
isAuthenticating: false,
|
||||
isAuthenticated: false,
|
||||
token: initialState.token,
|
||||
username: "",
|
||||
endpoint: "",
|
||||
rememberMe: false,
|
||||
info: "",
|
||||
error: payload.error,
|
||||
timerID: 0
|
||||
return new stateRecord({
|
||||
"error": payload.error
|
||||
});
|
||||
},
|
||||
[LOGOUT_USER]: (state) => {
|
||||
return Object.assign({}, state, {
|
||||
isAuthenticated: false,
|
||||
token: initialState.token,
|
||||
username: "",
|
||||
endpoint: "",
|
||||
rememberMe: false,
|
||||
info: {
|
||||
[LOGOUT_USER]: () => {
|
||||
return new stateRecord({
|
||||
info: new i18nRecord({
|
||||
id: "app.login.byebye",
|
||||
values: {}
|
||||
},
|
||||
error: "",
|
||||
timerID: 0
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,9 @@
|
||||
import Immutable from "immutable";
|
||||
|
||||
import { createReducer } from "../utils";
|
||||
import { stateRecord } from "../models/paginate";
|
||||
|
||||
export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */
|
||||
|
||||
const initialState = {
|
||||
isFetching: false,
|
||||
items: [],
|
||||
total: 0,
|
||||
error: ""
|
||||
};
|
||||
const initialState = new stateRecord();
|
||||
|
||||
// Creates a reducer managing pagination, given the action types to handle,
|
||||
// and a function telling how to extract the key from an action.
|
||||
@ -23,24 +19,28 @@ export default function paginate(types) {
|
||||
|
||||
return createReducer(initialState, {
|
||||
[requestType]: (state) => {
|
||||
return Object.assign({}, state, {
|
||||
isFetching: true,
|
||||
error: "",
|
||||
});
|
||||
return (
|
||||
state
|
||||
.set("isFetching", true)
|
||||
.set("error", null)
|
||||
);
|
||||
},
|
||||
[successType]: (state, payload) => {
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
items: payload.items,
|
||||
total: payload.total,
|
||||
error: ""
|
||||
});
|
||||
return (
|
||||
state
|
||||
.set("isFetching", false)
|
||||
.set("items", new Immutable.List(payload.items))
|
||||
.set("error", null)
|
||||
.set("nPages", payload.nPages)
|
||||
.set("currentPage", payload.currentPage)
|
||||
);
|
||||
},
|
||||
[failureType]: (state, payload) => {
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
error: payload.error
|
||||
});
|
||||
return (
|
||||
state
|
||||
.set("isFetching", false)
|
||||
.set("error", payload.error)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
5
app/utils/immutable.js
Normal file
5
app/utils/immutable.js
Normal file
@ -0,0 +1,5 @@
|
||||
export function immutableDiff (a, b) {
|
||||
return a.filter(function (i) {
|
||||
return b.indexOf(i) < 0;
|
||||
});
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export * from "./immutable";
|
||||
export * from "./locale";
|
||||
export * from "./misc";
|
||||
export * from "./pagination";
|
||||
export * from "./reducers";
|
||||
export * from "./url";
|
||||
|
@ -1,23 +1,25 @@
|
||||
export function getBrowserLocale () {
|
||||
var lang;
|
||||
export function getBrowserLocales () {
|
||||
var langs;
|
||||
|
||||
if (navigator.languages) {
|
||||
// chrome does not currently set navigator.language correctly https://code.google.com/p/chromium/issues/detail?id=101138
|
||||
// but it does set the first element of navigator.languages correctly
|
||||
lang = navigator.languages[0];
|
||||
langs = navigator.languages;
|
||||
} else if (navigator.userLanguage) {
|
||||
// IE only
|
||||
lang = navigator.userLanguage;
|
||||
langs = [navigator.userLanguage];
|
||||
} else {
|
||||
// as of this writing the latest version of firefox + safari set this correctly
|
||||
lang = navigator.language;
|
||||
langs = [navigator.language];
|
||||
}
|
||||
|
||||
// Some browsers does not return uppercase for second part
|
||||
var locales = langs.map(function (lang) {
|
||||
var locale = lang.split("-");
|
||||
locale = locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
|
||||
return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
|
||||
});
|
||||
|
||||
return locale;
|
||||
return locales;
|
||||
}
|
||||
|
||||
export function messagesMap(messagesDescriptorsArray) {
|
||||
|
14
app/utils/pagination.js
Normal file
14
app/utils/pagination.js
Normal file
@ -0,0 +1,14 @@
|
||||
export function buildPaginationObject(location, currentPage, nPages, goToPageAction) {
|
||||
const buildLinkToPage = function (pageNumber) {
|
||||
return {
|
||||
pathname: location.pathname,
|
||||
query: Object.assign({}, location.query, { page: pageNumber })
|
||||
};
|
||||
};
|
||||
return {
|
||||
currentPage: currentPage,
|
||||
nPages: nPages,
|
||||
goToPage: pageNumber => goToPageAction(buildLinkToPage(pageNumber)),
|
||||
buildLinkToPage: buildLinkToPage
|
||||
};
|
||||
}
|
@ -1,13 +1,19 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { defineMessages, injectIntl, intlShape } from "react-intl";
|
||||
|
||||
import * as actionCreators from "../actions";
|
||||
import { DEFAULT_LIMIT } from "../reducers/paginate";
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
import { buildPaginationObject, messagesMap } from "../utils";
|
||||
|
||||
import Albums from "../components/Albums";
|
||||
|
||||
export class AlbumsPage extends Component {
|
||||
import APIMessages from "../locales/messagesDescriptors/api";
|
||||
|
||||
const albumsMessages = defineMessages(messagesMap(APIMessages));
|
||||
|
||||
class AlbumsPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
// Load the data
|
||||
@ -24,20 +30,41 @@ export class AlbumsPage extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
const {formatMessage} = this.props.intl;
|
||||
if (this.props.error) {
|
||||
var errorMessage = this.props.error;
|
||||
if (this.props.error instanceof i18nRecord) {
|
||||
errorMessage = formatMessage(albumsMessages[this.props.error.id], this.props.error.values);
|
||||
}
|
||||
alert(errorMessage);
|
||||
this.context.router.replace("/");
|
||||
return null;
|
||||
}
|
||||
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
|
||||
return (
|
||||
<Albums albums={this.props.albumsList} albumsTotalCount={this.props.albumsCount} albumsPerPage={DEFAULT_LIMIT} currentPage={currentPage} location={this.props.location} />
|
||||
<Albums isFetching={this.props.isFetching} albums={this.props.albumsList} pagination={pagination} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumsPageIntl.contextTypes = {
|
||||
router: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
AlbumsPageIntl.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isFetching: state.pagination.albums.isFetching,
|
||||
error: state.pagination.albums.error,
|
||||
albumsList: state.pagination.albums.items,
|
||||
albumsCount: state.pagination.albums.total
|
||||
currentPage: state.pagination.albums.currentPage,
|
||||
nPages: state.pagination.albums.nPages
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AlbumsPage);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AlbumsPageIntl));
|
||||
|
@ -1,13 +1,19 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { defineMessages, injectIntl, intlShape } from "react-intl";
|
||||
|
||||
import * as actionCreators from "../actions";
|
||||
import { DEFAULT_LIMIT } from "../reducers/paginate";
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
import { buildPaginationObject, messagesMap } from "../utils";
|
||||
|
||||
import Artists from "../components/Artists";
|
||||
|
||||
export class ArtistsPage extends Component {
|
||||
import APIMessages from "../locales/messagesDescriptors/api";
|
||||
|
||||
const artistsMessages = defineMessages(messagesMap(APIMessages));
|
||||
|
||||
class ArtistsPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
// Load the data
|
||||
@ -24,20 +30,40 @@ export class ArtistsPage extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
const {formatMessage} = this.props.intl;
|
||||
if (this.props.error) {
|
||||
var errorMessage = this.props.error;
|
||||
if (this.props.error instanceof i18nRecord) {
|
||||
errorMessage = formatMessage(artistsMessages[this.props.error.id], this.props.error.values);
|
||||
}
|
||||
alert(errorMessage);
|
||||
this.context.router.replace("/");
|
||||
return null;
|
||||
}
|
||||
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
|
||||
return (
|
||||
<Artists artists={this.props.artistsList} artistsTotalCount={this.props.artistsCount} artistsPerPage={DEFAULT_LIMIT} currentPage={currentPage} location={this.props.location} />
|
||||
<Artists isFetching={this.props.isFetching} artists={this.props.artistsList} pagination={pagination} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistsPageIntl.contextTypes = {
|
||||
router: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
ArtistsPageIntl.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isFetching: state.pagination.artists.isFetching,
|
||||
artistsList: state.pagination.artists.items,
|
||||
artistsCount: state.pagination.artists.total
|
||||
currentPage: state.pagination.artists.currentPage,
|
||||
nPages: state.pagination.artists.nPages
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ArtistsPage);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistsPageIntl));
|
||||
|
@ -1,13 +1,19 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { defineMessages, injectIntl, intlShape } from "react-intl";
|
||||
|
||||
import * as actionCreators from "../actions";
|
||||
import { DEFAULT_LIMIT } from "../reducers/paginate";
|
||||
import { i18nRecord } from "../models/i18n";
|
||||
import { buildPaginationObject, messagesMap } from "../utils";
|
||||
|
||||
import Songs from "../components/Songs";
|
||||
|
||||
export class SongsPage extends Component {
|
||||
import APIMessages from "../locales/messagesDescriptors/api";
|
||||
|
||||
const songsMessages = defineMessages(messagesMap(APIMessages));
|
||||
|
||||
class SongsPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
// Load the data
|
||||
@ -24,20 +30,40 @@ export class SongsPage extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
const {formatMessage} = this.props.intl;
|
||||
if (this.props.error) {
|
||||
var errorMessage = this.props.error;
|
||||
if (this.props.error instanceof i18nRecord) {
|
||||
errorMessage = formatMessage(songsMessages[this.props.error.id], this.props.error.values);
|
||||
}
|
||||
alert(errorMessage);
|
||||
this.context.router.replace("/");
|
||||
return null;
|
||||
}
|
||||
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
|
||||
return (
|
||||
<Songs songs={this.props.songsList} songsTotalCount={this.props.songsCount} songsPerPage={DEFAULT_LIMIT} currentPage={currentPage} location={this.props.location} />
|
||||
<Songs isFetching={this.props.isFetching} songs={this.props.songsList} pagination={pagination} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SongsPageIntl.contextTypes = {
|
||||
router: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
SongsPageIntl.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isFetching: state.pagination.songs.isFetching,
|
||||
songsList: state.pagination.songs.items,
|
||||
songsCount: state.pagination.songs.total
|
||||
currentPage: state.pagination.songs.currentPage,
|
||||
nPages: state.pagination.songs.nPages
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SongsPage);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SongsPageIntl));
|
||||
|
15
index.all.js
15
index.all.js
@ -11,7 +11,7 @@ import fr from "react-intl/locale-data/fr";
|
||||
|
||||
import configureStore from "./app/store/configureStore";
|
||||
|
||||
import { getBrowserLocale } from "./app/utils";
|
||||
import { getBrowserLocales } from "./app/utils";
|
||||
import rawMessages from "./app/locales";
|
||||
|
||||
const store = configureStore();
|
||||
@ -22,8 +22,17 @@ export const rootElement = document.getElementById("root");
|
||||
// i18n
|
||||
export const onWindowIntl = () => {
|
||||
addLocaleData([...en, ...fr]);
|
||||
const locale = getBrowserLocale(); // TODO: Get navigator.languages as array and match
|
||||
var strings = rawMessages[locale] ? rawMessages[locale] : rawMessages["en-US"];
|
||||
const locales = getBrowserLocales();
|
||||
|
||||
var locale = "en-US";
|
||||
var strings = {};
|
||||
for (var i = 0; i < locales.length; ++i) {
|
||||
if (rawMessages[locales[i]]) {
|
||||
locale = locales[i];
|
||||
strings = rawMessages[locale];
|
||||
break;
|
||||
}
|
||||
}
|
||||
strings = Object.assign(rawMessages["en-US"], strings);
|
||||
|
||||
let render = () => {
|
||||
|
@ -26,6 +26,7 @@
|
||||
"html5shiv": "^3.7.3",
|
||||
"humps": "^1.1.0",
|
||||
"imagesloaded": "^4.1.0",
|
||||
"immutable": "^3.8.1",
|
||||
"intl": "^1.2.4",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"isotope-layout": "^3.0.1",
|
||||
|
Loading…
Reference in New Issue
Block a user