Initial Commit Rework

This commit is contained in:
Qstick 2017-09-03 22:20:56 -04:00
parent 74a4cc048c
commit 95051cbd63
2483 changed files with 101351 additions and 111396 deletions

View file

@ -0,0 +1,119 @@
.button {
composes: link from './Link.css';
overflow: hidden;
border: 1px solid;
border-radius: 4px;
vertical-align: middle;
text-align: center;
white-space: nowrap;
line-height: normal;
&:global(.isDisabled) {
opacity: 0.65;
}
&:hover {
text-decoration: none;
}
}
.danger {
border-color: $dangerBorderColor;
background-color: $dangerBackgroundColor;
color: $white;
&:hover {
border-color: $dangerHoverBorderColor;
background-color: $dangerHoverBackgroundColor;
color: $white;
}
}
.default {
border-color: $defaultBorderColor;
background-color: $defaultBackgroundColor;
color: $defaultColor;
&:hover {
border-color: $defaultHoverBorderColor;
background-color: $defaultHoverBackgroundColor;
color: $defaultColor;
}
}
.primary {
border-color: $primaryBorderColor;
background-color: $primaryBackgroundColor;
color: $white;
&:hover {
border-color: $primaryHoverBorderColor;
background-color: $primaryHoverBackgroundColor;
color: $white;
}
}
.success {
border-color: $successBorderColor;
background-color: $successBackgroundColor;
color: $white;
&:hover {
border-color: $successHoverBorderColor;
background-color: $successHoverBackgroundColor;
color: $white;
}
}
.warning {
border-color: $warningBorderColor;
background-color: $warningBackgroundColor;
color: $white;
&:hover {
border-color: $warningHoverBorderColor;
background-color: $warningHoverBackgroundColor;
color: $white;
}
}
/*
* Sizes
*/
.small {
padding: 1px 5px;
font-size: $smallFontSize;
}
.medium {
padding: 6px 16px;
font-size: $defaultFontSize;
}
.large {
padding: 10px 20px;
font-size: $largeFontSize;
}
/*
* Sizes
*/
.left {
margin-left: -1px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.center {
margin-left: -1px;
border-radius: 0;
}
.right {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View file

@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { align, kinds, sizes } from 'Helpers/Props';
import Link from './Link';
import styles from './Button.css';
class Button extends Component {
//
// Render
render() {
const {
className,
buttonGroupPosition,
kind,
size,
children,
...otherProps
} = this.props;
return (
<Link
className={classNames(
className,
styles[kind],
styles[size],
buttonGroupPosition && styles[buttonGroupPosition]
)}
{...otherProps}
>
{children}
</Link>
);
}
}
Button.propTypes = {
className: PropTypes.string.isRequired,
buttonGroupPosition: PropTypes.oneOf(align.all),
kind: PropTypes.oneOf(kinds.all),
size: PropTypes.oneOf(sizes.all),
children: PropTypes.node
};
Button.defaultProps = {
className: styles.button,
kind: kinds.DEFAULT,
size: sizes.MEDIUM
};
export default Button;

View file

@ -0,0 +1,33 @@
.button {
composes: button from 'Components/Form/FormInputButton.css';
position: relative;
}
.stateIconContainer {
position: absolute;
top: 50%;
left: -100%;
display: inline-flex;
visibility: hidden;
transition: left $defaultSpeed;
transform: translateX(-50%) translateY(-50%);
}
.clipboardIconContainer {
position: relative;
left: 0;
transition: left $defaultSpeed, opacity $defaultSpeed;
}
.showStateIcon {
.stateIconContainer {
left: 50%;
visibility: visible;
}
.clipboardIconContainer {
left: 100%;
opacity: 0;
}
}

View file

@ -0,0 +1,128 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Clipboard from 'Clipboard';
import { icons, kinds } from 'Helpers/Props';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import Icon from 'Components/Icon';
import FormInputButton from 'Components/Form/FormInputButton';
import styles from './ClipboardButton.css';
class ClipboardButton extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._id = getUniqueElememtId();
this._successTimeout = null;
this.state = {
showSuccess: false,
showError: false
};
}
componentDidMount() {
this._clipboard = new Clipboard(`#${this._id}`, {
text: () => this.props.value
});
this._clipboard.on('success', this.onSuccess);
}
componentDidUpdate() {
const {
showSuccess,
showError
} = this.state;
if (showSuccess || showError) {
this._testResultTimeout = setTimeout(this.resetState, 3000);
}
}
componentWillUnmount() {
if (this._clipboard) {
this._clipboard.destroy();
}
}
//
// Control
resetState = () => {
this.setState({
showSuccess: false,
showError: false
});
}
//
// Listeners
onSuccess = () => {
this.setState({
showSuccess: true
});
}
onError = () => {
this.setState({
showError: true
});
}
//
// Render
render() {
const {
value,
...otherProps
} = this.props;
const {
showSuccess,
showError
} = this.state;
const showStateIcon = showSuccess || showError;
const iconName = showError ? icons.DANGER : icons.CHECK;
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
return (
<FormInputButton
id={this._id}
className={styles.button}
{...otherProps}
>
<span className={showStateIcon && styles.showStateIcon}>
{
showSuccess &&
<span className={styles.stateIconContainer}>
<Icon
name={iconName}
kind={iconKind}
/>
</span>
}
{
<span className={styles.clipboardIconContainer}>
<Icon name={icons.CLIPBOARD} />
</span>
}
</span>
</FormInputButton>
);
}
}
ClipboardButton.propTypes = {
value: PropTypes.string.isRequired
};
export default ClipboardButton;

