diff --git a/src/api/ShlinkApiClient.js b/src/api/ShlinkApiClient.js index 071cf186..2539050b 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/api/ShlinkApiClient.js @@ -1,6 +1,6 @@ import axios from 'axios'; import qs from 'qs'; -import { isEmpty, isNil, pipe, reject } from 'ramda'; +import { isEmpty, isNil, reject } from 'ramda'; const API_VERSION = '1'; @@ -22,11 +22,6 @@ export class ShlinkApiClient { this._apiKey = apiKey; }; - /** - * Returns the list of short URLs - * @param options - * @returns {Promise} - */ listShortUrls = (options = {}) => this._performRequest('/short-codes', 'GET', options) .then(resp => resp.data.shortUrls) @@ -39,6 +34,11 @@ export class ShlinkApiClient { .catch(e => this._handleAuthError(e, this.listShortUrls, [filteredOptions])); }; + getShortUrlVisits = shortCode => + this._performRequest(`/short-codes/${shortCode}/visits`, 'GET') + .then(resp => resp.data.visits.data) + .catch(e => this._handleAuthError(e, this.listShortUrls, [shortCode])); + _performRequest = async (url, method = 'GET', params = {}, data = {}) => { if (isEmpty(this._token)) { this._token = await this._authenticate(); diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 64caa4cd..c8cf0024 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -9,7 +9,8 @@ import AsideMenu from './AsideMenu'; import { pick } from 'ramda'; export class MenuLayout extends React.Component { - componentDidMount() { + // FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered + componentWillMount() { const { serverId } = this.props.match.params; this.props.selectServer(serverId); } diff --git a/src/short-urls/SearchBar.js b/src/short-urls/SearchBar.js index 859f5d6c..7524214f 100644 --- a/src/short-urls/SearchBar.js +++ b/src/short-urls/SearchBar.js @@ -2,7 +2,7 @@ import searchIcon from '@fortawesome/fontawesome-free-solid/faSearch'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; import { connect } from 'react-redux'; -import { updateShortUrlsList } from './reducers/shortUrlsList'; +import { listShortUrls } from './reducers/shortUrlsList'; import './SearchBar.scss'; import { pick } from 'ramda'; @@ -47,10 +47,10 @@ export class SearchBar extends React.Component { resetTimer(); this.timer = setTimeout(() => { - this.props.updateShortUrlsList({ ...this.props.shortUrlsListParams, searchTerm }); + this.props.listShortUrls({ ...this.props.shortUrlsListParams, searchTerm }); resetTimer(); }, 500); } } -export default connect(pick(['shortUrlsListParams']), { updateShortUrlsList })(SearchBar); +export default connect(pick(['shortUrlsListParams']), { listShortUrls })(SearchBar); diff --git a/src/short-urls/ShortUrlVisits.js b/src/short-urls/ShortUrlVisits.js index 39eac8c7..fbbaf082 100644 --- a/src/short-urls/ShortUrlVisits.js +++ b/src/short-urls/ShortUrlVisits.js @@ -1,11 +1,32 @@ import React from 'react'; import { connect } from 'react-redux'; +import { pick } from 'ramda'; +import { getShortUrlVisits } from './reducers/shortUrlVisits'; export class ShortUrlsVisits extends React.Component { - render() { + componentDidMount() { const { match: { params } } = this.props; - return
Visits for {params.shortCode}
; + this.props.getShortUrlVisits(params.shortCode); + } + + render() { + const { match: { params }, selectedServer } = this.props; + const serverUrl = selectedServer ? selectedServer.url : ''; + const shortUrl = `${serverUrl}/${params.shortCode}`; + + return ( +
+
+
+

Visit stats for {shortUrl}

+ {/* TODO Once Shlink's API allows it, add total visits counter, long URL and creation time */} +
+
+
+ ); } } -export default connect()(ShortUrlsVisits); +export default connect(pick(['selectedServer']), { + getShortUrlVisits +})(ShortUrlsVisits); diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js index fa04ede3..1bd00332 100644 --- a/src/short-urls/ShortUrlsList.js +++ b/src/short-urls/ShortUrlsList.js @@ -12,8 +12,8 @@ import { pick } from 'ramda'; export class ShortUrlsList extends React.Component { refreshList = extraParams => { - const { listShortUrls, shortUrlsListParams, match: { params } } = this.props; - listShortUrls(params.serverId, { + const { listShortUrls, shortUrlsListParams } = this.props; + listShortUrls({ ...shortUrlsListParams, ...extraParams }); diff --git a/src/short-urls/reducers/shortUrlVisits.js b/src/short-urls/reducers/shortUrlVisits.js new file mode 100644 index 00000000..a484a4db --- /dev/null +++ b/src/short-urls/reducers/shortUrlVisits.js @@ -0,0 +1,46 @@ +import ShlinkApiClient from '../../api/ShlinkApiClient'; + +const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; +const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR'; +const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS'; + +const initialState = { + visits: [], + loading: false, + error: false +}; + +export default function dispatch (state = initialState, action) { + switch (action.type) { + case GET_SHORT_URL_VISITS_START: + return { + ...state, + loading: true + }; + case GET_SHORT_URL_VISITS_ERROR: + return { + ...state, + loading: false, + error: true + }; + case GET_SHORT_URL_VISITS: + return { + visits: action.visits, + loading: false, + error: false + }; + default: + return state; + } +} + +export const getShortUrlVisits = shortCode => async dispatch => { + dispatch({ type: GET_SHORT_URL_VISITS_START }); + + try { + const visits = await ShlinkApiClient.getShortUrlVisits(shortCode); + dispatch({ visits, type: GET_SHORT_URL_VISITS }); + } catch (e) { + dispatch({ type: GET_SHORT_URL_VISITS_ERROR }); + } +}; diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index e642c57a..674f1c1a 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,10 +1,8 @@ import ShlinkApiClient from '../../api/ShlinkApiClient'; -import ServersService from '../../servers/services/ServersService'; -export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; -export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR'; +const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; +const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR'; export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS'; -export const UPDATE_SHORT_URLS_LIST = LIST_SHORT_URLS; const initialState = { shortUrls: [], @@ -32,16 +30,7 @@ export default function reducer(state = initialState, action) { } } -export const listShortUrls = (serverId, params = {}) => { - // FIXME There should be a way to not need this, however, the active server is set when any route is loaded, in an - // FIXME outer component's componentDidMount, which makes it be invoked after this action - const selectedServer = ServersService.findServerById(serverId); - ShlinkApiClient.setConfig(selectedServer); - - return updateShortUrlsList(params); -}; - -export const updateShortUrlsList = (params = {}) => async dispatch => { +export const listShortUrls = (params = {}) => async dispatch => { dispatch({ type: LIST_SHORT_URLS_START }); try { diff --git a/src/short-urls/reducers/shortUrlsListParams.js b/src/short-urls/reducers/shortUrlsListParams.js index 0aafd86e..4d465e06 100644 --- a/src/short-urls/reducers/shortUrlsListParams.js +++ b/src/short-urls/reducers/shortUrlsListParams.js @@ -1,8 +1,7 @@ -import { UPDATE_SHORT_URLS_LIST, LIST_SHORT_URLS } from './shortUrlsList'; +import { LIST_SHORT_URLS } from './shortUrlsList'; export default function reducer(state = { page: 1 }, action) { switch (action.type) { - case UPDATE_SHORT_URLS_LIST: case LIST_SHORT_URLS: return { ...state, ...action.params }; default: