Updated styles in javascript to fulfill adidas rules

This commit is contained in:
Alejandro Celaya
2018-08-25 23:39:27 +02:00
parent ed0aa68452
commit 6a016d8e6f
70 changed files with 1250 additions and 759 deletions

View File

@@ -6,11 +6,11 @@ import React from 'react';
import { connect } from 'react-redux';
import { Collapse } from 'reactstrap';
import DateInput from '../common/DateInput';
import TagsSelector from '../utils/TagsSelector';
import CreateShortUrlResult from './helpers/CreateShortUrlResult';
import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult';
import TagsSelector from '../utils/TagsSelector';
export class CreateShortUrl extends React.Component {
export class CreateShortUrlComponent extends React.Component {
state = {
longUrl: '',
tags: [],
@@ -18,35 +18,37 @@ export class CreateShortUrl extends React.Component {
validSince: undefined,
validUntil: undefined,
maxVisits: undefined,
moreOptionsVisible: false
moreOptionsVisible: false,
};
render() {
const { createShortUrl, shortUrlCreationResult, resetCreateShortUrl } = this.props;
const changeTags = tags => this.setState({ tags: tags.map(pipe(trim, replace(/ /g, '-'))) });
const renderOptionalInput = (id, placeholder, type = 'text', props = {}) =>
const changeTags = (tags) => this.setState({ tags: tags.map(pipe(trim, replace(/ /g, '-'))) });
const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => (
<input
className="form-control"
type={type}
placeholder={placeholder}
value={this.state[id]}
onChange={e => this.setState({ [id]: e.target.value })}
onChange={(e) => this.setState({ [id]: e.target.value })}
{...props}
/>;
const createDateInput = (id, placeholder, props = {}) =>
/>
);
const createDateInput = (id, placeholder, props = {}) => (
<DateInput
selected={this.state[id]}
placeholderText={placeholder}
onChange={date => this.setState({ [id]: date })}
isClearable
onChange={(date) => this.setState({ [id]: date })}
{...props}
/>;
const formatDate = date => isNil(date) ? date : date.format();
const save = e => {
/>
);
const formatDate = (date) => isNil(date) ? date : date.format();
const save = (e) => {
e.preventDefault();
createShortUrl(pipe(
dissoc('moreOptionsVisible'), // Remove moreOptionsVisible property
dissoc('moreOptionsVisible'),
assoc('validSince', formatDate(this.state.validSince)),
assoc('validUntil', formatDate(this.state.validUntil))
)(this.state));
@@ -62,7 +64,7 @@ export class CreateShortUrl extends React.Component {
placeholder="Insert the URL to be shortened"
required
value={this.state.longUrl}
onChange={e => this.setState({ longUrl: e.target.value })}
onChange={(e) => this.setState({ longUrl: e.target.value })}
/>
</div>
@@ -95,7 +97,7 @@ export class CreateShortUrl extends React.Component {
<button
type="button"
className="btn btn-outline-secondary create-short-url__btn"
onClick={() => this.setState({ moreOptionsVisible: !this.state.moreOptionsVisible })}
onClick={() => this.setState(({ moreOptionsVisible }) => ({ moreOptionsVisible: !moreOptionsVisible }))}
>
<FontAwesomeIcon icon={this.state.moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
@@ -116,7 +118,9 @@ export class CreateShortUrl extends React.Component {
}
}
export default connect(pick(['shortUrlCreationResult']), {
const CreateShortUrl = connect(pick([ 'shortUrlCreationResult' ]), {
createShortUrl,
resetCreateShortUrl
})(CreateShortUrl);
resetCreateShortUrl,
})(CreateShortUrlComponent);
export default CreateShortUrl;

View File

@@ -1,52 +1,61 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import PropTypes from 'prop-types';
export default class Paginator extends React.Component {
render() {
const { paginator = {}, serverId } = this.props;
const { currentPage, pagesCount = 0 } = paginator;
if (pagesCount <= 1) {
return null;
const propTypes = {
serverId: PropTypes.string.isRequired,
paginator: PropTypes.shape({
currentPage: PropTypes.number,
pagesCount: PropTypes.number,
}),
};
export default function Paginator({ paginator = {}, serverId }) {
const { currentPage, pagesCount = 0 } = paginator;
if (pagesCount <= 1) {
return null;
}
const renderPages = () => {
const pages = [];
for (let i = 1; i <= pagesCount; i++) {
pages.push(
<PaginationItem key={i} active={currentPage === i}>
<PaginationLink
tag={Link}
to={`/server/${serverId}/list-short-urls/${i}`}
>
{i}
</PaginationLink>
</PaginationItem>
);
}
const renderPages = () => {
const pages = [];
return pages;
};
for (let i = 1; i <= pagesCount; i++) {
pages.push(
<PaginationItem key={i} active={currentPage === i}>
<PaginationLink
tag={Link}
to={`/server/${serverId}/list-short-urls/${i}`}
>
{i}
</PaginationLink>
</PaginationItem>
);
}
return pages;
};
return (
<Pagination listClassName="flex-wrap">
<PaginationItem disabled={currentPage === 1}>
<PaginationLink
previous
tag={Link}
to={`/server/${serverId}/list-short-urls/${currentPage - 1}`}
/>
</PaginationItem>
{renderPages()}
<PaginationItem disabled={currentPage >= pagesCount}>
<PaginationLink
next
tag={Link}
to={`/server/${serverId}/list-short-urls/${currentPage + 1}`}
/>
</PaginationItem>
</Pagination>
);
}
return (
<Pagination listClassName="flex-wrap">
<PaginationItem disabled={currentPage === 1}>
<PaginationLink
previous
tag={Link}
to={`/server/${serverId}/list-short-urls/${currentPage - 1}`}
/>
</PaginationItem>
{renderPages()}
<PaginationItem disabled={currentPage >= pagesCount}>
<PaginationLink
next
tag={Link}
to={`/server/${serverId}/list-short-urls/${currentPage + 1}`}
/>
</PaginationItem>
</Pagination>
);
}
Paginator.propTypes = propTypes;

View File

@@ -2,26 +2,34 @@ import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import React from 'react';
import { connect } from 'react-redux';
import Tag from '../utils/Tag';
import { listShortUrls } from './reducers/shortUrlsList';
import { isEmpty, pick } from 'ramda';
import PropTypes from 'prop-types';
import Tag from '../utils/Tag';
import SearchField from '../utils/SearchField';
import { listShortUrls } from './reducers/shortUrlsList';
import './SearchBar.scss';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
export function SearchBar({ listShortUrls, shortUrlsListParams }) {
const propTypes = {
listShortUrls: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
};
export function SearchBarComponent({ listShortUrls, shortUrlsListParams }) {
const selectedTags = shortUrlsListParams.tags || [];
return (
<div className="serach-bar-container">
<SearchField onChange={
searchTerm => listShortUrls({ ...shortUrlsListParams, searchTerm })
}/>
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
}
/>
{!isEmpty(selectedTags) && (
<h4 className="search-bar__selected-tag mt-2">
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon"/>
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
&nbsp;
{selectedTags.map(tag => (
{selectedTags.map((tag) => (
<Tag
key={tag}
text={tag}
@@ -29,7 +37,7 @@ export function SearchBar({ listShortUrls, shortUrlsListParams }) {
onClose={() => listShortUrls(
{
...shortUrlsListParams,
tags: selectedTags.filter(selectedTag => selectedTag !== tag)
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
}
)}
/>
@@ -40,4 +48,8 @@ export function SearchBar({ listShortUrls, shortUrlsListParams }) {
);
}
export default connect(pick(['shortUrlsListParams']), { listShortUrls })(SearchBar);
SearchBarComponent.propTypes = propTypes;
const SearchBar = connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })(SearchBarComponent);
export default SearchBar;

View File

@@ -1,22 +1,35 @@
import preloader from '@fortawesome/fontawesome-free-solid/faCircleNotch'
import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import { isEmpty, mapObjIndexed, pick } from 'ramda'
import React from 'react'
import { Doughnut, HorizontalBar } from 'react-chartjs-2'
import Moment from 'react-moment'
import { connect } from 'react-redux'
import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap'
import DateInput from '../common/DateInput'
import preloader from '@fortawesome/fontawesome-free-solid/faCircleNotch';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { isEmpty, mapObjIndexed, pick } from 'ramda';
import React from 'react';
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
import Moment from 'react-moment';
import { connect } from 'react-redux';
import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap';
import PropTypes from 'prop-types';
import DateInput from '../common/DateInput';
import {
processOsStats,
processBrowserStats,
processCountriesStats,
processReferrersStats,
} from '../visits/services/VisitsParser'
import { getShortUrlVisits } from './reducers/shortUrlVisits'
import './ShortUrlVisits.scss'
} from '../visits/services/VisitsParser';
import MutedMessage from '../utils/MuttedMessage';
import ExternalLink from '../utils/ExternalLink';
import { serverType } from '../servers/prop-types';
import { getShortUrlVisits, shortUrlVisitsType } from './reducers/shortUrlVisits';
import './ShortUrlVisits.scss';
const propTypes = {
processOsStats: PropTypes.func,
processBrowserStats: PropTypes.func,
processCountriesStats: PropTypes.func,
processReferrersStats: PropTypes.func,
match: PropTypes.object,
getShortUrlVisits: PropTypes.func,
selectedServer: serverType,
shortUrlVisits: shortUrlVisitsType,
};
const defaultProps = {
processOsStats,
processBrowserStats,
@@ -24,14 +37,15 @@ const defaultProps = {
processReferrersStats,
};
export class ShortUrlsVisits extends React.Component {
export class ShortUrlsVisitsComponent extends React.Component {
state = { startDate: undefined, endDate: undefined };
loadVisits = () => {
const { match: { params } } = this.props;
this.props.getShortUrlVisits(params.shortCode, mapObjIndexed(
value => value && value.format ? value.format('YYYY-MM-DD') : value,
const { match: { params }, getShortUrlVisits } = this.props;
getShortUrlVisits(params.shortCode, mapObjIndexed(
(value) => value && value.format ? value.format('YYYY-MM-DD') : value,
this.state
))
));
};
componentDidMount() {
@@ -46,7 +60,7 @@ export class ShortUrlsVisits extends React.Component {
processBrowserStats,
processCountriesStats,
processReferrersStats,
shortUrlVisits: { visits, loading, error, shortUrl }
shortUrlVisits: { visits, loading, error, shortUrl },
} = this.props;
const serverUrl = selectedServer ? selectedServer.url : '';
const shortLink = `${serverUrl}/${params.shortCode}`;
@@ -63,31 +77,42 @@ export class ShortUrlsVisits extends React.Component {
'#46BFBD',
'#FDB45C',
'#949FB1',
'#4D5360'
'#4D5360',
],
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
borderWidth: 2
}
]
borderWidth: 2,
},
],
});
const renderGraphCard = (title, stats, isBarChart, label) =>
const renderGraphCard = (title, stats, isBarChart, label) => (
<div className="col-md-6">
<Card className="mt-4">
<CardHeader>{title}</CardHeader>
<CardBody>
{!isBarChart && <Doughnut data={generateGraphData(stats, label || title, isBarChart)} options={{
legend: {
position: 'right'
}
}} />}
{isBarChart && <HorizontalBar data={generateGraphData(stats, label || title, isBarChart)} options={{
legend: {
display: false
}
}} />}
{!isBarChart && (
<Doughnut
data={generateGraphData(stats, label || title, isBarChart)}
options={{
legend: {
position: 'right',
},
}}
/>
)}
{isBarChart && (
<HorizontalBar
data={generateGraphData(stats, label || title, isBarChart)}
options={{
legend: {
display: false,
},
}}
/>
)}
</CardBody>
</Card>
</div>;
</div>
);
const renderContent = () => {
if (loading) {
return <MutedMessage><FontAwesomeIcon icon={preloader} spin /> Loading...</MutedMessage>;
@@ -115,13 +140,14 @@ export class ShortUrlsVisits extends React.Component {
);
};
const renderCreated = () =>
const renderCreated = () => (
<span>
<b id="created"><Moment fromNow>{shortUrl.dateCreated}</Moment></b>
<UncontrolledTooltip placement="bottom" target="created">
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</UncontrolledTooltip>
</span>;
</span>
);
return (
<div className="shlink-container">
@@ -133,20 +159,22 @@ export class ShortUrlsVisits extends React.Component {
shortUrl.visitsCount &&
<span className="badge badge-main float-right">Visits: {shortUrl.visitsCount}</span>
}
Visit stats for <a target="_blank" href={shortLink}>{shortLink}</a>
Visit stats for <ExternalLink href={shortLink}>{shortLink}</ExternalLink>
</h2>
<hr />
{shortUrl.dateCreated && <div>
Created:
&nbsp;
{loading && <small>Loading...</small>}
{!loading && renderCreated()}
</div>}
{shortUrl.dateCreated && (
<div>
Created:
&nbsp;
{loading && <small>Loading...</small>}
{!loading && renderCreated()}
</div>
)}
<div>
Long URL:
&nbsp;
{loading && <small>Loading...</small>}
{!loading && <a target="_blank" href={shortUrl.longUrl}>{shortUrl.longUrl}</a>}
{!loading && <ExternalLink href={shortUrl.longUrl}>{shortUrl.longUrl}</ExternalLink>}
</div>
</CardBody>
</Card>
@@ -159,7 +187,7 @@ export class ShortUrlsVisits extends React.Component {
selected={this.state.startDate}
placeholderText="Since"
isClearable
onChange={date => this.setState({ startDate: date }, () => this.loadVisits())}
onChange={(date) => this.setState({ startDate: date }, () => this.loadVisits())}
/>
</div>
<div className="col-xl-3 col-lg-4 col-md-6">
@@ -167,8 +195,8 @@ export class ShortUrlsVisits extends React.Component {
selected={this.state.endDate}
placeholderText="Until"
isClearable
onChange={date => this.setState({ endDate: date }, () => this.loadVisits())}
className="short-url-visits__date-input"
onChange={(date) => this.setState({ endDate: date }, () => this.loadVisits())}
/>
</div>
</div>
@@ -182,9 +210,12 @@ export class ShortUrlsVisits extends React.Component {
}
}
ShortUrlsVisits.defaultProps = defaultProps;
ShortUrlsVisitsComponent.propTypes = propTypes;
ShortUrlsVisitsComponent.defaultProps = defaultProps;
export default connect(
pick(['selectedServer', 'shortUrlVisits']),
const ShortUrlsVisits = connect(
pick([ 'selectedServer', 'shortUrlVisits' ]),
{ getShortUrlVisits }
)(ShortUrlsVisits);
)(ShortUrlsVisitsComponent);
export default ShortUrlsVisits;

View File

@@ -1,12 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import { assoc } from 'ramda';
import Paginator from './Paginator';
import SearchBar from './SearchBar';
import ShortUrlsList from './ShortUrlsList';
import { assoc } from 'ramda';
export function ShortUrls(props) {
export function ShortUrlsComponent(props) {
const { match: { params } } = props;
// Using a key on a component makes react to create a new instance every time the key changes
const urlsListKey = `${params.serverId}_${params.page}`;
@@ -19,4 +20,8 @@ export function ShortUrls(props) {
);
}
export default connect(state => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList))(ShortUrls);
const ShortUrls = connect(
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
)(ShortUrlsComponent);
export default ShortUrls;

View File

@@ -1,14 +1,17 @@
import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown'
import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp'
import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import { head, isEmpty, pick, toPairs, keys, values } from 'ramda'
import React from 'react'
import { connect } from 'react-redux'
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'
import { ShortUrlsRow } from './helpers/ShortUrlsRow'
import { listShortUrls } from './reducers/shortUrlsList'
import './ShortUrlsList.scss'
import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown';
import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { head, isEmpty, pick, toPairs, keys, values } from 'ramda';
import React from 'react';
import { connect } from 'react-redux';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import qs from 'qs';
import PropTypes from 'prop-types';
import { serverType } from '../servers/prop-types';
import { ShortUrlsRow } from './helpers/ShortUrlsRow';
import { listShortUrls, shortUrlType } from './reducers/shortUrlsList';
import './ShortUrlsList.scss';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
const SORTABLE_FIELDS = {
dateCreated: 'Created at',
@@ -17,29 +20,43 @@ const SORTABLE_FIELDS = {
visits: 'Visits',
};
export class ShortUrlsList extends React.Component {
refreshList = extraParams => {
const propTypes = {
listShortUrls: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
match: PropTypes.object,
location: PropTypes.object,
loading: PropTypes.bool,
error: PropTypes.bool,
shortUrlsList: PropTypes.arrayOf(shortUrlType),
selectedServer: serverType,
};
export class ShortUrlsListComponent extends React.Component {
refreshList = (extraParams) => {
const { listShortUrls, shortUrlsListParams } = this.props;
listShortUrls({
...shortUrlsListParams,
...extraParams
...extraParams,
});
};
determineOrderDir = field => {
determineOrderDir = (field) => {
if (this.state.orderField !== field) {
return 'ASC';
}
const newOrderMap = {
'ASC': 'DESC',
'DESC': undefined,
ASC: 'DESC',
DESC: undefined,
};
return this.state.orderDir ? newOrderMap[this.state.orderDir] : 'ASC';
};
orderBy = field => {
orderBy = (field) => {
const newOrderDir = this.determineOrderDir(field);
this.setState({ orderField: newOrderDir !== undefined ? field : undefined, orderDir: newOrderDir });
this.refreshList({ orderBy: { [field]: newOrderDir } })
this.refreshList({ orderBy: { [field]: newOrderDir } });
};
renderOrderIcon = (field, className = 'short-urls-list__header-icon') => {
if (this.state.orderField !== field) {
@@ -58,21 +75,23 @@ export class ShortUrlsList extends React.Component {
super(props);
const { orderBy } = props.shortUrlsListParams;
this.state = {
orderField: orderBy ? head(keys(orderBy)) : undefined,
orderDir: orderBy ? head(values(orderBy)) : undefined,
}
};
}
componentDidMount() {
const { match: { params }, location } = this.props;
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
this.refreshList({ page: params.page, tags: query.tag ? [query.tag] : [] });
this.refreshList({ page: params.page, tags: query.tag ? [ query.tag ] : [] });
}
renderShortUrls() {
const { shortUrlsList, selectedServer, loading, error, shortUrlsListParams } = this.props;
if (error) {
return (
<tr>
@@ -85,11 +104,11 @@ export class ShortUrlsList extends React.Component {
return <tr><td colSpan="6" className="text-center">Loading...</td></tr>;
}
if (! loading && isEmpty(shortUrlsList)) {
if (!loading && isEmpty(shortUrlsList)) {
return <tr><td colSpan="6" className="text-center">No results found</td></tr>;
}
return shortUrlsList.map(shortUrl => (
return shortUrlsList.map((shortUrl) => (
<ShortUrlsRow
shortUrl={shortUrl}
selectedServer={selectedServer}
@@ -108,11 +127,12 @@ export class ShortUrlsList extends React.Component {
Order by
</DropdownToggle>
<DropdownMenu className="short-urls-list__order-dropdown">
{toPairs(SORTABLE_FIELDS).map(([key, value]) =>
{toPairs(SORTABLE_FIELDS).map(([ key, value ]) => (
<DropdownItem key={key} active={this.state.orderField === key} onClick={() => this.orderBy(key)}>
{value}
{this.renderOrderIcon(key, 'short-urls-list__header-icon--mobile')}
</DropdownItem>)}
</DropdownItem>
))}
</DropdownMenu>
</UncontrolledDropdown>
</div>
@@ -166,4 +186,11 @@ export class ShortUrlsList extends React.Component {
}
}
export default connect(pick(['selectedServer', 'shortUrlsListParams']), { listShortUrls })(ShortUrlsList);
ShortUrlsListComponent.propTypes = propTypes;
const ShortUrlsList = connect(
pick([ 'selectedServer', 'shortUrlsListParams' ]),
{ listShortUrls }
)(ShortUrlsListComponent);
export default ShortUrlsList;

View File

@@ -3,8 +3,18 @@ import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { isNil } from 'ramda';
import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import './CreateShortUrlResult.scss'
import { Card, CardBody, Tooltip } from 'reactstrap';
import PropTypes from 'prop-types';
import { createShortUrlResultType } from '../reducers/shortUrlCreationResult';
import './CreateShortUrlResult.scss';
const TIME_TO_SHOW_COPY_TOOLTIP = 2000;
const propTypes = {
resetCreateShortUrl: PropTypes.func,
error: PropTypes.bool,
result: createShortUrlResultType,
};
export default class CreateShortUrlResult extends React.Component {
state = { showCopyTooltip: false };
@@ -30,7 +40,7 @@ export default class CreateShortUrlResult extends React.Component {
const { shortUrl } = result;
const onCopy = () => {
this.setState({ showCopyTooltip: true });
setTimeout(() => this.setState({ showCopyTooltip: false }), 2000);
setTimeout(() => this.setState({ showCopyTooltip: false }), TIME_TO_SHOW_COPY_TOOLTIP);
};
return (
@@ -39,8 +49,12 @@ export default class CreateShortUrlResult extends React.Component {
<b>Great!</b> The short URL is <b>{shortUrl}</b>
<CopyToClipboard text={shortUrl} onCopy={onCopy}>
<button className="btn btn-light btn-sm create-short-url-result__copy-btn" id="copyBtn" type="button">
<FontAwesomeIcon icon={copyIcon}/> Copy
<button
className="btn btn-light btn-sm create-short-url-result__copy-btn"
id="copyBtn"
type="button"
>
<FontAwesomeIcon icon={copyIcon} /> Copy
</button>
</CopyToClipboard>
@@ -51,4 +65,6 @@ export default class CreateShortUrlResult extends React.Component {
</Card>
);
}
};
}
CreateShortUrlResult.propTypes = propTypes;

View File

@@ -1,30 +1,33 @@
import React from 'react';
import { connect } from 'react-redux';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import TagsSelector from '../../utils/TagsSelector';
import PropTypes from 'prop-types';
import { pick } from 'ramda';
import TagsSelector from '../../utils/TagsSelector';
import {
editShortUrlTags,
resetShortUrlsTags,
shortUrlTagsType,
shortUrlTagsEdited
shortUrlTagsEdited,
} from '../reducers/shortUrlTags';
import { pick } from 'ramda';
import ExternalLink from '../../utils/ExternalLink';
import { shortUrlType } from '../reducers/shortUrlsList';
const propTypes = {
isOpen: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
shortUrl: PropTypes.shape({
tags: PropTypes.arrayOf(PropTypes.string),
shortCode: PropTypes.string,
}).isRequired,
shortUrl: shortUrlType.isRequired,
shortUrlTags: shortUrlTagsType,
editShortUrlTags: PropTypes.func,
shortUrlTagsEdited: PropTypes.func,
resetShortUrlsTags: PropTypes.func,
};
export class EditTagsModal extends React.Component {
export class EditTagsModalComponent extends React.Component {
saveTags = () => {
const { editShortUrlTags, shortUrl, toggle } = this.props;
editShortUrlTags(shortUrl.shortCode, this.state.tags)
.then(() => {
this.tagsSaved = true;
@@ -39,11 +42,13 @@ export class EditTagsModal extends React.Component {
const { shortUrlTagsEdited, shortUrl } = this.props;
const { tags } = this.state;
shortUrlTagsEdited(shortUrl.shortCode, tags);
};
componentDidMount() {
const { resetShortUrlsTags } = this.props;
resetShortUrlsTags();
this.tagsSaved = false;
}
@@ -57,12 +62,12 @@ export class EditTagsModal extends React.Component {
const { isOpen, toggle, url, shortUrlTags } = this.props;
return (
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={this.refreshShortUrls}>
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={() => this.refreshShortUrls}>
<ModalHeader toggle={toggle}>
Edit tags for <a target="_blank" href={url}>{url}</a>
Edit tags for <ExternalLink href={url}>{url}</ExternalLink>
</ModalHeader>
<ModalBody>
<TagsSelector tags={this.state.tags} onChange={tags => this.setState({ tags })} />
<TagsSelector tags={this.state.tags} onChange={(tags) => this.setState({ tags })} />
{shortUrlTags.error && (
<div className="p-2 mt-2 bg-danger text-white text-center">
Something went wrong while saving the tags :(
@@ -74,8 +79,8 @@ export class EditTagsModal extends React.Component {
<button
className="btn btn-primary"
type="button"
onClick={this.saveTags}
disabled={shortUrlTags.saving}
onClick={() => this.saveTags}
>
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
</button>
@@ -85,9 +90,11 @@ export class EditTagsModal extends React.Component {
}
}
EditTagsModal.propTypes = propTypes;
EditTagsModalComponent.propTypes = propTypes;
export default connect(
pick(['shortUrlTags']),
const EditTagsModal = connect(
pick([ 'shortUrlTags' ]),
{ editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited }
)(EditTagsModal);
)(EditTagsModalComponent);
export default EditTagsModal;

View File

@@ -1,11 +1,21 @@
import React from 'react'
import React from 'react';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import PropTypes from 'prop-types';
import './PreviewModal.scss';
import ExternalLink from '../../utils/ExternalLink';
export default function PreviewModal ({ url, toggle, isOpen }) {
const propTypes = {
url: PropTypes.string,
toggle: PropTypes.func,
isOpen: PropTypes.bool,
};
export default function PreviewModal({ url, toggle, isOpen }) {
return (
<Modal isOpen={isOpen} toggle={toggle} size="lg">
<ModalHeader toggle={toggle}>Preview for <a target="_blank" href={url}>{url}</a></ModalHeader>
<ModalHeader toggle={toggle}>
Preview for <ExternalLink href={url}>{url}</ExternalLink>
</ModalHeader>
<ModalBody>
<div className="text-center">
<p className="preview-modal__loader">Loading...</p>
@@ -15,3 +25,5 @@ export default function PreviewModal ({ url, toggle, isOpen }) {
</Modal>
);
}
PreviewModal.propTypes = propTypes;

View File

@@ -1,11 +1,21 @@
import React from 'react'
import React from 'react';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import PropTypes from 'prop-types';
import './QrCodeModal.scss';
import ExternalLink from '../../utils/ExternalLink';
export default function QrCodeModal ({ url, toggle, isOpen }) {
const propTypes = {
url: PropTypes.string,
toggle: PropTypes.func,
isOpen: PropTypes.bool,
};
export default function QrCodeModal({ url, toggle, isOpen }) {
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}>QR code for <a target="_blank" href={url}>{url}</a></ModalHeader>
<ModalHeader toggle={toggle}>
QR code for <ExternalLink href={url}>{url}</ExternalLink>
</ModalHeader>
<ModalBody>
<div className="text-center">
<img src={`${url}/qr-code`} className="qr-code-modal__img" alt="QR code" />
@@ -14,3 +24,5 @@ export default function QrCodeModal ({ url, toggle, isOpen }) {
</Modal>
);
}
QrCodeModal.propTypes = propTypes;

View File

@@ -1,9 +1,23 @@
import { isEmpty } from 'ramda';
import React from 'react';
import Moment from 'react-moment';
import PropTypes from 'prop-types';
import Tag from '../../utils/Tag';
import './ShortUrlsRow.scss';
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
import { serverType } from '../../servers/prop-types';
import ExternalLink from '../../utils/ExternalLink';
import { shortUrlType } from '../reducers/shortUrlsList';
import { ShortUrlsRowMenu } from './ShortUrlsRowMenu';
import './ShortUrlsRow.scss';
const COPIED_MSG_TIME = 2000;
const propTypes = {
refreshList: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
selectedServer: serverType,
shortUrl: shortUrlType,
};
export class ShortUrlsRow extends React.Component {
state = { copiedToClipboard: false };
@@ -15,7 +29,8 @@ export class ShortUrlsRow extends React.Component {
const { refreshList, shortUrlsListParams } = this.props;
const selectedTags = shortUrlsListParams.tags || [];
return tags.map(tag => (
return tags.map((tag) => (
<Tag
key={tag}
text={tag}
@@ -34,10 +49,10 @@ export class ShortUrlsRow extends React.Component {
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</td>
<td className="short-urls-row__cell" data-th="Short URL: ">
<a href={completeShortUrl} target="_blank">{completeShortUrl}</a>
<ExternalLink href={completeShortUrl}>{completeShortUrl}</ExternalLink>
</td>
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
<a href={shortUrl.originalUrl} target="_blank">{shortUrl.originalUrl}</a>
<ExternalLink href={shortUrl.originalUrl}>{shortUrl.originalUrl}</ExternalLink>
</td>
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">{shortUrl.visitsCount}</td>
@@ -54,7 +69,7 @@ export class ShortUrlsRow extends React.Component {
shortUrl={shortUrl}
onCopyToClipboard={() => {
this.setState({ copiedToClipboard: true });
setTimeout(() => this.setState({ copiedToClipboard: false }), 2000);
setTimeout(() => this.setState({ copiedToClipboard: false }), COPIED_MSG_TIME);
}}
/>
</td>
@@ -62,3 +77,5 @@ export class ShortUrlsRow extends React.Component {
);
}
}
ShortUrlsRow.propTypes = propTypes;

View File

@@ -9,11 +9,21 @@ import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Link } from 'react-router-dom';
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import PropTypes from 'prop-types';
import { serverType } from '../../servers/prop-types';
import { shortUrlType } from '../reducers/shortUrlsList';
import PreviewModal from './PreviewModal';
import QrCodeModal from './QrCodeModal';
import './ShortUrlsRowMenu.scss';
import EditTagsModal from './EditTagsModal';
const propTypes = {
completeShortUrl: PropTypes.string,
onCopyToClipboard: PropTypes.func,
selectedServer: serverType,
shortUrl: shortUrlType,
};
export class ShortUrlsRowMenu extends React.Component {
state = {
isOpen: false,
@@ -21,26 +31,26 @@ export class ShortUrlsRowMenu extends React.Component {
isPreviewOpen: false,
isTagsModalOpen: false,
};
toggle = () => this.setState({ isOpen: !this.state.isOpen });
toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
render() {
const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props;
const serverId = selectedServer ? selectedServer.id : '';
const toggleQrCode = () => this.setState({isQrModalOpen: !this.state.isQrModalOpen});
const togglePreview = () => this.setState({isPreviewOpen: !this.state.isPreviewOpen});
const toggleTags = () => this.setState({isTagsModalOpen: !this.state.isTagsModalOpen});
const toggleQrCode = () => this.setState(({ isQrModalOpen }) => ({ isQrModalOpen: !isQrModalOpen }));
const togglePreview = () => this.setState(({ isPreviewOpen }) => ({ isPreviewOpen: !isPreviewOpen }));
const toggleTags = () => this.setState(({ isTagsModalOpen }) => ({ isTagsModalOpen: !isTagsModalOpen }));
return (
<ButtonDropdown toggle={this.toggle} isOpen={this.state.isOpen} direction="left">
<DropdownToggle size="sm" caret className="short-urls-row-menu__dropdown-toggle btn-outline-secondary">
&nbsp;<FontAwesomeIcon icon={menuIcon}/>&nbsp;
&nbsp;<FontAwesomeIcon icon={menuIcon} />&nbsp;
</DropdownToggle>
<DropdownMenu>
<DropdownItem tag={Link} to={`/server/${serverId}/short-code/${shortUrl.shortCode}/visits`}>
<FontAwesomeIcon icon={pieChartIcon}/> &nbsp;Visit Stats
<FontAwesomeIcon icon={pieChartIcon} /> &nbsp;Visit Stats
</DropdownItem>
<DropdownItem onClick={toggleTags}>
<FontAwesomeIcon icon={tagsIcon}/> &nbsp;Edit tags
<FontAwesomeIcon icon={tagsIcon} /> &nbsp;Edit tags
</DropdownItem>
<EditTagsModal
url={completeShortUrl}
@@ -49,10 +59,10 @@ export class ShortUrlsRowMenu extends React.Component {
toggle={toggleTags}
/>
<DropdownItem divider/>
<DropdownItem divider />
<DropdownItem onClick={togglePreview}>
<FontAwesomeIcon icon={pictureIcon}/> &nbsp;Preview
<FontAwesomeIcon icon={pictureIcon} /> &nbsp;Preview
</DropdownItem>
<PreviewModal
url={completeShortUrl}
@@ -61,7 +71,7 @@ export class ShortUrlsRowMenu extends React.Component {
/>
<DropdownItem onClick={toggleQrCode}>
<FontAwesomeIcon icon={qrIcon}/> &nbsp;QR code
<FontAwesomeIcon icon={qrIcon} /> &nbsp;QR code
</DropdownItem>
<QrCodeModal
url={completeShortUrl}
@@ -69,11 +79,11 @@ export class ShortUrlsRowMenu extends React.Component {
toggle={toggleQrCode}
/>
<DropdownItem divider/>
<DropdownItem divider />
<CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
<DropdownItem>
<FontAwesomeIcon icon={copyIcon}/> &nbsp;Copy to clipboard
<FontAwesomeIcon icon={copyIcon} /> &nbsp;Copy to clipboard
</DropdownItem>
</CopyToClipboard>
</DropdownMenu>
@@ -81,3 +91,5 @@ export class ShortUrlsRowMenu extends React.Component {
);
}
}
ShortUrlsRowMenu.propTypes = propTypes;

View File

@@ -1,11 +1,20 @@
import ShlinkApiClient from '../../api/ShlinkApiClient';
import { curry } from 'ramda';
import PropTypes from 'prop-types';
import shlinkApiClient from '../../api/ShlinkApiClient';
const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR';
const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
export const createShortUrlResultType = {
result: PropTypes.shape({
shortUrl: PropTypes.string,
}),
saving: PropTypes.bool,
error: PropTypes.bool,
};
const defaultState = {
result: null,
saving: false,
@@ -38,16 +47,18 @@ export default function reducer(state = defaultState, action) {
}
}
export const _createShortUrl = (ShlinkApiClient, data) => async dispatch => {
export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => {
dispatch({ type: CREATE_SHORT_URL_START });
try {
const result = await ShlinkApiClient.createShortUrl(data);
const result = await shlinkApiClient.createShortUrl(data);
dispatch({ type: CREATE_SHORT_URL, result });
} catch (e) {
dispatch({ type: CREATE_SHORT_URL_ERROR });
}
};
export const createShortUrl = curry(_createShortUrl)(ShlinkApiClient);
export const createShortUrl = curry(_createShortUrl)(shlinkApiClient);
export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL });

View File

@@ -1,11 +1,15 @@
import ShlinkApiClient from '../../api/ShlinkApiClient';
import { curry } from 'ramda';
import PropTypes from 'prop-types';
import shlinkApiClient from '../../api/ShlinkApiClient';
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
export const EDIT_SHORT_URL_TAGS_ERROR = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_ERROR';
export const EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS';
export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS';
export const SHORT_URL_TAGS_EDITED = 'shlink/shortUrlTags/SHORT_URL_TAGS_EDITED';
export const shortUrlTagsType = PropTypes.shape({
@@ -50,19 +54,21 @@ export default function reducer(state = defaultState, action) {
}
}
export const _editShortUrlTags = (ShlinkApiClient, shortCode, tags) => async (dispatch, getState) => {
export const _editShortUrlTags = (shlinkApiClient, shortCode, tags) => async (dispatch) => {
dispatch({ type: EDIT_SHORT_URL_TAGS_START });
try {
// Update short URL tags
await ShlinkApiClient.updateShortUrlTags(shortCode, tags);
await shlinkApiClient.updateShortUrlTags(shortCode, tags);
dispatch({ tags, shortCode, type: EDIT_SHORT_URL_TAGS });
} catch (e) {
dispatch({ type: EDIT_SHORT_URL_TAGS_ERROR });
throw e;
}
};
export const editShortUrlTags = curry(_editShortUrlTags)(ShlinkApiClient);
export const editShortUrlTags = curry(_editShortUrlTags)(shlinkApiClient);
export const resetShortUrlsTags = () => ({ type: RESET_EDIT_SHORT_URL_TAGS });

View File

@@ -1,50 +1,60 @@
import ShlinkApiClient from '../../api/ShlinkApiClient';
import { curry } from 'ramda';
import PropTypes from 'prop-types';
import shlinkApiClient from '../../api/ShlinkApiClient';
import { shortUrlType } from './shortUrlsList';
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';
export const shortUrlVisitsType = {
shortUrl: shortUrlType,
visits: PropTypes.array,
loading: PropTypes.bool,
error: PropTypes.bool,
};
const initialState = {
shortUrl: {},
visits: [],
loading: false,
error: false
error: false,
};
export default function dispatch (state = initialState, action) {
export default function dispatch(state = initialState, action) {
switch (action.type) {
case GET_SHORT_URL_VISITS_START:
return {
...state,
loading: true
loading: true,
};
case GET_SHORT_URL_VISITS_ERROR:
return {
...state,
loading: false,
error: true
error: true,
};
case GET_SHORT_URL_VISITS:
return {
shortUrl: action.shortUrl,
visits: action.visits,
loading: false,
error: false
error: false,
};
default:
return state;
}
}
export const _getShortUrlVisits = (ShlinkApiClient, shortCode, dates) => dispatch => {
export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => (dispatch) => {
dispatch({ type: GET_SHORT_URL_VISITS_START });
Promise.all([
ShlinkApiClient.getShortUrlVisits(shortCode, dates),
ShlinkApiClient.getShortUrl(shortCode)
shlinkApiClient.getShortUrlVisits(shortCode, dates),
shlinkApiClient.getShortUrl(shortCode),
])
.then(([visits, shortUrl]) => dispatch({ visits, shortUrl, type: GET_SHORT_URL_VISITS }))
.then(([ visits, shortUrl ]) => dispatch({ visits, shortUrl, type: GET_SHORT_URL_VISITS }))
.catch(() => dispatch({ type: GET_SHORT_URL_VISITS_ERROR }));
};
export const getShortUrlVisits = curry(_getShortUrlVisits)(ShlinkApiClient);
export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient);

View File

@@ -1,11 +1,19 @@
import ShlinkApiClient from '../../api/ShlinkApiClient';
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
import { assoc, assocPath } from 'ramda';
import PropTypes from 'prop-types';
import shlinkApiClient from '../../api/ShlinkApiClient';
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
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 shortUrlType = PropTypes.shape({
tags: PropTypes.arrayOf(PropTypes.string),
shortCode: PropTypes.string,
originalUrl: PropTypes.string,
});
const initialState = {
shortUrls: {},
loading: true,
@@ -19,34 +27,36 @@ export default function reducer(state = initialState, action) {
return {
loading: false,
error: false,
shortUrls: action.shortUrls
shortUrls: action.shortUrls,
};
case LIST_SHORT_URLS_ERROR:
return {
loading: false,
error: true,
shortUrls: []
shortUrls: [],
};
case SHORT_URL_TAGS_EDITED:
const { data } = state.shortUrls;
return assocPath(['shortUrls', 'data'], data.map(shortUrl =>
return assocPath([ 'shortUrls', 'data' ], data.map((shortUrl) =>
shortUrl.shortCode === action.shortCode
? assoc('tags', action.tags, shortUrl)
: shortUrl
), state);
: shortUrl), state);
default:
return state;
}
}
export const _listShortUrls = (ShlinkApiClient, params = {}) => async dispatch => {
export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) => {
dispatch({ type: LIST_SHORT_URLS_START });
try {
const shortUrls = await ShlinkApiClient.listShortUrls(params);
const shortUrls = await shlinkApiClient.listShortUrls(params);
dispatch({ type: LIST_SHORT_URLS, shortUrls, params });
} catch (e) {
dispatch({ type: LIST_SHORT_URLS_ERROR, params });
}
};
export const listShortUrls = (params = {}) => _listShortUrls(ShlinkApiClient, params);
export const listShortUrls = (params = {}) => _listShortUrls(shlinkApiClient, params);

View File

@@ -1,7 +1,14 @@
import PropTypes from 'prop-types';
import { LIST_SHORT_URLS } from './shortUrlsList';
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';
export const shortUrlsListParamsType = {
page: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
searchTerm: PropTypes.string,
};
const defaultState = { page: '1' };
export default function reducer(state = defaultState, action) {