mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Created tags list page
This commit is contained in:
parent
03113583f0
commit
49290b56ee
12 changed files with 150 additions and 8 deletions
|
@ -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();
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
51
src/tags/TagsList.js
Normal 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
18
src/tags/TagsList.scss
Normal 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;
|
||||||
|
}
|
49
src/tags/reducers/tagsList.js
Normal file
49
src/tags/reducers/tagsList.js
Normal 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);
|
|
@ -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}>×</span>}
|
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>×</span>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue