Created tags list page

This commit is contained in:
Alejandro Celaya 2018-08-16 18:59:00 +02:00
parent 03113583f0
commit 49290b56ee
12 changed files with 150 additions and 8 deletions

View file

@ -49,6 +49,11 @@ export class ShlinkApiClient {
.then(resp => resp.data.tags) .then(resp => resp.data.tags)
.catch(e => this._handleAuthError(e, this.updateShortUrlTags, [shortCode, tags])); .catch(e => this._handleAuthError(e, this.updateShortUrlTags, [shortCode, tags]));
listTags = () =>
this._performRequest('/tags', 'GET')
.then(resp => resp.data.tags.data)
.catch(e => this._handleAuthError(e, this.listTags, []));
_performRequest = async (url, method = 'GET', params = {}, data = {}) => { _performRequest = async (url, method = 'GET', params = {}, data = {}) => {
if (isEmpty(this._token)) { if (isEmpty(this._token)) {
this._token = await this._authenticate(); this._token = await this._authenticate();

View file

@ -1,5 +1,6 @@
import listIcon from '@fortawesome/fontawesome-free-solid/faBars'; import listIcon from '@fortawesome/fontawesome-free-solid/faList';
import createIcon from '@fortawesome/fontawesome-free-solid/faPlus'; import createIcon from '@fortawesome/fontawesome-free-solid/faLink';
import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import React from 'react'; import React from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
@ -41,9 +42,17 @@ export default function AsideMenu({ selectedServer, className, showOnMobile }) {
activeClassName="aside-menu__item--selected" activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/create-short-url`} to={`/server/${serverId}/create-short-url`}
> >
<FontAwesomeIcon icon={createIcon} /> <FontAwesomeIcon icon={createIcon} flip="horizontal" />
<span className="aside-menu__item-text">Create short URL</span> <span className="aside-menu__item-text">Create short URL</span>
</NavLink> </NavLink>
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/tags`}
>
<FontAwesomeIcon icon={tagsIcon} />
<span className="aside-menu__item-text">List tags</span>
</NavLink>
<DeleteServerButton <DeleteServerButton
className="aside-menu__item aside-menu__item--danger" className="aside-menu__item aside-menu__item--danger"

View file

@ -13,6 +13,7 @@ import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars';
import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import classnames from 'classnames'; import classnames from 'classnames';
import './MenuLayout.scss'; import './MenuLayout.scss';
import TagsList from '../tags/TagsList';
export class MenuLayout extends React.Component { export class MenuLayout extends React.Component {
state = { showSideBar: false }; state = { showSideBar: false };
@ -78,6 +79,11 @@ export class MenuLayout extends React.Component {
path="/server/:serverId/short-code/:shortCode/visits" path="/server/:serverId/short-code/:shortCode/visits"
component={ShortUrlsVisits} component={ShortUrlsVisits}
/> />
<Route
exact
path="/server/:serverId/tags"
component={TagsList}
/>
</Switch> </Switch>
</div> </div>
</div> </div>

View file

@ -25,7 +25,7 @@ body,
@extend .bg-main; @extend .bg-main;
} }
.short-urls-container { .shlink-container {
padding: 20px 0; padding: 20px 0;
@media (min-width: $mdMin) { @media (min-width: $mdMin) {

View file

@ -7,6 +7,7 @@ import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListPara
import shortUrlCreationResultReducer from '../short-urls/reducers/shortUrlCreationResult'; import shortUrlCreationResultReducer from '../short-urls/reducers/shortUrlCreationResult';
import shortUrlVisitsReducer from '../short-urls/reducers/shortUrlVisits'; import shortUrlVisitsReducer from '../short-urls/reducers/shortUrlVisits';
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags'; import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
import tagsListReducer from '../tags/reducers/tagsList';
export default combineReducers({ export default combineReducers({
servers: serversReducer, servers: serversReducer,
@ -16,4 +17,5 @@ export default combineReducers({
shortUrlCreationResult: shortUrlCreationResultReducer, shortUrlCreationResult: shortUrlCreationResultReducer,
shortUrlVisits: shortUrlVisitsReducer, shortUrlVisits: shortUrlVisitsReducer,
shortUrlTags: shortUrlTagsReducer, shortUrlTags: shortUrlTagsReducer,
tagsList: tagsListReducer,
}); });

View file

@ -53,7 +53,7 @@ export class CreateShortUrl extends React.Component {
}; };
return ( return (
<div className="short-urls-container"> <div className="shlink-container">
<form onSubmit={save}> <form onSubmit={save}>
<div className="form-group"> <div className="form-group">
<input <input

View file

@ -115,8 +115,9 @@ export class ShortUrlsVisits extends React.Component {
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment> <Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</UncontrolledTooltip> </UncontrolledTooltip>
</span>; </span>;
return ( return (
<div className="short-urls-container"> <div className="shlink-container">
<header> <header>
<Card className="bg-light"> <Card className="bg-light">
<CardBody> <CardBody>

View file

@ -11,7 +11,7 @@ export function ShortUrls(props) {
const urlsListKey = `${params.serverId}_${params.page}`; const urlsListKey = `${params.serverId}_${params.page}`;
return ( return (
<div className="short-urls-container"> <div className="shlink-container">
<div className="form-group"><SearchBar /></div> <div className="form-group"><SearchBar /></div>
<ShortUrlsList {...props} shortUrlsList={props.shortUrlsList.data || []} key={urlsListKey} /> <ShortUrlsList {...props} shortUrlsList={props.shortUrlsList.data || []} key={urlsListKey} />
<Paginator paginator={props.shortUrlsList.pagination} serverId={props.match.params.serverId} /> <Paginator paginator={props.shortUrlsList.pagination} serverId={props.match.params.serverId} />

51
src/tags/TagsList.js Normal file
View file

@ -0,0 +1,51 @@
import React from 'react';
import { connect } from 'react-redux';
import { pick, splitEvery } from 'ramda';
import { listTags } from './reducers/tagsList';
import './TagsList.scss';
import { Card } from 'reactstrap';
import ColorGenerator from '../utils/ColorGenerator';
export class TagsList extends React.Component {
componentDidMount() {
const { listTags } = this.props;
listTags();
}
render() {
const { tagsList, colorGenerator } = this.props;
const tagsCount = Math.round(tagsList.tags.length);
if (tagsCount < 1) {
return <div>No tags</div>;
}
const tagsGroups = splitEvery(Math.round(tagsCount / 4), tagsList.tags);
return (
<div className="shlink-container">
<div className="row">
{tagsGroups.map((group, index) => (
<div key={index} className="col-md-6 col-xl-3">
{group.map(tag => (
<div
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
className="tags-list__tag-container"
>
<Card body className="tags-list__tag-card">
<h5 className="tags-list__tag-title">{tag}</h5>
</Card>
</div>
))}
</div>
))}
</div>
</div>
);
}
}
TagsList.defaultProps = {
colorGenerator: ColorGenerator
};
export default connect(pick(['tagsList']), { listTags })(TagsList);

18
src/tags/TagsList.scss Normal file
View file

@ -0,0 +1,18 @@
.tags-list__tag-container {
border-radius: .26rem;
}
.tags-list__tag-card.tags-list__tag-card {
padding: .75rem 2.5rem 0.75rem 1rem;
background-color: #eeeeee;
margin-bottom: .5rem;
transition: background-color 200ms, color 200ms;
}
.tags-list__tag-card.tags-list__tag-card:hover {
background-color: transparent;
color: #fff;
}
.tags-list__tag-title {
margin: 0;
}

View file

@ -0,0 +1,49 @@
import ShlinkApiClient from '../../api/ShlinkApiClient';
const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
const defaultState = {
tags: [],
loading: false,
error: false,
};
export default function reducer(state = defaultState, action) {
switch(action.type) {
case LIST_TAGS_START:
return {
...state,
loading: true,
error: false,
};
case LIST_TAGS_ERROR:
return {
...state,
loading: false,
error: true,
};
case LIST_TAGS:
return {
tags: action.tags,
loading: false,
error: false,
};
default:
return state;
}
}
export const _listTags = ShlinkApiClient => async dispatch => {
dispatch({ type: LIST_TAGS_START });
try {
const tags = await ShlinkApiClient.listTags();
dispatch({ tags, type: LIST_TAGS });
} catch (e) {
dispatch({ type: LIST_TAGS_ERROR });
throw e;
}
};
export const listTags = () => _listTags(ShlinkApiClient);

View file

@ -6,6 +6,7 @@ export default function Tag (
{ {
colorGenerator, colorGenerator,
text, text,
children,
clearable, clearable,
onClick = () => ({}), onClick = () => ({}),
onClose = () => ({}) onClose = () => ({})
@ -17,7 +18,7 @@ export default function Tag (
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable ? 'auto' : 'pointer' }} style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable ? 'auto' : 'pointer' }}
onClick={onClick} onClick={onClick}
> >
{text} {children || text}
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>&times;</span>} {clearable && <span className="close tag__close-selected-tag" onClick={onClose}>&times;</span>}
</span> </span>
); );