Add favorite and more buttons to currently playing area

This commit is contained in:
metalex9
2019-06-12 13:27:51 -05:00
parent 2bb15bb20f
commit 308f84df0e
27 changed files with 1522 additions and 1243 deletions

View File

@@ -1,3 +1,3 @@
{
"presets": ["react", "env"]
"presets": ["@babel/preset-react", "@babel/preset-env"]
}

2037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,12 +25,12 @@
},
"homepage": "https://github.com/metalex9/generative-music#readme",
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"aws-sdk": "^2.420.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^0.1.19",
"cross-env": "^5.2.0",
"css-loader": "^2.1.1",
@@ -49,6 +49,8 @@
"offline-plugin": "^5.0.6",
"prettier": "^1.16.4",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"sass-loader": "^7.1.0",
"url-loader": "^1.1.2",
"webpack": "^4.29.6",
@@ -88,13 +90,12 @@
"@generative-music/piece-townsend": "^2.12.0",
"@generative-music/piece-trees": "^2.12.0",
"@generative-music/samples.generative.fm": "^1.11.1",
"@generative-music/visualizer": "^1.0.0",
"audiobuffer-to-wav": "^1.0.0",
"classnames": "^2.2.6",
"clone": "^2.1.2",
"nice-color-palettes": "^2.0.0",
"react": "^16.8.4",
"react-device-detect": "^1.6.2",
"react-dom": "^16.8.4",
"react-redux": "^5.1.1",
"react-render-html": "^0.6.0",
"react-router": "^4.3.1",

View File