View file

@ -0,0 +1,16 @@
.button {
composes: link from 'Components/Link/Link.css';
margin: 0 2px;
width: 22px;
border-radius: 4px;
background-color: transparent;
text-align: center;
font-size: inherit;
&:hover {
border: none;
background-color: inherit;
color: $iconButtonHoverColor;
}
}

View file

@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Link from './Link';
import styles from './IconButton.css';
function IconButton(props) {
const {
className,
iconClassName,
name,
kind,
size,
...otherProps
} = props;
return (
<Link
className={className}
{...otherProps}
>
<Icon
className={iconClassName}
name={name}
kind={kind}
size={size}
/>
</Link>
);
}
IconButton.propTypes = {
className: PropTypes.string.isRequired,
iconClassName: PropTypes.string,
kind: PropTypes.string,
name: PropTypes.string.isRequired,
size: PropTypes.number
};
IconButton.defaultProps = {
className: styles.button
};
export default IconButton;

View file

@ -0,0 +1,25 @@
.link {
margin: 0;
padding: 0;
outline: none;
border: 0;
background: none;
color: inherit;
text-align: inherit;
text-decoration: none;
cursor: pointer;
&:global(.isDisabled) {
/*color: $disabledColor;*/
pointer-events: none;
}
}
.to {
color: $linkColor;
&:hover {
color: $linkHoverColor;
text-decoration: underline;
}
}

View file

