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
|
5. Web player
|
||||||
6. Homepage
|
6. Homepage
|
||||||
7. Settings
|
7. Settings
|
||||||
|
@ -6,25 +6,4 @@
|
||||||
9. Discover
|
9. Discover
|
||||||
|
|
||||||
# API middleware
|
# API middleware
|
||||||
* https://github.com/reactjs/redux/issues/1824#issuecomment-228609501
|
* Immutable.js : entities in API
|
||||||
* 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
|
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import humps from "humps";
|
import humps from "humps";
|
||||||
|
|
||||||
import { CALL_API } from "../middleware/api";
|
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) {
|
export default function (action, requestType, successType, failureType) {
|
||||||
const itemName = action.rstrip("s");
|
const itemName = action.rstrip("s");
|
||||||
const fetchItemsSuccess = function (itemsList, itemsCount) {
|
const fetchItemsSuccess = function (itemsList, itemsCount, pageNumber) {
|
||||||
|
const nPages = Math.ceil(itemsCount / DEFAULT_LIMIT);
|
||||||
return {
|
return {
|
||||||
type: successType,
|
type: successType,
|
||||||
payload: {
|
payload: {
|
||||||
items: itemsList,
|
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 = {
|
var extraParams = {
|
||||||
offset: offset,
|
offset: offset,
|
||||||
limit: limit
|
limit: limit
|
||||||
|
@ -47,7 +51,7 @@ export default function (action, requestType, successType, failureType) {
|
||||||
dispatch: [
|
dispatch: [
|
||||||
fetchItemsRequest,
|
fetchItemsRequest,
|
||||||
jsonData => dispatch => {
|
jsonData => dispatch => {
|
||||||
dispatch(fetchItemsSuccess(jsonData[itemName], jsonData[action]));
|
dispatch(fetchItemsSuccess(jsonData[itemName], jsonData[action], pageNumber));
|
||||||
},
|
},
|
||||||
fetchItemsFailure
|
fetchItemsFailure
|
||||||
],
|
],
|
||||||
|
@ -61,8 +65,7 @@ export default function (action, requestType, successType, failureType) {
|
||||||
const loadItems = function({ pageNumber = 1, filter = null, include = [] } = {}) {
|
const loadItems = function({ pageNumber = 1, filter = null, include = [] } = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { auth } = getState();
|
const { auth } = getState();
|
||||||
const offset = (pageNumber - 1) * DEFAULT_LIMIT;
|
dispatch(fetchItems(auth.endpoint, auth.username, auth.token.token, filter, pageNumber, include));
|
||||||
dispatch(fetchItems(auth.endpoint, auth.username, auth.token.token, filter, offset, include));
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import Cookies from "js-cookie";
|
||||||
|
|
||||||
import { CALL_API } from "../middleware/api";
|
import { CALL_API } from "../middleware/api";
|
||||||
|
|
||||||
|
import { i18nRecord } from "../models/i18n";
|
||||||
|
|
||||||
export const DEFAULT_SESSION_INTERVAL = 1800 * 1000; // 30 mins default
|
export const DEFAULT_SESSION_INTERVAL = 1800 * 1000; // 30 mins default
|
||||||
|
|
||||||
function _cleanEndpoint (endpoint) {
|
function _cleanEndpoint (endpoint) {
|
||||||
|
@ -46,7 +48,7 @@ export function loginKeepAlive(username, token, endpoint) {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
error => dispatch => {
|
error => dispatch => {
|
||||||
dispatch(loginUserFailure(error || "Your session expired… =("));
|
dispatch(loginUserFailure(error || new i18nRecord({ id: "app.login.expired", values: {}})));
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
action: "ping",
|
action: "ping",
|
||||||
|
@ -141,7 +143,7 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
|
||||||
loginUserRequest,
|
loginUserRequest,
|
||||||
jsonData => dispatch => {
|
jsonData => dispatch => {
|
||||||
if (!jsonData.auth || !jsonData.sessionExpire) {
|
if (!jsonData.auth || !jsonData.sessionExpire) {
|
||||||
return Promise.reject("API error.");
|
return Promise.reject(new i18nRecord({ id: "app.api.error", values: {} }));
|
||||||
}
|
}
|
||||||
const token = {
|
const token = {
|
||||||
token: jsonData.auth,
|
token: jsonData.auth,
|
||||||
|
|
|
@ -16,3 +16,5 @@ export const SONGS_SUCCESS = "SONGS_SUCCESS";
|
||||||
export const SONGS_REQUEST = "SONGS_REQUEST";
|
export const SONGS_REQUEST = "SONGS_REQUEST";
|
||||||
export const SONGS_FAILURE = "SONGS_FAILURE";
|
export const SONGS_FAILURE = "SONGS_FAILURE";
|
||||||
export var { loadSongs } = APIAction("songs", SONGS_REQUEST, SONGS_SUCCESS, SONGS_FAILURE);
|
export var { loadSongs } = APIAction("songs", SONGS_REQUEST, SONGS_SUCCESS, SONGS_FAILURE);
|
||||||
|
|
||||||
|
export * from "./paginate";
|
||||||
|
|
|
@ -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 React, { Component, PropTypes } from "react";
|
||||||
|
import Immutable from "immutable";
|
||||||
|
|
||||||
import FilterablePaginatedGrid from "./elements/Grid";
|
import FilterablePaginatedGrid from "./elements/Grid";
|
||||||
|
|
||||||
export default class Albums extends Component {
|
export default class Albums extends Component {
|
||||||
render () {
|
render () {
|
||||||
|
const grid = {
|
||||||
|
isFetching: this.props.isFetching,
|
||||||
|
items: this.props.albums,
|
||||||
|
itemsLabel: "app.common.album",
|
||||||
|
subItemsType: "tracks",
|
||||||
|
subItemsLabel: "app.common.track"
|
||||||
|
};
|
||||||
return (
|
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 = {
|
||||||
albums: PropTypes.array.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
albumsTotalCount: PropTypes.number.isRequired,
|
albums: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||||
albumsPerPage: PropTypes.number.isRequired,
|
pagination: PropTypes.object.isRequired,
|
||||||
currentPage: PropTypes.number.isRequired,
|
|
||||||
location: PropTypes.object.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
|
import Immutable from "immutable";
|
||||||
|
|
||||||
import FilterablePaginatedGrid from "./elements/Grid";
|
import FilterablePaginatedGrid from "./elements/Grid";
|
||||||
|
|
||||||
export default class Artists extends Component {
|
class Artists extends Component {
|
||||||
render () {
|
render () {
|
||||||
|
const grid = {
|
||||||
|
isFetching: this.props.isFetching,
|
||||||
|
items: this.props.artists,
|
||||||
|
itemsLabel: "app.common.artist",
|
||||||
|
subItemsType: "albums",
|
||||||
|
subItemsLabel: "app.common.album"
|
||||||
|
};
|
||||||
return (
|
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 = {
|
||||||
artists: PropTypes.array.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
artistsTotalCount: PropTypes.number.isRequired,
|
artists: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||||
artistsPerPage: PropTypes.number.isRequired,
|
pagination: PropTypes.object.isRequired,
|
||||||
currentPage: PropTypes.number.isRequired,
|
|
||||||
location: PropTypes.object.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Artists;
|
||||||
|
|
|
@ -2,12 +2,14 @@ import React, { Component, PropTypes } from "react";
|
||||||
import CSSModules from "react-css-modules";
|
import CSSModules from "react-css-modules";
|
||||||
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { i18nRecord } from "../models/i18n";
|
||||||
import { messagesMap } from "../utils";
|
import { messagesMap } from "../utils";
|
||||||
|
import APIMessages from "../locales/messagesDescriptors/api";
|
||||||
import messages from "../locales/messagesDescriptors/Login";
|
import messages from "../locales/messagesDescriptors/Login";
|
||||||
|
|
||||||
import css from "../styles/Login.scss";
|
import css from "../styles/Login.scss";
|
||||||
|
|
||||||
const loginMessages = defineMessages(messagesMap(messages));
|
const loginMessages = defineMessages(messagesMap(Array.concat([], APIMessages, messages)));
|
||||||
|
|
||||||
class LoginFormCSSIntl extends Component {
|
class LoginFormCSSIntl extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
|
@ -58,13 +60,17 @@ class LoginFormCSSIntl extends Component {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
var infoMessage = "";
|
var infoMessage = this.props.info;
|
||||||
if (typeof this.props.info === "object") {
|
if (this.props.info && this.props.info instanceof i18nRecord) {
|
||||||
infoMessage = (
|
infoMessage = (
|
||||||
<FormattedMessage {...loginMessages[this.props.info.id]} values={ this.props.info.values} />
|
<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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -73,7 +79,7 @@ class LoginFormCSSIntl extends Component {
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="alert alert-danger" id="loginFormError">
|
<div className="alert alert-danger" id="loginFormError">
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,8 +141,8 @@ LoginFormCSSIntl.propTypes = {
|
||||||
rememberMe: PropTypes.bool,
|
rememberMe: PropTypes.bool,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
isAuthenticating: PropTypes.bool,
|
isAuthenticating: PropTypes.bool,
|
||||||
error: PropTypes.string,
|
error: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(i18nRecord)]),
|
||||||
info: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
info: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(i18nRecord)]),
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
|
||||||
import { Link} from "react-router";
|
import { Link} from "react-router";
|
||||||
import CSSModules from "react-css-modules";
|
import CSSModules from "react-css-modules";
|
||||||
import { defineMessages, FormattedMessage } from "react-intl";
|
import { defineMessages, FormattedMessage } from "react-intl";
|
||||||
|
import Immutable from "immutable";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
import FilterBar from "./elements/FilterBar";
|
import FilterBar from "./elements/FilterBar";
|
||||||
|
@ -25,7 +26,7 @@ export class SongsTableRow extends Component {
|
||||||
<td></td>
|
<td></td>
|
||||||
<td className="title">{this.props.song.name}</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={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="genre">{this.props.song.genre}</td>
|
||||||
<td className="length">{length}</td>
|
<td className="length">{length}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -58,6 +59,15 @@ class SongsTableCSS extends Component {
|
||||||
displayedSongs.forEach(function (song) {
|
displayedSongs.forEach(function (song) {
|
||||||
rows.push(<SongsTableRow song={song} key={song.id} />);
|
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 (
|
return (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table table-hover" styleName="songs">
|
<table className="table table-hover" styleName="songs">
|
||||||
|
@ -67,10 +77,10 @@ class SongsTableCSS extends Component {
|
||||||
<th>
|
<th>
|
||||||
<FormattedMessage {...songsMessages["app.songs.title"]} />
|
<FormattedMessage {...songsMessages["app.songs.title"]} />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th className="text-capitalize">
|
||||||
<FormattedMessage {...songsMessages["app.common.artist"]} values={{itemCount: 1}} />
|
<FormattedMessage {...songsMessages["app.common.artist"]} values={{itemCount: 1}} />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th className="text-capitalize">
|
||||||
<FormattedMessage {...songsMessages["app.common.album"]} values={{itemCount: 1}} />
|
<FormattedMessage {...songsMessages["app.common.album"]} values={{itemCount: 1}} />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
|
@ -83,13 +93,14 @@ class SongsTableCSS extends Component {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{rows}</tbody>
|
<tbody>{rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{loading}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SongsTableCSS.propTypes = {
|
SongsTableCSS.propTypes = {
|
||||||
songs: PropTypes.array.isRequired,
|
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||||
filterText: PropTypes.string
|
filterText: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,21 +124,18 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const nPages = Math.ceil(this.props.songsTotalCount / this.props.songsPerPage);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
||||||
<SongsTable songs={this.props.songs} filterText={this.state.filterText} />
|
<SongsTable isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} />
|
||||||
<Pagination nPages={nPages} currentPage={this.props.currentPage} location={this.props.location} />
|
<Pagination {...this.props.pagination} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterablePaginatedSongsTable.propTypes = {
|
FilterablePaginatedSongsTable.propTypes = {
|
||||||
songs: PropTypes.array.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
songsTotalCount: PropTypes.number.isRequired,
|
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||||
songsPerPage: PropTypes.number.isRequired,
|
pagination: PropTypes.object.isRequired
|
||||||
currentPage: PropTypes.number.isRequired,
|
|
||||||
location: PropTypes.object.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
import { Link} from "react-router";
|
import { Link} from "react-router";
|
||||||
import CSSModules from "react-css-modules";
|
import CSSModules from "react-css-modules";
|
||||||
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||||
|
import Immutable from "immutable";
|
||||||
import imagesLoaded from "imagesloaded";
|
import imagesLoaded from "imagesloaded";
|
||||||
import Isotope from "isotope-layout";
|
import Isotope from "isotope-layout";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
@ -8,27 +10,30 @@ import shallowCompare from "react-addons-shallow-compare";
|
||||||
|
|
||||||
import FilterBar from "./FilterBar";
|
import FilterBar from "./FilterBar";
|
||||||
import Pagination from "./Pagination";
|
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";
|
import css from "../../styles/elements/Grid.scss";
|
||||||
|
|
||||||
class GridItemCSS extends Component {
|
const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||||
|
|
||||||
|
class GridItemCSSIntl extends Component {
|
||||||
render () {
|
render () {
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
var nSubItems = this.props.item[this.props.subItemsType];
|
var nSubItems = this.props.item[this.props.subItemsType];
|
||||||
if (Array.isArray(nSubItems)) {
|
if (Array.isArray(nSubItems)) {
|
||||||
nSubItems = nSubItems.length;
|
nSubItems = nSubItems.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: i18n
|
var subItemsLabel = formatMessage(gridMessages[this.props.subItemsLabel], { itemCount: nSubItems });
|
||||||
var subItemsLabel = this.props.subItemsType;
|
|
||||||
if (nSubItems < 2) {
|
|
||||||
subItemsLabel = subItemsLabel.rstrip("s");
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id;
|
||||||
|
|
||||||
// TODO: i18n
|
const title = formatMessage(gridMessages["app.grid.goTo" + this.props.item.type.capitalize() + "Page"]);
|
||||||
const title = "Go to " + this.props.itemsType.rstrip("s") + " page";
|
|
||||||
return (
|
return (
|
||||||
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
|
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
|
||||||
<div className="grid-item-content text-center">
|
<div className="grid-item-content text-center">
|
||||||
|
@ -41,13 +46,15 @@ class GridItemCSS extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GridItemCSS.propTypes = {
|
GridItemCSSIntl.propTypes = {
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
itemsType: PropTypes.string.isRequired,
|
itemsLabel: PropTypes.string.isRequired,
|
||||||
subItemsType: 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. */
|
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);
|
(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
|
// 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
|
// 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)));
|
removeKeys.forEach(removeKey => this.iso.remove(document.getElementById(removeKey)));
|
||||||
this.iso.arrange();
|
this.iso.arrange();
|
||||||
}
|
}
|
||||||
if (addKeys.length > 0) {
|
if (addKeys.count() > 0) {
|
||||||
const itemsToAdd = addKeys.map((addKey) => document.getElementById(addKey));
|
const itemsToAdd = addKeys.map((addKey) => document.getElementById(addKey)).toArray();
|
||||||
this.iso.addItems(itemsToAdd);
|
this.iso.addItems(itemsToAdd);
|
||||||
this.iso.arrange();
|
this.iso.arrange();
|
||||||
}
|
}
|
||||||
|
@ -178,18 +185,32 @@ export class Grid extends Component {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
var gridItems = [];
|
var gridItems = [];
|
||||||
const itemsType = this.props.itemsType;
|
const itemsLabel = this.props.itemsLabel;
|
||||||
const subItemsType = this.props.subItemsType;
|
const subItemsType = this.props.subItemsType;
|
||||||
|
const subItemsLabel = this.props.subItemsLabel;
|
||||||
this.props.items.forEach(function (item) {
|
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 (
|
return (
|
||||||
<div className="row">
|
<div>
|
||||||
<div className="grid" ref="grid">
|
{ loading }
|
||||||
{/* Sizing element */}
|
<div className="row">
|
||||||
<div className="grid-sizer col-xs-6 col-sm-3"></div>
|
<div className="grid" ref="grid">
|
||||||
{/* Other items */}
|
{/* Sizing element */}
|
||||||
{ gridItems }
|
<div className="grid-sizer col-xs-6 col-sm-3"></div>
|
||||||
|
{/* Other items */}
|
||||||
|
{ gridItems }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -197,8 +218,9 @@ export class Grid extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Grid.propTypes = {
|
Grid.propTypes = {
|
||||||
items: PropTypes.array.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
itemsType: PropTypes.string.isRequired,
|
items: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||||
|
itemsLabel: PropTypes.string.isRequired,
|
||||||
subItemsType: PropTypes.string.isRequired,
|
subItemsType: PropTypes.string.isRequired,
|
||||||
filterText: PropTypes.string
|
filterText: PropTypes.string
|
||||||
};
|
};
|
||||||
|
@ -220,23 +242,17 @@ export default class FilterablePaginatedGrid extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const nPages = Math.ceil(this.props.itemsTotalCount / this.props.itemsPerPage);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
<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} />
|
<Grid filterText={this.state.filterText} {...this.props.grid} />
|
||||||
<Pagination nPages={nPages} currentPage={this.props.currentPage} location={this.props.location} />
|
<Pagination {...this.props.pagination} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterablePaginatedGrid.propTypes = {
|
FilterablePaginatedGrid.propTypes = {
|
||||||
items: PropTypes.array.isRequired,
|
grid: PropTypes.object.isRequired,
|
||||||
itemsTotalCount: PropTypes.number.isRequired,
|
pagination: PropTypes.object.isRequired
|
||||||
itemsPerPage: PropTypes.number.isRequired,
|
|
||||||
currentPage: PropTypes.number.isRequired,
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
itemsType: PropTypes.string.isRequired,
|
|
||||||
subItemsType: PropTypes.string.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
import { Link, withRouter } from "react-router";
|
import { Link } from "react-router";
|
||||||
import CSSModules from "react-css-modules";
|
import CSSModules from "react-css-modules";
|
||||||
import { defineMessages, injectIntl, intlShape, FormattedMessage, FormattedHTMLMessage } from "react-intl";
|
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)));
|
const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||||
|
|
||||||
class PaginationCSSIntl extends Component {
|
class PaginationCSSIntl extends Component {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.buildLinkTo.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
computePaginationBounds(currentPage, nPages, maxNumberPagesShown=5) {
|
computePaginationBounds(currentPage, nPages, maxNumberPagesShown=5) {
|
||||||
// Taken from http://stackoverflow.com/a/8608998/2626416
|
// Taken from http://stackoverflow.com/a/8608998/2626416
|
||||||
var lowerLimit = currentPage;
|
var lowerLimit = currentPage;
|
||||||
|
@ -39,18 +34,12 @@ class PaginationCSSIntl extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLinkTo(pageNumber) {
|
goToPage(ev) {
|
||||||
return {
|
ev.preventDefault();
|
||||||
pathname: this.props.location.pathname,
|
|
||||||
query: Object.assign({}, this.props.location.query, { page: pageNumber })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage() {
|
|
||||||
const pageNumber = parseInt(this.refs.pageInput.value);
|
const pageNumber = parseInt(this.refs.pageInput.value);
|
||||||
$(this.refs.paginationModal).modal("hide");
|
$(this.refs.paginationModal).modal("hide");
|
||||||
if (pageNumber) {
|
if (pageNumber) {
|
||||||
this.props.router.push(this.buildLinkTo(pageNumber));
|
this.props.goToPage(pageNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +68,7 @@ class PaginationCSSIntl extends Component {
|
||||||
// Push first page
|
// Push first page
|
||||||
pagesButton.push(
|
pagesButton.push(
|
||||||
<li className="page-item" key={key}>
|
<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 }} />
|
<FormattedHTMLMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: 1 }} />
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -106,7 +95,7 @@ class PaginationCSSIntl extends Component {
|
||||||
const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: i });
|
const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: i });
|
||||||
pagesButton.push(
|
pagesButton.push(
|
||||||
<li className={className} key={key}>
|
<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 }} />
|
<FormattedHTMLMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: i }} />
|
||||||
{currentSpan}
|
{currentSpan}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -128,7 +117,7 @@ class PaginationCSSIntl extends Component {
|
||||||
// Push last page
|
// Push last page
|
||||||
pagesButton.push(
|
pagesButton.push(
|
||||||
<li className="page-item" key={key}>
|
<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 }} />
|
<FormattedHTMLMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: this.props.nPages }} />
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -152,8 +141,8 @@ class PaginationCSSIntl extends Component {
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<form>
|
<form onSubmit={this.goToPage.bind(this)}>
|
||||||
<input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label={formatMessage(paginationMessages["app.pagination.pageToGoTo"])} />
|
<input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label={formatMessage(paginationMessages["app.pagination.pageToGoTo"])} autoFocus />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
|
@ -176,9 +165,10 @@ class PaginationCSSIntl extends Component {
|
||||||
|
|
||||||
PaginationCSSIntl.propTypes = {
|
PaginationCSSIntl.propTypes = {
|
||||||
currentPage: PropTypes.number.isRequired,
|
currentPage: PropTypes.number.isRequired,
|
||||||
location: PropTypes.object.isRequired,
|
goToPage: PropTypes.func.isRequired,
|
||||||
|
buildLinkToPage: PropTypes.func.isRequired,
|
||||||
nPages: PropTypes.number.isRequired,
|
nPages: PropTypes.number.isRequired,
|
||||||
intl: intlShape.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>
|
<li>
|
||||||
<Link to="/artists" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseArtists"])} styleName={isActive.artists}>
|
<Link to="/artists" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseArtists"])} styleName={isActive.artists}>
|
||||||
<span className="glyphicon glyphicon-user" aria-hidden="true"></span>
|
<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}} />
|
<FormattedMessage {...sidebarLayoutMessages["app.common.artist"]} values={{itemCount: 42}} />
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden-md">
|
<span className="hidden-md text-capitalize">
|
||||||
<FormattedMessage {...sidebarLayoutMessages["app.common.artist"]} values={{itemCount: 42}} />
|
<FormattedMessage {...sidebarLayoutMessages["app.common.artist"]} values={{itemCount: 42}} />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -93,8 +93,10 @@ class SidebarLayoutIntl extends Component {
|
||||||
<li>
|
<li>
|
||||||
<Link to="/albums" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseAlbums"])} styleName={isActive.albums}>
|
<Link to="/albums" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseAlbums"])} styleName={isActive.albums}>
|
||||||
<span className="glyphicon glyphicon-cd" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-cd" aria-hidden="true"></span>
|
||||||
<span className="sr-only"><FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} /></span>
|
<span className="sr-only text-capitalize">
|
||||||
<span className="hidden-md">
|
<FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} />
|
||||||
|
</span>
|
||||||
|
<span className="hidden-md text-capitalize">
|
||||||
<FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} />
|
<FormattedMessage {...sidebarLayoutMessages["app.common.album"]} values={{itemCount: 42}} />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -102,11 +104,11 @@ class SidebarLayoutIntl extends Component {
|
||||||
<li>
|
<li>
|
||||||
<Link to="/songs" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseSongs"])} styleName={isActive.songs}>
|
<Link to="/songs" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseSongs"])} styleName={isActive.songs}>
|
||||||
<span className="glyphicon glyphicon-music" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-music" aria-hidden="true"></span>
|
||||||
<span className="sr-only">
|
<span className="sr-only text-capitalize">
|
||||||
<FormattedMessage {...sidebarLayoutMessages["app.common.song"]} values={{itemCount: 42}} />
|
<FormattedMessage {...sidebarLayoutMessages["app.common.track"]} values={{itemCount: 42}} />
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden-md">
|
<span className="hidden-md text-capitalize">
|
||||||
<FormattedMessage {...sidebarLayoutMessages["app.common.song"]} values={{itemCount: 42}} />
|
<FormattedMessage {...sidebarLayoutMessages["app.common.track"]} values={{itemCount: 42}} />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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
|
//# sourceMappingURL=fix.ie9.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,15 +1,22 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"app.common.album": "{itemCount, plural, one {Album} other {Albums}}", // Album
|
"app.api.invalidResponse": "Invalid response text.", // Invalid response from the API
|
||||||
"app.common.artist": "{itemCount, plural, one {Artist} other {Artists}}", // Artist
|
"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.cancel": "Cancel", // Cancel
|
||||||
"app.common.close": "Close", // Close
|
"app.common.close": "Close", // Close
|
||||||
"app.common.go": "Go", // Go
|
"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.filter": "Filter…", // Filtering input placeholder
|
||||||
"app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar
|
"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.byebye": "See you soon!", // Info message on successful logout
|
||||||
"app.login.connecting": "Connecting…", // Info message while trying to connect
|
"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.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.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.password": "Password", // Password input placeholder
|
||||||
"app.login.rememberMe": "Remember me", // Remember me checkbox label
|
"app.login.rememberMe": "Remember me", // Remember me checkbox label
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"app.common.album": "{itemCount, plural, one {Album} other {Albums}}", // Albums
|
"app.api.invalidResponse": "Réponse invalide reçue.", // Invalid response from the API
|
||||||
"app.common.artist": "{itemCount, plural, one {Artiste} other {Artistes}}", // Artists
|
"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.cancel": "Annuler", // Cancel
|
||||||
"app.common.close": "Fermer", // Close
|
"app.common.close": "Fermer", // Close
|
||||||
"app.common.go": "Aller", // Go
|
"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.filter": "Filtrer…", // Filtering input placeholder
|
||||||
"app.filter.whatAreWeListeningToToday": "Que voulez-vous écouter aujourd'hui\u00a0?", // Description for the filter bar
|
"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.byebye": "À bientôt\u00a0!", // Info message on successful logout
|
||||||
"app.login.connecting": "Connexion…", // Info message while trying to connect
|
"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.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.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.password": "Mot de passe", // Password input placeholder
|
||||||
"app.login.rememberMe": "Se souvenir", // Remember me checkbox label
|
"app.login.rememberMe": "Se souvenir", // Remember me checkbox label
|
||||||
|
|
|
@ -45,6 +45,11 @@ const messages = [
|
||||||
id: "app.login.byebye",
|
id: "app.login.byebye",
|
||||||
defaultMessage: "See you soon!",
|
defaultMessage: "See you soon!",
|
||||||
description: "Info message on successful logout"
|
description: "Info message on successful logout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.login.expired",
|
||||||
|
defaultMessage: "Your session expired… =(",
|
||||||
|
description: "Error message on expired session"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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",
|
id: "app.common.artist",
|
||||||
description: "Artist",
|
description: "Artist",
|
||||||
defaultMessage: "{itemCount, plural, one {Artist} other {Artists}}"
|
defaultMessage: "{itemCount, plural, one {artist} other {artists}}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "app.common.album",
|
id: "app.common.album",
|
||||||
description: "Album",
|
description: "Album",
|
||||||
defaultMessage: "{itemCount, plural, one {Album} other {Albums}}"
|
defaultMessage: "{itemCount, plural, one {album} other {albums}}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "app.common.song",
|
id: "app.common.track",
|
||||||
description: "Song",
|
description: "Track",
|
||||||
defaultMessage: "{itemCount, plural, one {Song} other {Songs}}"
|
defaultMessage: "{itemCount, plural, one {track} other {tracks}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.common.loading",
|
||||||
|
description: "Loading indicator",
|
||||||
|
defaultMessage: "Loading…"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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 X2JS from "x2js";
|
||||||
|
|
||||||
import { assembleURLAndParams } from "../utils";
|
import { assembleURLAndParams } from "../utils";
|
||||||
|
import { i18nRecord } from "../models/i18n";
|
||||||
|
|
||||||
export const API_VERSION = 350001; /** API version to use. */
|
export const API_VERSION = 350001; /** API version to use. */
|
||||||
export const BASE_API_PATH = "/server/xml.server.php"; /** Base API path after endpoint. */
|
export const BASE_API_PATH = "/server/xml.server.php"; /** Base API path after endpoint. */
|
||||||
|
@ -26,7 +27,10 @@ function _parseToJSON (responseText) {
|
||||||
if (responseText) {
|
if (responseText) {
|
||||||
return x2js.xml_str2json(responseText).root;
|
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) {
|
function _checkAPIErrors (jsonData) {
|
||||||
|
@ -34,7 +38,10 @@ function _checkAPIErrors (jsonData) {
|
||||||
return Promise.reject(jsonData.error.cdata + " (" + jsonData.error.code + ")");
|
return Promise.reject(jsonData.error.cdata + " (" + jsonData.error.code + ")");
|
||||||
} else if (!jsonData) {
|
} else if (!jsonData) {
|
||||||
// No data returned
|
// No data returned
|
||||||
return Promise.reject("Empty response");
|
return Promise.reject(new i18nRecord({
|
||||||
|
id: "app.api.emptyResponse",
|
||||||
|
values: {}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return jsonData;
|
return jsonData;
|
||||||
}
|
}
|
||||||
|
@ -52,6 +59,8 @@ function _uglyFixes (endpoint, token) {
|
||||||
|
|
||||||
var _uglyFixesSongs = function (songs) {
|
var _uglyFixesSongs = function (songs) {
|
||||||
for (var i = 0; i < songs.length; i++) {
|
for (var i = 0; i < songs.length; i++) {
|
||||||
|
// Add song type
|
||||||
|
songs[i].type = "track";
|
||||||
// Fix for name becoming title in songs objects
|
// Fix for name becoming title in songs objects
|
||||||
songs[i].name = songs[i].title;
|
songs[i].name = songs[i].title;
|
||||||
// Fix for length being time in songs objects
|
// Fix for length being time in songs objects
|
||||||
|
@ -66,6 +75,9 @@ function _uglyFixes (endpoint, token) {
|
||||||
|
|
||||||
var _uglyFixesAlbums = function (albums) {
|
var _uglyFixesAlbums = function (albums) {
|
||||||
for (var i = 0; i < albums.length; i++) {
|
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
|
// Fix for absence of distinction between disks in the same album
|
||||||
if (albums[i].disk > 1) {
|
if (albums[i].disk > 1) {
|
||||||
albums[i].name = albums[i].name + " [Disk " + albums[i].disk + "]";
|
albums[i].name = albums[i].name + " [Disk " + albums[i].disk + "]";
|
||||||
|
@ -117,6 +129,9 @@ function _uglyFixes (endpoint, token) {
|
||||||
|
|
||||||
if (jsonData.artist) {
|
if (jsonData.artist) {
|
||||||
for (var i = 0; i < jsonData.artist.length; i++) {
|
for (var i = 0; i < jsonData.artist.length; i++) {
|
||||||
|
// Add artist type
|
||||||
|
jsonData.artist[i].type = "artist";
|
||||||
|
|
||||||
// Fix for artists art not included
|
// 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;
|
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(_checkHTTPStatus)
|
||||||
.then (response => response.text())
|
.then (response => response.text())
|
||||||
.then(_parseToJSON)
|
.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.
|
// Action key that carries API call info interpreted by this Redux middleware.
|
||||||
|
@ -239,7 +254,7 @@ export default store => next => reduxAction => {
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
if (failureDispatch) {
|
if (failureDispatch) {
|
||||||
if (typeof error !== "string") {
|
if (error instanceof Error) {
|
||||||
error = error.message;
|
error = error.message;
|
||||||
}
|
}
|
||||||
store.dispatch(failureDispatch(error));
|
store.dispatch(failureDispatch(error));
|
||||||
|
|
|
@ -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
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Immutable from "immutable";
|
||||||
|
|
||||||
|
export const i18nRecord = new Immutable.Record({
|
||||||
|
id: null,
|
||||||
|
values: new Immutable.Map()
|
||||||
|
});
|
|
@ -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 {LOGIN_USER_REQUEST, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, LOGOUT_USER} from "../actions";
|
||||||
import { createReducer } from "../utils";
|
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) {
|
if (initialToken) {
|
||||||
initialToken.expires = new Date(initialToken.expires);
|
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 || {
|
* Reducers
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createReducer(initialState, {
|
export default createReducer(initialState, {
|
||||||
[LOGIN_USER_REQUEST]: (state) => {
|
[LOGIN_USER_REQUEST]: () => {
|
||||||
return Object.assign({}, state, {
|
return new stateRecord({
|
||||||
isAuthenticating: true,
|
isAuthenticating: true,
|
||||||
info: {
|
info: new i18nRecord({
|
||||||
id: "app.login.connecting",
|
id: "app.login.connecting",
|
||||||
values: {}
|
values: {}
|
||||||
},
|
})
|
||||||
error: "",
|
|
||||||
timerID: null
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[LOGIN_USER_SUCCESS]: (state, payload) => {
|
[LOGIN_USER_SUCCESS]: (state, payload) => {
|
||||||
return Object.assign({}, state, {
|
return new stateRecord({
|
||||||
isAuthenticating: false,
|
"isAuthenticated": true,
|
||||||
isAuthenticated: true,
|
"token": new tokenRecord(payload.token),
|
||||||
token: payload.token,
|
"username": payload.username,
|
||||||
username: payload.username,
|
"endpoint": payload.endpoint,
|
||||||
endpoint: payload.endpoint,
|
"rememberMe": payload.rememberMe,
|
||||||
rememberMe: payload.rememberMe,
|
"info": new i18nRecord({
|
||||||
info: {
|
|
||||||
id: "app.login.success",
|
id: "app.login.success",
|
||||||
values: { username: payload.username}
|
values: {username: payload.username}
|
||||||
},
|
}),
|
||||||
error: "",
|
"timerID": payload.timerID
|
||||||
timerID: payload.timerID
|
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
[LOGIN_USER_FAILURE]: (state, payload) => {
|
[LOGIN_USER_FAILURE]: (state, payload) => {
|
||||||
return Object.assign({}, state, {
|
return new stateRecord({
|
||||||
isAuthenticating: false,
|
"error": payload.error
|
||||||
isAuthenticated: false,
|
|
||||||
token: initialState.token,
|
|
||||||
username: "",
|
|
||||||
endpoint: "",
|
|
||||||
rememberMe: false,
|
|
||||||
info: "",
|
|
||||||
error: payload.error,
|
|
||||||
timerID: 0
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[LOGOUT_USER]: (state) => {
|
[LOGOUT_USER]: () => {
|
||||||
return Object.assign({}, state, {
|
return new stateRecord({
|
||||||
isAuthenticated: false,
|
info: new i18nRecord({
|
||||||
token: initialState.token,
|
|
||||||
username: "",
|
|
||||||
endpoint: "",
|
|
||||||
rememberMe: false,
|
|
||||||
info: {
|
|
||||||
id: "app.login.byebye",
|
id: "app.login.byebye",
|
||||||
values: {}
|
values: {}
|
||||||
},
|
})
|
||||||
error: "",
|
|
||||||
timerID: 0
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
|
import Immutable from "immutable";
|
||||||
|
|
||||||
import { createReducer } from "../utils";
|
import { createReducer } from "../utils";
|
||||||
|
import { stateRecord } from "../models/paginate";
|
||||||
|
|
||||||
export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */
|
const initialState = new stateRecord();
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
isFetching: false,
|
|
||||||
items: [],
|
|
||||||
total: 0,
|
|
||||||
error: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Creates a reducer managing pagination, given the action types to handle,
|
// Creates a reducer managing pagination, given the action types to handle,
|
||||||
// and a function telling how to extract the key from an action.
|
// and a function telling how to extract the key from an action.
|
||||||
|
@ -23,24 +19,28 @@ export default function paginate(types) {
|
||||||
|
|
||||||
return createReducer(initialState, {
|
return createReducer(initialState, {
|
||||||
[requestType]: (state) => {
|
[requestType]: (state) => {
|
||||||
return Object.assign({}, state, {
|
return (
|
||||||
isFetching: true,
|
state
|
||||||
error: "",
|
.set("isFetching", true)
|
||||||
});
|
.set("error", null)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[successType]: (state, payload) => {
|
[successType]: (state, payload) => {
|
||||||
return Object.assign({}, state, {
|
return (
|
||||||
isFetching: false,
|
state
|
||||||
items: payload.items,
|
.set("isFetching", false)
|
||||||
total: payload.total,
|
.set("items", new Immutable.List(payload.items))
|
||||||
error: ""
|
.set("error", null)
|
||||||
});
|
.set("nPages", payload.nPages)
|
||||||
|
.set("currentPage", payload.currentPage)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[failureType]: (state, payload) => {
|
[failureType]: (state, payload) => {
|
||||||
return Object.assign({}, state, {
|
return (
|
||||||
isFetching: false,
|
state
|
||||||
error: payload.error
|
.set("isFetching", false)
|
||||||
});
|
.set("error", payload.error)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 "./locale";
|
||||||
export * from "./misc";
|
export * from "./misc";
|
||||||
|
export * from "./pagination";
|
||||||
export * from "./reducers";
|
export * from "./reducers";
|
||||||
export * from "./url";
|
export * from "./url";
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
export function getBrowserLocale () {
|
export function getBrowserLocales () {
|
||||||
var lang;
|
var langs;
|
||||||
|
|
||||||
if (navigator.languages) {
|
if (navigator.languages) {
|
||||||
// chrome does not currently set navigator.language correctly https://code.google.com/p/chromium/issues/detail?id=101138
|
// 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
|
// but it does set the first element of navigator.languages correctly
|
||||||
lang = navigator.languages[0];
|
langs = navigator.languages;
|
||||||
} else if (navigator.userLanguage) {
|
} else if (navigator.userLanguage) {
|
||||||
// IE only
|
// IE only
|
||||||
lang = navigator.userLanguage;
|
langs = [navigator.userLanguage];
|
||||||
} else {
|
} else {
|
||||||
// as of this writing the latest version of firefox + safari set this correctly
|
// 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
|
// Some browsers does not return uppercase for second part
|
||||||
var locale = lang.split("-");
|
var locales = langs.map(function (lang) {
|
||||||
locale = locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
|
var locale = lang.split("-");
|
||||||
|
return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
|
||||||
|
});
|
||||||
|
|
||||||
return locale;
|
return locales;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function messagesMap(messagesDescriptorsArray) {
|
export function messagesMap(messagesDescriptorsArray) {
|
||||||
|
|
|
@ -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 { bindActionCreators } from "redux";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { defineMessages, injectIntl, intlShape } from "react-intl";
|
||||||
|
|
||||||
import * as actionCreators from "../actions";
|
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";
|
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 () {
|
componentWillMount () {
|
||||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||||
// Load the data
|
// Load the data
|
||||||
|
@ -24,20 +30,41 @@ export class AlbumsPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 (
|
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) => ({
|
const mapStateToProps = (state) => ({
|
||||||
|
isFetching: state.pagination.albums.isFetching,
|
||||||
|
error: state.pagination.albums.error,
|
||||||
albumsList: state.pagination.albums.items,
|
albumsList: state.pagination.albums.items,
|
||||||
albumsCount: state.pagination.albums.total
|
currentPage: state.pagination.albums.currentPage,
|
||||||
|
nPages: state.pagination.albums.nPages
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
actions: bindActionCreators(actionCreators, 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 { bindActionCreators } from "redux";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { defineMessages, injectIntl, intlShape } from "react-intl";
|
||||||
|
|
||||||
import * as actionCreators from "../actions";
|
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";
|
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 () {
|
componentWillMount () {
|
||||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||||
// Load the data
|
// Load the data
|
||||||
|
@ -24,20 +30,40 @@ export class ArtistsPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 (
|
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) => ({
|
const mapStateToProps = (state) => ({
|
||||||
|
isFetching: state.pagination.artists.isFetching,
|
||||||
artistsList: state.pagination.artists.items,
|
artistsList: state.pagination.artists.items,
|
||||||
artistsCount: state.pagination.artists.total
|
currentPage: state.pagination.artists.currentPage,
|
||||||
|
nPages: state.pagination.artists.nPages
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
actions: bindActionCreators(actionCreators, 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 { bindActionCreators } from "redux";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { defineMessages, injectIntl, intlShape } from "react-intl";
|
||||||
|
|
||||||
import * as actionCreators from "../actions";
|
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";
|
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 () {
|
componentWillMount () {
|
||||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||||
// Load the data
|
// Load the data
|
||||||
|
@ -24,20 +30,40 @@ export class SongsPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 (
|
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) => ({
|
const mapStateToProps = (state) => ({
|
||||||
|
isFetching: state.pagination.songs.isFetching,
|
||||||
songsList: state.pagination.songs.items,
|
songsList: state.pagination.songs.items,
|
||||||
songsCount: state.pagination.songs.total
|
currentPage: state.pagination.songs.currentPage,
|
||||||
|
nPages: state.pagination.songs.nPages
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
actions: bindActionCreators(actionCreators, 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 configureStore from "./app/store/configureStore";
|
||||||
|
|
||||||
import { getBrowserLocale } from "./app/utils";
|
import { getBrowserLocales } from "./app/utils";
|
||||||
import rawMessages from "./app/locales";
|
import rawMessages from "./app/locales";
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
@ -22,8 +22,17 @@ export const rootElement = document.getElementById("root");
|
||||||
// i18n
|
// i18n
|
||||||
export const onWindowIntl = () => {
|
export const onWindowIntl = () => {
|
||||||
addLocaleData([...en, ...fr]);
|
addLocaleData([...en, ...fr]);
|
||||||
const locale = getBrowserLocale(); // TODO: Get navigator.languages as array and match
|
const locales = getBrowserLocales();
|
||||||
var strings = rawMessages[locale] ? rawMessages[locale] : rawMessages["en-US"];
|
|
||||||
|
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);
|
strings = Object.assign(rawMessages["en-US"], strings);
|
||||||
|
|
||||||
let render = () => {
|
let render = () => {
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"html5shiv": "^3.7.3",
|
"html5shiv": "^3.7.3",
|
||||||
"humps": "^1.1.0",
|
"humps": "^1.1.0",
|
||||||
"imagesloaded": "^4.1.0",
|
"imagesloaded": "^4.1.0",
|
||||||
|
"immutable": "^3.8.1",
|
||||||
"intl": "^1.2.4",
|
"intl": "^1.2.4",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"isotope-layout": "^3.0.1",
|
"isotope-layout": "^3.0.1",
|
||||||
|
|
Loading…
Reference in New Issue