@@ -5,7 +5,7 @@
.control-button {
width: $buttonWidth;
height: $buttonWidth;
background: $backgroundColor;
background: transparent;
border: none;
position: relative;
cursor: pointer;

View File

@@ -7,7 +7,7 @@ $sideControlWidth: 33%;
background: $backgroundColor;
height: $controlsHeight;
width: 100%;
border-top: 1px solid $primaryColor;
border-top: 1px solid $secondaryColor;
display: flex;
justify-content: space-between;
align-items: center;

View File

@@ -1,4 +1,5 @@
@import '_colors.scss';
@import '_variables.scss';
@mixin textEllipsisOverflow {
overflow: hidden;
@@ -13,16 +14,29 @@
max-width: 100%;
white-space: nowrap;
line-height: 1.5;
font-size: 0.9em;
&__title {
margin-left: 0.5em;
font-size: 0.9em;
color: $primaryColor;
@include textEllipsisOverflow;
}
&__artist {
&__btns {
margin-left: 0.5em;
font-size: 0.8em;
@include textEllipsisOverflow;
&__btn {
margin: 0.2em 0.5em 0;
font-size: 1.25em;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
}
&__visualizer {
max-height: $controlsHeight;
}
}

View File

@@ -1,25 +1,64 @@
import React from 'react';
import React, { useEffect, useState, useRef } from 'react';
import propTypes from 'prop-types';
import VisualizerContainer from '@containers/visualizer.container';
import { react, animators } from '@generative-music/visualizer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHeart, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import pieces from '@pieces';
import artists from '@data/artists';
import FavoriteButton from '@containers/favorite-button.container';
import MoreButton from '@components/shared/more-button';
import './currently-playing.scss';
const CurrentlyPlayingComponent = ({ selectedPieceId }) => {
const { Static, Animated } = react;
const CurrentlyPlayingComponent = ({ selectedPieceId, isPlaying }) => {
const hasSelection = selectedPieceId !== null;
const { artist, title } = hasSelection
? pieces.find(({ id }) => id === selectedPieceId)
: { artist: '', title: '' };
const containerRef = useRef(null);
const [animator, setAnimator] = useState(null);
const [height, setHeight] = useState(null);
useEffect(() => {
if (isPlaying) {
setAnimator(
animators.makeEndlessAnimator({
animationDuration: 30 * 1000,
now: Date.now,
})
);
} else {
setAnimator(null);
}
}, [isPlaying]);
useEffect(() => {
setHeight(containerRef.current.clientHeight);
}, [containerRef]);
const visualizer =
isPlaying && animator !== null ? (
<Animated width={height} height={height} animator={animator} />
) : (
<Static width={height} height={height} />
);
return (
<div className="currently-playing">
<div className="currently-playing" ref={containerRef}>
<div className="currently-playing__visualizer">
{hasSelection && <VisualizerContainer />}
{hasSelection && visualizer}
</div>
<div className="currently-playing__info">
<div className="currently-playing__info__title">{title}</div>
<div className="currently-playing__info__artist">
{artist !== '' ? artists[artist] : artist}
<div className="currently-playing__info__btns">
<FavoriteButton
className="currently-playing__info__btns__btn"
pieceId={selectedPieceId}
/>
<MoreButton
className="currently-playing__info__btns__btn"
pieceId={selectedPieceId}
/>
</div>
</div>
</div>
@@ -28,6 +67,7 @@ const CurrentlyPlayingComponent = ({ selectedPieceId }) => {
CurrentlyPlayingComponent.propTypes = {
selectedPieceId: propTypes.string,
isPlaying: propTypes.bool.isRequired,
};
export default CurrentlyPlayingComponent;

View File

@@ -1,177 +0,0 @@
import svg from 'svg.js';
import colorSchemes from 'nice-color-palettes/1000';
const createVisualization = containerElement => {
const TWO = 2;
const STROKE_WIDTH_DIVISOR = 100;
const LENGTH = 55;
const STROKE_WIDTH = Math.max(LENGTH / STROKE_WIDTH_DIVISOR, 1);
const DIAGONAL = Math.ceil(
Math.sqrt(TWO * LENGTH * LENGTH) + TWO * STROKE_WIDTH
);
const HALF_DIAGONAL = DIAGONAL / TWO;
const HALF_LENGTH = LENGTH / TWO;
const HALF_STROKE_WIDTH = STROKE_WIDTH / TWO;
const ANIMATION_TIME_MS = 10000;
const TRI_CLIP_SIZE = HALF_LENGTH + STROKE_WIDTH;
const P_ROTATE = 0.0375;
const P_RECT = 0.4;
const P_SQR = 0.2;
const P_TRI = 0.1;
const P_50_PCT = 0.5;
const P_FILL = P_50_PCT;
const P_FILL_BLACK = P_50_PCT;
const ROTATION_ANGLE = 90;
const INTERVAL_TIME_MS = ANIMATION_TIME_MS * TWO;
const OFFSET = (DIAGONAL - LENGTH) / TWO;
const getRandomElement = arr => arr[Math.floor(Math.random() * arr.length)];
const getRandomColorScheme = () => getRandomElement(colorSchemes);
let colors;
const setNewColorScheme = () => {
colors = getRandomColorScheme();
};
setNewColorScheme();
const getRandomColor = () =>
colors[Math.floor(Math.random() * colors.length)];
const draw = svg(containerElement).size(DIAGONAL, DIAGONAL);
const stroke = el =>
el.attr({
stroke: 'black',
'stroke-width': STROKE_WIDTH,
'fill-opacity': 0,
});
const strokeAndFill = (el, fill = getRandomColor()) =>
el.attr({
'fill-opacity': 1,
stroke: 'black',
'stroke-width': STROKE_WIDTH,
fill,
});
const shape = el =>
el
.attr({
'fill-opacity': 0,
})
.dmove(OFFSET, OFFSET);
const rect = (w, h) => shape(draw.rect(w, h));
const triClip = sqr =>
sqr
.clone()
.size(TRI_CLIP_SIZE, TRI_CLIP_SIZE)
.dmove(-HALF_STROKE_WIDTH, -HALF_STROKE_WIDTH);
const tri = (x1, y1, x2, y2, clipper) =>
shape(
draw.polygon(`${x1},${y1} ${x2},${y2} ${HALF_LENGTH},${HALF_LENGTH}`)
).clipWith(clipper);
const container = rect(LENGTH, LENGTH);
stroke(container);
const tRect = rect(LENGTH, HALF_LENGTH);
const bRect = rect(LENGTH, HALF_LENGTH).dmove(0, HALF_LENGTH);
const lRect = rect(HALF_LENGTH, LENGTH);
const rRect = rect(HALF_LENGTH, LENGTH).dmove(HALF_LENGTH, 0);
const sqrTl = rect(HALF_LENGTH, HALF_LENGTH);
const sqrTr = rect(HALF_LENGTH, HALF_LENGTH).dmove(HALF_LENGTH, 0);
const sqrBl = rect(HALF_LENGTH, HALF_LENGTH).dmove(0, HALF_LENGTH);
const sqrBr = rect(HALF_LENGTH, HALF_LENGTH).dmove(HALF_LENGTH, HALF_LENGTH);
const tlClipper = draw.clip().add(triClip(sqrTl));
const t1 = tri(0, 0, 0, HALF_LENGTH, tlClipper);
const t2 = tri(0, 0, HALF_LENGTH, 0, tlClipper);
const trClipper = draw.clip().add(triClip(sqrTr));
const t3 = tri(HALF_LENGTH, 0, LENGTH, 0, trClipper);
const t4 = tri(LENGTH, 0, LENGTH, HALF_LENGTH, trClipper);
const blClipper = draw.clip().add(triClip(sqrBl));
const t5 = tri(0, HALF_LENGTH, 0, LENGTH, blClipper);
const t6 = tri(0, LENGTH, HALF_LENGTH, LENGTH, blClipper);
const brClipper = draw.clip().add(triClip(sqrBr));
const t7 = tri(HALF_LENGTH, LENGTH, LENGTH, LENGTH, brClipper);
const t8 = tri(LENGTH, LENGTH, LENGTH, HALF_LENGTH, brClipper);
const rectangles = [tRect, bRect, lRect, rRect];
const squares = [sqrTl, sqrTr, sqrBl, sqrBr];
const triangles = [t1, t2, t3, t4, t5, t6, t7, t8];
const allShapes = rectangles.concat(squares).concat(triangles);
const group = draw.group();
allShapes
.concat([container, tlClipper, trClipper, blClipper, brClipper])
.forEach(s => {
group.add(s);
});
const makeNext = (animate = true, changeScheme = false) => {
if (changeScheme) {
setNewColorScheme();
}
allShapes.concat([group]).forEach(s => s.finish());
if (Math.random() >= P_ROTATE || !animate) {
[[rectangles, P_RECT], [squares, P_SQR], [triangles, P_TRI]].forEach(
([shapes, p]) => {
shapes.forEach(s => {
const el = animate ? s.animate(ANIMATION_TIME_MS, '-') : s;
if (Math.random() < p) {
if (Math.random() < P_FILL) {
stroke(el);
} else if (Math.random() < P_FILL_BLACK) {
strokeAndFill(el, '#000');
} else {
strokeAndFill(el);
}
} else {
el.attr({
'fill-opacity': 0,
'stroke-width': 0,
});
}
});
}
);
} else {
const { rotation } = group.transform();
const dRotation =
Math.random() < P_50_PCT ? -ROTATION_ANGLE : ROTATION_ANGLE;
group
.animate(ANIMATION_TIME_MS, '-')
.rotate(rotation + dRotation, HALF_DIAGONAL, HALF_DIAGONAL);
setNewColorScheme();
}
};
let interval;
return {
start: () => {
makeNext();
interval = setInterval(() => makeNext(), INTERVAL_TIME_MS);
},
stop: () => {
allShapes.concat([group]).forEach(s => s.finish());
allShapes.forEach(s => {
s.attr({
'fill-opacity': 0,
'stroke-width': 0,
});
});
if (interval) {
clearInterval(interval);
}
},
};
};
export default createVisualization;

View File

@@ -1,39 +0,0 @@
import React, { Component, createRef } from 'react';
import propTypes from 'prop-types';
import createVisualization from './create-visualization';
import './visualizer.scss';
class VisualizerComponent extends Component {
constructor(props) {
super(props);
this.containerElement = createRef();
}
render() {
return <div className="visualizer" ref={this.containerElement} />;
}
componentDidMount() {
const visualization = createVisualization(this.containerElement.current);
if (this.props.isPlaying) {
visualization.start();
}
this.setState({
visualization,
});
}
componentWillUnmount() {
this.state.visualization.stop();
}
componentDidUpdate(previousProps) {
if (previousProps.isPlaying && !this.props.isPlaying) {
this.state.visualization.stop();
} else if (!previousProps.isPlaying && this.props.isPlaying) {
this.state.visualization.start();
}
}
}
VisualizerComponent.propTypes = {
isPlaying: propTypes.bool.isRequired,
};
export default VisualizerComponent;

View File

@@ -1,7 +0,0 @@
@import '_colors.scss';
.visualizer {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -28,13 +28,14 @@ const getTimerIcon = ({ lastDurationsMS, remainingMS }) => {
return faHourglassEnd;
};
const makePrimaryButton = (faIcon, onClick) =>
const makePrimaryButton = (faIcon, onClick, title) =>
function PrimaryButtonComponent() {
return (
<ControlButtonComponent
faIcon={faIcon}
onClick={onClick}
isPrimary={true}
title={title}
/>
);
};
@@ -61,8 +62,8 @@ const MainControlsComponent = ({
setIsPopoverOpen(val => !val);
};
const PrimaryButtonComponent = isPlaying
? makePrimaryButton(faStop, onStopClick)
: makePrimaryButton(faPlay, onPlayClick);
? makePrimaryButton(faStop, onStopClick, 'Stop')
: makePrimaryButton(faPlay, onPlayClick, 'Play');
const isTimerButton = el =>
el &&
@@ -94,19 +95,26 @@ const MainControlsComponent = ({
faIcon={getTimerIcon(timer)}
onClick={toggleIsPopoverOpen}
isActive={timer.remainingMS > 0}
title="Play timer..."
/>
</Popover>
<ControlButtonComponent
faIcon={faStepBackward}
onClick={onPreviousClick}
title="Previous piece"
/>
<PrimaryButtonComponent />
<ControlButtonComponent faIcon={faStepForward} onClick={onNextClick} />
<ControlButtonComponent
faIcon={faStepForward}
onClick={onNextClick}
title="Next piece"
/>
{!isMobile && (
<ControlButtonComponent
faIcon={faRandom}
onClick={isShuffleActive ? disableShuffle : enableShuffle}
isActive={isShuffleActive}
title={`${isShuffleActive ? 'Disable' : 'Enable'} shuffle`}
/>
)}
</div>

View File

@@ -23,6 +23,7 @@ const StartTimerContent = ({ lastDurationsMS, startTimer }) => {
type="button"
className="timer-box__durations__item__btn"
onClick={() => handleDurationSelect(durationMS)}
title={`Start ${durationMS / 60000}-minute timer`}
>{`${durationMS / 60000} minutes`}</button>
</li>
))}
@@ -33,6 +34,7 @@ const StartTimerContent = ({ lastDurationsMS, startTimer }) => {
className="timer-box__input"
value={customDuration}
onChange={event => setCustomDuration(event.target.value)}
title="Timer duration in minutes"
/>
minutes
<button
@@ -40,6 +42,11 @@ const StartTimerContent = ({ lastDurationsMS, startTimer }) => {
className="timer-box__btn timer-box__btn--inline"
disabled={isStartDisabled}
onClick={() => handleDurationSelect(customDuration * 60 * 1000)}
title={
isStartDisabled
? 'Please select or enter a duration'
: `Start ${customDuration}-minute timer`
}
>
Start
</button>
@@ -71,6 +78,7 @@ const InProgressContent = ({ remainingMS, updateTimer, cancelTimer }) => {
type="button"
className="timer-box__durations__item__btn"
onClick={() => updateTimer(addMS)}
title={`Add ${addMS / 60000} minutes to timer`}
>{`+ ${addMS / 60 / 1000} minutes`}</button>
</li>
))}
@@ -79,6 +87,7 @@ const InProgressContent = ({ remainingMS, updateTimer, cancelTimer }) => {
type="button"
className="timer-box__btn"
onClick={() => cancelTimer()}
title="Cancel timer and resume endless playback"
>
Cancel Timer
</button>

View File

@@ -4,6 +4,7 @@
.timer-box {
border: 1px solid $primaryColor;
background-color: $backgroundColor;
color: $primaryColor;
padding: 1em;
text-align: center;
@@ -25,16 +26,20 @@
padding: 0.5em 1em;
cursor: pointer;
&:hover {
background: $interactColor;
}
@include hoverStyle(#{&}) {
&:hover {
background: $primaryHoverColor;
}
&:active {
background: $activeInteractColor;
&:active {
background: $primaryActiveColor;
}
}
&:disabled {
background: #ccc;
color: $secondaryColor;
background: $primaryActiveColor;
cursor: not-allowed;
}
}
@@ -57,13 +62,20 @@
border: none;
cursor: pointer;
font-size: 1em;
color: $primaryColor;
// no need for @include hoverStyle since clicking
// will hide these
&:hover {
background-color: #eee;
background-color: #444;
}
&:active {
background-color: #ccc;
background-color: #333;
}
&:focus {
outline: none;
}
}
}

View File

@@ -7,7 +7,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import ControlButton from '../control-button';
const IconComponent = ({ pct, onClick }) => {
const IconComponent = ({ pct, onClick, title }) => {
let icon;
if (pct <= 0) {
icon = faVolumeMute;
@@ -16,12 +16,13 @@ const IconComponent = ({ pct, onClick }) => {
} else {
icon = faVolumeUp;
}
return <ControlButton faIcon={icon} onClick={onClick} />;
return <ControlButton faIcon={icon} onClick={onClick} title={title} />;
};
IconComponent.propTypes = {
pct: propTypes.number.isRequired,
onClick: propTypes.func.isRequired,
title: propTypes.string.isRequired,
};
export default IconComponent;

View File

@@ -8,7 +8,11 @@ const VolumeComponent = ({ volumePct, isMuted, onChange, mute, unmute }) => {
const displayPct = isMuted ? 0 : volumePct;
return (
<div className="volume">
<Icon pct={displayPct} onClick={isMuted ? unmute : mute} />
<Icon
pct={displayPct}
onClick={isMuted ? unmute : mute}
title={isMuted ? 'Unmute' : 'Mute'}
/>
<Slider pct={displayPct} onChange={onChange} />
</div>
);

View File

@@ -72,6 +72,7 @@ const SliderComponent = ({ pct, onChange }) => {
onDragStart={preventDefault}
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
title="Click or drag to change volume"
>
<div
className="slider__bar"

View File

@@ -11,6 +11,9 @@ import {
faEllipsisH,
} from '@fortawesome/free-solid-svg-icons';
import Popover from 'react-tiny-popover';
import IconButton from '@components/shared/icon-button';
import FavoriteButton from '@containers/favorite-button.container';
import MoreButton from '@components/shared/more-button';
import formatPlayTime from './format-play-time';
import defaultImage from '@images/default.png';
import './piece.scss';
@@ -68,7 +71,7 @@ const Piece = ({
};
return (
<div className="piece" key={piece.id} onClick={() => onPieceClick(piece)}>
<div className="piece" key={piece.id}>
<div
className="piece__image"
onClick={
@@ -88,60 +91,14 @@ const Piece = ({
)}
</div>
<div className="piece__btns">
<button
type="button"
className={classNames('piece__btns__btn', 'piece__btns__btn--heart', {
'piece__btns__btn--heart--is-filled': isFavorite,
})}
onClick={
isFavorite
? () => removeFavorite(piece.id)
: () => addFavorite(piece.id)
}
title={isFavorite ? 'Unfavorite' : 'Favorite'}
>
<FontAwesomeIcon icon={faHeart} />
</button>
<button
type="button"
<FavoriteButton className="piece__btns__btn" pieceId={piece.id} />
<IconButton
className="piece__btns__btn"
faIcon={isSelected && (isPlaying || isLoading) ? faStop : faPlay}
onClick={isPlaying ? () => onStopClick() : () => onPlayClick()}
title={isPlaying ? 'Stop' : 'Play'}
>
<FontAwesomeIcon icon={isPlaying && isSelected ? faStop : faPlay} />
</button>
<Popover
isOpen={isMenuOpen}
onClickOutside={() => setIsMenuOpen(false)}
content={
<div className="piece__btns__menu">
<button
type="button"
className="piece__btns__menu__btn"
onClick={() => copyLinkToClipboard()}
>
Copy link to piece
</button>
</div>
}
align="end"
disableReposition="true"
padding="-30"
transitionDuration="0"
>
<button
type="button"
className="piece__btns__btn"
title="More..."
onClick={
isMenuOpen
? () => setIsMenuOpen(false)
: () => setIsMenuOpen(true)
}
>
<FontAwesomeIcon icon={faEllipsisH} />
</button>
</Popover>
/>
<MoreButton className="piece__btns__btn" pieceId={piece.id} />
</div>
<div className="piece__info">
<div className="piece__info__title">

View File

@@ -117,65 +117,7 @@ $pieceWidth: 12em;
box-sizing: border-box;
&__btn {
background: none;
color: $primaryColor;
font-size: 1.25em;
border: none;
cursor: pointer;
&:focus {
outline: none;
}
@include hoverStyle(#{&}) {
&:hover {
color: $primaryHoverColor;
}
&:active {
color: $primaryActiveColor;
}
}
&--heart {
&--is-filled {
color: #e0245e;
@include hoverStyle(#{&}) {
&:hover {
color: #b31c4b;
}
&:active {
color: #861538;
}
}
}
}
}
&__menu {
border: 1px solid $primaryColor;
&__btn {
padding: 0.5em;
background-color: $backgroundColor;
color: $primaryColor;
border: none;
font-size: 0.9em;
cursor: pointer;
// don't need @include hoverStyle since the menu will close on click
&:hover {
background-color: hsl(0, 0%, 20%);
}
&:active {
background-color: hsl(0, 0%, 30%);
}
&:focus {
outline: none;
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import propTypes from 'prop-types';
import { faHeart } from '@fortawesome/free-solid-svg-icons';
import IconButton from './icon-button';
const FavoriteButton = ({
pieceId,
favorites,
addFavorite,
removeFavorite,
className,
}) => {
const isFavorite = favorites.has(pieceId);
return (
<IconButton
faIcon={faHeart}
onClick={
isFavorite ? () => removeFavorite(pieceId) : () => addFavorite(pieceId)
}
title={isFavorite ? 'Unfavorite' : 'Favorite'}
isFilled={isFavorite}
className={className}
/>
);
};
FavoriteButton.propTypes = {
pieceId: propTypes.string.isRequired,
favorites: propTypes.object.isRequired,
addFavorite: propTypes.func.isRequired,
removeFavorite: propTypes.func.isRequired,
className: propTypes.string,
};
export default FavoriteButton;

View File

@@ -0,0 +1,36 @@
@import '_mixins.scss';
@import '_colors.scss';
.icon-button {
padding: 0;
background: none;
color: $primaryColor;
border: none;
cursor: pointer;
&:focus {
outline: none;
}
@include hoverStyle(#{&}) {
&:hover {
color: $primaryHoverColor;
}
&:active {
color: $primaryActiveColor;
}
}
&--is-filled {
color: #e0245e;
@include hoverStyle(#{&}) {
&:hover {
color: #b31c4b;
}
&:active {
color: #861538;
}
}
}
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import propTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import './icon-button.scss';
const IconButton = ({
faIcon,
onClick,
title,
className = '',
isFilled = false,
}) => (
<button
type="button"
title={title}
onClick={onClick}
className={classNames('icon-button', className, {
'icon-button--is-filled': isFilled,
})}
>
<FontAwesomeIcon icon={faIcon} />
</button>
);
IconButton.propTypes = {
faIcon: propTypes.object.isRequired,
onClick: propTypes.func.isRequired,
title: propTypes.string.isRequired,
className: propTypes.string,
isFilled: propTypes.bool,
};
export default IconButton;

View File

@@ -0,0 +1,68 @@
import React, { useState } from 'react';
import propTypes from 'prop-types';
import Popover from 'react-tiny-popover';
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import IconButton from '@components/shared/icon-button';
import './more-button.scss';
const execCommandCopy = text => {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
};
const clipboardApiCopy = text => navigator.clipboard.writeText(text);
const copyFn = navigator.clipboard ? clipboardApiCopy : execCommandCopy;
const MoreButton = ({ pieceId, className }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const copyLinkToClipboard = () => {
const link = `${location.origin}/music/${pieceId}`;
copyFn(link);
setIsMenuOpen(false);
};
return (
<Popover
isOpen={isMenuOpen}
onClickOutside={() => setIsMenuOpen(false)}
content={
<div className="more-btn__menu">
<button
type="button"
className="more-btn__menu__btn"
onClick={() => copyLinkToClipboard()}
>
Copy link to piece
</button>
</div>
}
align="end"
disableReposition="true"
padding="-25"
transitionDuration="0"
>
<IconButton
faIcon={faEllipsisH}
className={className}
title="More..."
onClick={
isMenuOpen ? () => setIsMenuOpen(false) : () => setIsMenuOpen(true)
}
/>
</Popover>
);
};
MoreButton.propTypes = {
pieceId: propTypes.string.isRequired,
className: propTypes.string,
};
export default MoreButton;

View File

@@ -0,0 +1,29 @@
@import '_colors.scss';
.more-btn {
&__menu {
border: 1px solid $primaryColor;
&__btn {
padding: 0.5em;
background-color: $backgroundColor;
color: $primaryColor;
border: none;
font-size: 0.9em;
cursor: pointer;
// don't need @include hoverStyle since the menu will close on click
&:hover {
background-color: hsl(0, 0%, 20%);
}
&:active {
background-color: hsl(0, 0%, 30%);
}
&:focus {
outline: none;
}
}
}
}

View File

@@ -1,8 +1,9 @@
import { connect } from 'react-redux';
import CurrentlyPlayingComponent from '../components/app/controls/currently-playing';
const mapStateToProps = ({ selectedPieceId }) => ({
const mapStateToProps = ({ selectedPieceId, isPlaying }) => ({
selectedPieceId,
isPlaying,
});
export default connect(mapStateToProps)(CurrentlyPlayingComponent);

View File

@@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import FavoriteButton from '@components/shared/favorite-button';
import addFavorite from '@store/actions/creators/add-favorite.creator';
import removeFavorite from '@store/actions/creators/remove-favorite.creator';
const mapStateToProps = ({ favorites }) => ({ favorites });
export default connect(
mapStateToProps,
{ addFavorite, removeFavorite }
)(FavoriteButton);

View File

@@ -1,6 +0,0 @@
import { connect } from 'react-redux';
import VisualizerComponent from '../components/app/controls/currently-playing/visualizer';
const mapStateToProps = ({ isPlaying }) => ({ isPlaying });
export default connect(mapStateToProps)(VisualizerComponent);

View File

@@ -15,7 +15,7 @@ const log = msg => console.log(msg);
const makeConfig = alias => ({
mode: 'development',
devtool: 'sourcemap',
entry: ['babel-polyfill', './src'],
entry: ['@babel/polyfill', './src'],
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[hash].js',