@ -0,0 +1,101 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import classNames from 'classnames';
import styles from './Link.css';
class Link extends Component {
//
// Listeners
onClick = (event) => {
const {
isDisabled,
onPress
} = this.props;
if (!isDisabled && onPress) {
onPress(event);
}
}
//
// Render
render() {
const {
className,
component,
to,
target,
isDisabled,
noRouter,
onPress,
...otherProps
} = this.props;
const linkProps = { target };
let el = component;
if (to) {
if (/\w+?:\/\//.test(to)) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_blank';
} else if (noRouter) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else if (to.startsWith(window.Sonarr.urlBase)) {
el = RouterLink;
linkProps.to = to;
linkProps.target = target;
} else {
el = RouterLink;
linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
}
if (el === 'button' || el === 'input') {
linkProps.type = otherProps.type || 'button';
linkProps.disabled = isDisabled;
}
linkProps.className = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
const props = {
...otherProps,
...linkProps
};
props.onClick = this.onClick;
return (
React.createElement(el, props)
);
}
}
Link.propTypes = {
className: PropTypes.string,
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
to: PropTypes.string,
target: PropTypes.string,
isDisabled: PropTypes.bool,
noRouter: PropTypes.bool,
onPress: PropTypes.func
};
Link.defaultProps = {
component: 'button',
noRouter: false
};
export default Link;

View file

@ -0,0 +1,37 @@
.button {
composes: button from 'Components/Link/Button.css';
position: relative;
}
.spinnerContainer {
position: absolute;
top: 50%;
left: -100%;
display: inline-flex;
visibility: hidden;
transition: left $defaultSpeed;
transform: translateX(-50%) translateY(-50%);
}
.spinner {
z-index: 1;
}
.label {
position: relative;
left: 0;
transition: left $defaultSpeed, opacity $defaultSpeed;
}
.isSpinning {
.spinnerContainer {
left: 50%;
visibility: visible;
}
.label {
left: 100%;
opacity: 0;
}
}

View file

@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Button from './Button';
import styles from './SpinnerButton.css';
function SpinnerButton(props) {
const {
className,
isSpinning,
isDisabled,
spinnerIcon,
children,
...otherProps
} = props;
return (
<Button
className={classNames(
className,
styles.button,
isSpinning && styles.isSpinning
)}
isDisabled={isDisabled || isSpinning}
{...otherProps}
>
<span className={styles.spinnerContainer}>
<Icon
className={styles.spinner}
name={classNames(
spinnerIcon,
'fa-spin'
)}
/>
</span>
<span className={styles.label}>
{children}
</span>
</Button>
);
}
SpinnerButton.propTypes = {
className: PropTypes.string.isRequired,
isSpinning: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool,
spinnerIcon: PropTypes.string.isRequired,
children: PropTypes.node
};
SpinnerButton.defaultProps = {
className: styles.button,
spinnerIcon: icons.SPINNER
};
export default SpinnerButton;

View file

@ -0,0 +1,23 @@
.iconContainer {
composes: spinnerContainer from 'Components/Link/SpinnerButton.css';
}
.icon {
z-index: 1;
}
.label {
composes: label from 'Components/Link/SpinnerButton.css';
}
.showIcon {
.iconContainer {
left: 50%;
visibility: visible;
}
.label {
left: 100%;
opacity: 0;
}
}

View file

@ -0,0 +1,162 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import SpinnerButton from 'Components/Link/SpinnerButton';
import styles from './SpinnerErrorButton.css';
function getTestResult(error) {
if (!error) {
return {
wasSuccessful: true,
hasWarning: false,
hasError: false
};
}
if (error.status !== 400) {
return {
wasSuccessful: false,
hasWarning: false,
hasError: true
};
}
const failures = error.responseJSON;
const hasWarning = _.some(failures, { isWarning: true });
const hasError = _.some(failures, (failure) => !failure.isWarning);
return {
wasSuccessful: false,
hasWarning,
hasError
};
}
class SpinnerErrorButton extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._testResultTimeout = null;
this.state = {
wasSuccessful: false,
hasWarning: false,
hasError: false
};
}
componentDidUpdate(prevProps) {
const {
isSpinning,
error
} = this.props;
if (prevProps.isSpinning && !isSpinning) {
const testResult = getTestResult(error);
this.setState(testResult, () => {
const {
wasSuccessful,
hasWarning,
hasError
} = testResult;
if (wasSuccessful || hasWarning || hasError) {
this._testResultTimeout = setTimeout(this.resetState, 3000);
}
});
}
}
componentWillUnmount() {
if (this._testResultTimeout) {
clearTimeout(this._testResultTimeout);
}
}
//
// Control
resetState = () => {
this.setState({
wasSuccessful: false,
hasWarning: false,
hasError: false
});
}
//
// Render
render() {
const {
isSpinning,
error,
children,
...otherProps
} = this.props;
const {
wasSuccessful,
hasWarning,
hasError
} = this.state;
const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS;
if (hasWarning) {
iconName = icons.WARNING;
iconKind = kinds.WARNING;
}
if (hasError) {
iconName = icons.DANGER;
iconKind = kinds.DANGER;
}
return (
<SpinnerButton
isSpinning={isSpinning}
{...otherProps}
>
<span className={showIcon && styles.showIcon}>
{
showIcon &&
<span className={styles.iconContainer}>
<Icon
name={iconName}
kind={iconKind}
/>
</span>
}
{
<span className={styles.label}>
{
children
}
</span>
}
</span>
</SpinnerButton>
);
}
}
SpinnerErrorButton.propTypes = {
isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object,
children: PropTypes.node.isRequired
};
export default SpinnerErrorButton;

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons } from 'Helpers/Props';
import IconButton from './IconButton';
function SpinnerIconButton(props) {
const {
name,
spinningName,
isDisabled,
isSpinning,
...otherProps
} = props;
return (
<IconButton
name={isSpinning ? `${spinningName || name} fa-spin` : name}
isDisabled={isDisabled || isSpinning}
{...otherProps}
/>
);
}
SpinnerIconButton.propTypes = {
name: PropTypes.string.isRequired,
spinningName: PropTypes.string.isRequired,
isDisabled: PropTypes.bool.isRequired,
isSpinning: PropTypes.bool.isRequired
};
SpinnerIconButton.defaultProps = {
spinningName: icons.SPINNER,
isDisabled: false,
isSpinning: false
};
export default SpinnerIconButton;