├── .gitignore ├── README.md ├── README.old.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.js ├── components │ ├── Album.js │ ├── AlbumList.js │ ├── Background.js │ ├── DetailPlaying.js │ ├── InputForm.js │ └── Player.js ├── global-styles.js ├── handleState │ ├── handlePlayer.js │ └── handleStatePlayList.js └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![gif](https://user-images.githubusercontent.com/19645646/52931678-8f0fdf00-3390-11e9-826e-fa540ae2e67f.gif) 2 | 3 | 해당 프로젝트는 React Hook을 공부하기 위해 진행된 사이드 프로젝트입니다. 4 | 5 | ## HookPlayer 6 | 7 | React 의 Hook 기능이 드디어 정상 기능으로 탑재되었습니다. 8 | 9 | 해당 Hook 기능을 사용해보기 위해서, 작은 사이드 프로젝트를 진행해보았습니다. 10 | 11 | Youtube 주소 또는 Video ID 를 활용하여 자신만의 플레이 리스트를 만들고, 12 | 13 | 이를 Local-Storage에 보관, 동기화하여 플레이 리스트가 유지되도록 개발하였습니다. 14 | 15 | Hook에 대해 사용해본 경험은 없었으나, 생각보다도 열띈 반응에 Hook에 대해 알아보고, 16 | 17 | 이해를 돕고자 해당 프로젝트를 진행하였습니다. 18 | 19 | 20 | ### `yarn` 21 | 22 | 명령어 yarn 을 사용하여, 필요한 모듈을 설치합니다. 23 | 24 | ### `yarn start` 25 | 26 | 이후 yarn start로 https://localhost:3000 에서 HookPlayer를 만나보실 수 있습니다. 27 | -------------------------------------------------------------------------------- /README.old.md: -------------------------------------------------------------------------------- 1 | # hookPlayer-dev4us -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hook-player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://dev4us.github.io/hook-player/", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-svg-core": "^1.2.15", 8 | "@fortawesome/free-regular-svg-icons": "^5.7.2", 9 | "@fortawesome/free-solid-svg-icons": "^5.7.2", 10 | "@fortawesome/react-fontawesome": "^0.1.4", 11 | "axios": "^0.18.0", 12 | "gh-pages": "^2.0.1", 13 | "react": "next", 14 | "react-dom": "next", 15 | "react-favicon": "^0.0.14", 16 | "react-gh-corner": "^1.1.2", 17 | "react-helmet": "^5.2.0", 18 | "react-scripts": "2.1.5", 19 | "react-toastify": "^4.5.2", 20 | "react-yt": "^0.1.2", 21 | "styled-components": "^4.1.3", 22 | "styled-reset": "^1.6.8", 23 | "youtube-duration-format": "^0.2.0" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject", 30 | "predeploy": "npm run build", 31 | "deploy": "gh-pages -d build" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": [ 37 | ">0.2%", 38 | "not dead", 39 | "not ie <= 11", 40 | "not op_mini all" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev4us/hook-player/b7dcc9ec26a73f93d2ba4cdef3c1571136393fb2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Helmet } from "react-helmet"; 4 | import Favicon from "react-favicon"; 5 | 6 | // For handle State 7 | import handleStatePlayList from "./handleState/handleStatePlayList"; 8 | import handlePlayer from "./handleState/handlePlayer"; 9 | 10 | // Components 11 | import InputForm from "./components/InputForm"; 12 | import AlbumList from "./components/AlbumList"; 13 | import Player from "./components/Player"; 14 | import Background from "./components/Background"; 15 | import GHCorner from "react-gh-corner"; 16 | 17 | const MainFrame = styled.div` 18 | display: flex; 19 | 20 | justify-content: center; 21 | align-items: center; 22 | position: absolute; 23 | width: 100%; 24 | height: 100%; 25 | 26 | top: 0; 27 | left: 0; 28 | z-index: 2; 29 | `; 30 | 31 | const BodyFrame = styled.div` 32 | display: flex; 33 | justify-content: center; 34 | flex-wrap: wrap; 35 | height: 80%; 36 | width: 70%; 37 | @media (max-width: 500px) { 38 | width: 90%; 39 | } 40 | `; 41 | 42 | const DetailPlayFrame = styled.div` 43 | /*flex: 1;*/ 44 | width: 700px; 45 | height: 100%; 46 | @media (max-width: 985px) { 47 | height: 66%; 48 | } 49 | `; 50 | const ControllerPlayFrame = styled.div` 51 | display: flex; 52 | flex-direction: column; 53 | flex: 1; 54 | height: 100%; 55 | background: rgba(50, 50, 50, 0.5); 56 | -webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12), 57 | 0 3px 6px rgba(0, 0, 0, 0.1725); 58 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.1725); 59 | @media (max-width: 986px) { 60 | margin-top: 20%; 61 | margin-bottom: 30px; 62 | } 63 | `; 64 | const ControllerTitle = styled.div` 65 | font-size: 1.2rem; 66 | color: white; 67 | padding-top: 30px; 68 | padding-left: 30px; 69 | `; 70 | 71 | function App() { 72 | const { 73 | statePlayList, 74 | addStatePlayList, 75 | deleteStatePlayList 76 | } = handleStatePlayList(); 77 | const { nowPlaying, setNowPlaying } = handlePlayer(statePlayList[0]); 78 | 79 | return ( 80 |
81 | 82 | 83 | HookPlayer - dev4us 84 | 85 | 86 | 91 | 92 | 93 | 94 | 99 | 100 | 101 | 102 | 109 | 113 | 120 | 121 | 122 | 123 | 127 | 133 | 134 | 135 | 136 |
137 | ); 138 | } 139 | 140 | export default App; 141 | -------------------------------------------------------------------------------- /src/components/Album.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { keyframes, css } from "styled-components"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faTrash } from "@fortawesome/free-solid-svg-icons"; 5 | import { toast } from "react-toastify"; 6 | 7 | const AlbumItem = styled.div` 8 | display: flex; 9 | position: relative; 10 | align-items: center; 11 | border-bottom: 1px dashed white; 12 | width: 100%; 13 | height: 100px; 14 | padding-left: 15px; 15 | cursor: pointer; 16 | ${props => 17 | props.isPlaying && 18 | css` 19 | background: rgba(228, 43, 43, 0.4); 20 | `}; 21 | 22 | &:hover { 23 | background: rgba(0, 0, 0, 0.5); 24 | } 25 | `; 26 | 27 | const rotate = keyframes` 28 | 25%{ 29 | transform:rotate(90deg); 30 | } 31 | 50%{ 32 | transform:rotate(180deg); 33 | } 34 | 75%{ 35 | transform:rotate(270deg); 36 | } 37 | 100%{ 38 | transform:rotate(360deg); 39 | } 40 | `; 41 | const AlbumThumbnail = styled.div` 42 | display: block; 43 | width: 70px; 44 | height: 75%; 45 | border-radius: 100%; 46 | background-image: url(${props => props.background}); 47 | background-size: cover; 48 | background-position: bottom; 49 | margin-right: 40px; 50 | ${props => 51 | props.isPlaying && 52 | css` 53 | animation: ${rotate} 5s linear infinite; 54 | `}; 55 | @media (max-width: 500px) { 56 | width: 55px; 57 | height: 55px; 58 | margin-right: 10px; 59 | } 60 | `; 61 | const SubContainer = styled.div` 62 | display: block; 63 | float: right; 64 | width: 80%; 65 | height: 100%; 66 | `; 67 | 68 | const SubText = styled.div` 69 | display: flex; 70 | align-items: center; 71 | float: left; 72 | width: 80%; 73 | height: 100%; 74 | `; 75 | const SubIcon = styled.div` 76 | display: flex; 77 | align-items: center; 78 | float: right; 79 | width: 20%; 80 | height: 100%; 81 | `; 82 | const SubSongName = styled.a` 83 | color: white; 84 | font-size: 15px; 85 | font-weight: bold; 86 | `; 87 | 88 | const SubDuration = styled.a` 89 | font-size: 15px; 90 | color: white; 91 | margin-left: 5px; 92 | `; 93 | 94 | const DeleteBtn = styled(FontAwesomeIcon)` 95 | position: absolute; 96 | right: 1rem; 97 | font-size: 1.2rem; 98 | cursor: pointer; 99 | color: #8c8c8c; 100 | &:hover { 101 | color: white; 102 | } 103 | `; 104 | 105 | const Album = ({ 106 | thumbnail, 107 | movieId, 108 | statePlayList, 109 | nowPlaying, 110 | setNowPlaying, 111 | deleteStatePlayList, 112 | songName, 113 | singer, 114 | duration 115 | }) => { 116 | const isPlaying = movieId === nowPlaying.videoKey; 117 | const getFullName = () => { 118 | if (songName !== "" && songName !== undefined) { 119 | if (songName.length > 30) { 120 | songName = songName.substring(0, 30) + "..."; 121 | } 122 | return songName + " - " + singer; 123 | } else { 124 | return null; 125 | } 126 | }; 127 | 128 | return ( 129 | 131 | setNowPlaying(statePlayList.find(val => val.videoKey === movieId)) 132 | } 133 | isPlaying={isPlaying} 134 | > 135 | 136 | 137 | 138 | {getFullName()} 139 | {`(${duration})`} 140 | 141 | 142 | { 145 | e.stopPropagation(); 146 | if (deleteStatePlayList(movieId) !== true) { 147 | toast.error("하나의 항목은 유지하여야 합니다."); 148 | } 149 | }} 150 | /> 151 | 152 | 153 | 154 | ); 155 | }; 156 | 157 | export default Album; 158 | -------------------------------------------------------------------------------- /src/components/AlbumList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Album from "./Album"; 4 | 5 | const AlbumContainer = styled.div` 6 | flex: 1; 7 | height: 100%; 8 | overflow-y: scroll; 9 | margin-top: 10%; 10 | `; 11 | 12 | const AlbumList = ({ 13 | statePlayList, 14 | nowPlaying, 15 | setNowPlaying, 16 | deleteStatePlayList 17 | }) => { 18 | return ( 19 | 20 | {statePlayList.map((data, index) => ( 21 | 33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | export default AlbumList; 39 | -------------------------------------------------------------------------------- /src/components/Background.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Component = styled.div` 5 | display: flex; 6 | position: relative; 7 | width: 100%; 8 | height: 100%; 9 | top: 0; 10 | left: 0; 11 | 12 | background-image: url(${props => props.backgroundURL}); 13 | background-position: center; 14 | background-size: cover; 15 | 16 | filter: blur(8px) opacity(0.8); 17 | -webkit-filter: blur(8px) opacity(0.8); 18 | z-index: 1; 19 | `; 20 | 21 | const Background = ({ backgroundURL }) => { 22 | return ; 23 | }; 24 | 25 | export default Background; 26 | -------------------------------------------------------------------------------- /src/components/DetailPlaying.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | flex: 1; 7 | align-items: center; 8 | flex-direction: column; 9 | width: 100%; 10 | `; 11 | 12 | const PlayingThumbnail = styled.img` 13 | width: 85%; 14 | height: 70%; 15 | -webkit-box-shadow: 0 15px 14px rgba(0, 0, 0, 0.5), 16 | 0 25px 25px rgba(0, 0, 0, 0.075); 17 | box-shadow: 0 15px 14px rgba(0, 0, 0, 0.5), 0 25px 25px rgba(0, 0, 0, 0.075); 18 | `; 19 | 20 | const SongTitle = styled.p` 21 | margin-top: 1.3rem; 22 | color: white; 23 | font-size: 1.2rem; 24 | letter-spacing: 1px; 25 | `; 26 | 27 | const SingerTitle = styled.p` 28 | margin-top: 0.5rem; 29 | color: #cbcbcb; 30 | font-size: 1rem; 31 | `; 32 | 33 | const DetailPlaying = ({ nowPlaying }) => { 34 | const { max_thumbnail, songName, singer } = nowPlaying; 35 | let afterSongName = ""; 36 | 37 | const getSongName = () => { 38 | if (songName !== "" && songName !== undefined) { 39 | if (songName.length > 30) { 40 | afterSongName = songName.substring(0, 30) + "..."; 41 | } 42 | return afterSongName; 43 | } else { 44 | return null; 45 | } 46 | }; 47 | return ( 48 | 49 | 50 | {getSongName()} 51 | {singer} 52 | 53 | ); 54 | }; 55 | 56 | export default DetailPlaying; 57 | -------------------------------------------------------------------------------- /src/components/InputForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled, { keyframes } from "styled-components"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faPlus } from "@fortawesome/free-solid-svg-icons"; 5 | import axios from "axios"; 6 | import ytDurationFormat from "youtube-duration-format"; 7 | 8 | const Container = styled.div` 9 | display: flex; 10 | align-items: baseline; 11 | width: 100%; 12 | height: 5%; 13 | margin-top: 3%; 14 | `; 15 | 16 | const Subscription = styled.span` 17 | width: 15%; 18 | margin-left: 2%; 19 | margin-right: 2%; 20 | font-size: 1rem; 21 | font-weight: bold; 22 | color: #ececec; 23 | `; 24 | 25 | const SearchBtnHover = keyframes` 26 | 100%{ 27 | background:rgba(255, 255, 255, 0.5); 28 | box-shadow:none; 29 | } 30 | `; 31 | 32 | const SearchBtnFrame = styled.span` 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | cursor: pointer; 37 | width: 10%; 38 | height: 40px; 39 | border: 2px solid white; 40 | border-radius: 10px; 41 | 42 | -webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12), 43 | 0 3px 6px rgba(0, 0, 0, 0.1725); 44 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.1725); 45 | 46 | &:hover { 47 | animation: ${SearchBtnHover} 0.1s ease; 48 | animation-fill-mode: forwards; 49 | } 50 | `; 51 | 52 | const SearchBtn = styled(FontAwesomeIcon)` 53 | font-size: 1.2rem; 54 | cursor: pointer; 55 | color: #dcdcdc; 56 | `; 57 | 58 | const SearchInput = styled.input` 59 | background: none; 60 | width: 70%; 61 | height: 100%; 62 | color: #ececec; 63 | border: none; 64 | border-bottom: 2px solid #ececec; 65 | `; 66 | 67 | const InputForm = ({ statePlayList, addStatePlayList }) => { 68 | const [inputUrl, setUrl] = useState(""); 69 | 70 | const getMovieData = async () => { 71 | if (!inputUrl) { 72 | alert("URL 이나 VideoId를 입력해주세요."); 73 | return false; 74 | } 75 | 76 | const fullUrl = inputUrl; 77 | let video_id = ""; 78 | 79 | if (fullUrl.includes("v=") === false) { 80 | video_id = fullUrl; 81 | } else { 82 | video_id = fullUrl.split("v=")[1]; 83 | let ampersandPosition = video_id.indexOf("&"); 84 | 85 | if (ampersandPosition !== -1) { 86 | video_id = video_id.substring(0, ampersandPosition); 87 | } 88 | } 89 | 90 | const API_KEY = "AIzaSyCC2pMVczqa5crA9qxUFnceNC_0p2gV7gg"; 91 | const API_URL = `https://www.googleapis.com/youtube/v3/videos?id=${video_id}&key=${API_KEY}&part=snippet,contentDetails,statistics,status`; 92 | const res = await axios.get(API_URL); 93 | setUrl(""); 94 | 95 | if (res.status === 200 && res.data.items.length >= 1) { 96 | const highestThumbnail = () => { 97 | let returnData = ""; 98 | 99 | if (res.data.items[0].snippet.thumbnails.maxres) { 100 | returnData = res.data.items[0].snippet.thumbnails.maxres.url; 101 | } else if (res.data.items[0].snippet.thumbnails.high) { 102 | returnData = res.data.items[0].snippet.thumbnails.high.url; 103 | } else if (res.data.items[0].snippet.thumbnails.medium) { 104 | returnData = res.data.items[0].snippet.thumbnails.medium.url; 105 | } 106 | return returnData; 107 | }; 108 | const returnData = [ 109 | { 110 | id: statePlayList.length || 0, 111 | songName: res.data.items[0].snippet.title, 112 | singer: res.data.items[0].snippet.channelTitle, 113 | videoKey: res.data.items[0].id, 114 | thumbnail: highestThumbnail(), 115 | max_thumbnail: highestThumbnail(), 116 | duration: ytDurationFormat(res.data.items[0].contentDetails.duration) 117 | } 118 | ]; 119 | 120 | if (statePlayList.find(val => val.videoKey === res.data.items[0].id)) { 121 | alert("이미 등록된 음원입니다"); 122 | return false; 123 | } 124 | 125 | addStatePlayList(returnData); 126 | } else { 127 | alert("존재하지 않거나 적절하지 않은 주소입니다."); 128 | } 129 | }; 130 | 131 | return ( 132 | 133 | Youtube Link: 134 | setUrl(e.target.value)} 137 | placeholder={`Insert Youtube URL or videoId`} 138 | /> 139 | getMovieData()}> 140 | 141 | 142 | 143 | ); 144 | }; 145 | 146 | export default InputForm; 147 | -------------------------------------------------------------------------------- /src/components/Player.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DetailPlaying from "./DetailPlaying"; 3 | import YouTube from "react-yt"; 4 | import styled from "styled-components"; 5 | import { toast } from "react-toastify"; 6 | 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | import { 9 | faPlayCircle, 10 | faPauseCircle, 11 | faChevronCircleLeft, 12 | faChevronCircleRight, 13 | faVolumeMute, 14 | faVolumeUp 15 | } from "@fortawesome/free-solid-svg-icons"; 16 | 17 | const Container = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | `; 22 | const LeftFrame = styled.div` 23 | width: 100%; 24 | height: 100%; 25 | `; 26 | 27 | const PlayerHiddenFrame = styled.div` 28 | display: none; 29 | width: 1px; 30 | height: 1px; 31 | overflow: hidden; 32 | `; 33 | const Controller = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | width: 80%; 38 | padding-top: 5%; 39 | `; 40 | const DurationStatus = styled.div` 41 | width: 100%; 42 | height: 35px; 43 | color: white; 44 | font-size: 0.8rem; 45 | text-align: right; 46 | letter-spacing: 0.1rem; 47 | `; 48 | 49 | const DurationBar = styled.input` 50 | -webkit-appearance: none; 51 | width: 100%; 52 | height: 10px; 53 | border-radius: 5px; 54 | background: rgba(255, 255, 255, 0.5); 55 | outline: none; 56 | cursor: pointer; 57 | 58 | &::-webkit-slider-thumb { 59 | -webkit-appearance: none; 60 | appearance: none; 61 | width: 5px; 62 | height: 20px; 63 | background: white; 64 | cursor: pointer; 65 | } 66 | &::-moz-range-thumb { 67 | width: 5px; 68 | height: 20px; 69 | background: white; 70 | cursor: pointer; 71 | } 72 | `; 73 | 74 | const VolumeBar = styled.input` 75 | -webkit-appearance: none; 76 | width: 40%; 77 | height: 10px; 78 | border-radius: 5px; 79 | background: rgba(255, 255, 255, 0.5); 80 | outline: none; 81 | cursor: pointer; 82 | 83 | &::-webkit-slider-thumb { 84 | -webkit-appearance: none; 85 | appearance: none; 86 | width: 5px; 87 | height: 20px; 88 | background: white; 89 | cursor: pointer; 90 | } 91 | &::-moz-range-thumb { 92 | width: 5px; 93 | height: 20px; 94 | background: white; 95 | cursor: pointer; 96 | } 97 | `; 98 | 99 | const RemotePlayer = styled.div` 100 | display: flex; 101 | flex-direction: row; 102 | align-items: center; 103 | width: 100%; 104 | `; 105 | const PlayPauseBtn = styled(FontAwesomeIcon)` 106 | cursor: pointer; 107 | font-size: 3rem; 108 | color: white; 109 | margin-left: 5px; 110 | margin-right: 5px; 111 | `; 112 | 113 | const ArrowBtn = styled(FontAwesomeIcon)` 114 | cursor: pointer; 115 | font-size: 2rem; 116 | color: white; 117 | `; 118 | 119 | const VolumnIcon = styled(FontAwesomeIcon)` 120 | font-size: 1rem; 121 | color: white; 122 | cursor: pointer; 123 | `; 124 | 125 | const SongController = styled.div` 126 | display: flex; 127 | align-items: center; 128 | justify-content: flex-end; 129 | flex: 6.25; 130 | `; 131 | const VolumeController = styled.div` 132 | display: flex; 133 | align-items: center; 134 | justify-content: flex-end; 135 | flex: 3.75; 136 | `; 137 | 138 | const Player = ({ nowPlaying, statePlayList, setNowPlaying }) => { 139 | return ( 140 | <> 141 | 142 | { 159 | const isPlaying = getPlayerState() === 1; 160 | const currentTime = getCurrentTime(); 161 | const duration = getDuration(); 162 | const nowIndex = statePlayList.findIndex( 163 | val => val.videoKey === nowPlaying.videoKey 164 | ); 165 | const formatTime = time => { 166 | if (time === 0) { 167 | return "0:00"; 168 | } 169 | const minutes = Math.floor(time / 60); 170 | const seconds = time - minutes * 60; 171 | return `${minutes}:${seconds < 10 ? "0" + seconds : seconds}`; 172 | }; 173 | const getProcessPer = (current, duration) => { 174 | if ( 175 | typeof current === undefined || 176 | typeof duration === undefined || 177 | duration === 0 178 | ) { 179 | return 0; 180 | } 181 | 182 | if (current === duration && duration !== 0) { 183 | let nextIndex = Math.floor( 184 | Math.random() * statePlayList.length 185 | ); 186 | 187 | setNowPlaying(statePlayList[nextIndex]); 188 | } 189 | 190 | return Math.ceil((current / duration) * 100); 191 | }; 192 | 193 | const moveCurrent = (value, duration) => { 194 | if (typeof value !== undefined && typeof duration !== undefined) { 195 | playVideo(); 196 | seekTo(Math.ceil((value / 100) * duration)); 197 | } 198 | }; 199 | const prevPlay = nowIndex => { 200 | // turn nextMusic 201 | if (0 === nowIndex) { 202 | toast.error("이전 항목이 없습니다."); 203 | } else { 204 | setNowPlaying(statePlayList[nowIndex - 1]); 205 | } 206 | }; 207 | const nextPlay = nowIndex => { 208 | // turn nextMusic 209 | if (statePlayList.length - 1 === nowIndex) { 210 | toast.error("다음 항목이 없습니다."); 211 | } else { 212 | setNowPlaying(statePlayList[nowIndex + 1]); 213 | } 214 | }; 215 | 216 | return ( 217 | 218 | {iframe} 219 | 220 | 221 | 222 | {formatTime(currentTime)} / {formatTime(duration)} 223 | moveCurrent(e.target.value, duration)} 229 | /> 230 | 231 | 232 | 233 | 234 | { 237 | prevPlay(nowIndex); 238 | }} 239 | /> 240 | {isPlaying ? ( 241 | pauseVideo()} 244 | /> 245 | ) : ( 246 | playVideo()} 249 | /> 250 | )} 251 | { 254 | nextPlay(nowIndex); 255 | }} 256 | /> 257 | 258 | 259 | {isMuted() && ( 260 | unMute()} 263 | /> 264 | )} 265 | {!isMuted() && ( 266 | mute()} 269 | /> 270 | )} 271 | setVolume(e.target.value)} 277 | /> 278 | 279 | 280 | 281 | 282 | ); 283 | }} 284 | /> 285 | 286 | 287 | ); 288 | }; 289 | 290 | export default Player; 291 | -------------------------------------------------------------------------------- /src/global-styles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | import reset from "styled-reset"; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | @import url('https://fonts.googleapis.com/css?family=PT+Sans:400,700'); 6 | ${reset} 7 | .App, .root{ 8 | width:100%; 9 | height:100%; 10 | } 11 | * { 12 | box-sizing: border-box; 13 | } 14 | *:focus{ 15 | outline:none; 16 | } 17 | html,body{ 18 | width:100%; 19 | height:100%; 20 | } 21 | body{ 22 | font-family: 'PT Sans', sans-serif, -apple-system,system-ui,BlinkMacSystemFont; 23 | font-weight:400; 24 | } 25 | a{ 26 | color:inherit; 27 | text-decoration:none; 28 | } 29 | input, 30 | button{ 31 | &.focus, 32 | &.active{outline:none} 33 | } 34 | h1,h2,h3,h4,h5,h6{ 35 | font-family:'Maven Pro', sans-serif; 36 | } 37 | `; 38 | 39 | export default GlobalStyle; 40 | -------------------------------------------------------------------------------- /src/handleState/handlePlayer.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const handlePlayer = initial => { 4 | const [nowPlaying, setNowPlaying] = useState(initial); 5 | 6 | return { 7 | nowPlaying, 8 | setNowPlaying 9 | }; 10 | }; 11 | 12 | export default handlePlayer; 13 | -------------------------------------------------------------------------------- /src/handleState/handleStatePlayList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const handleStatePlayList = () => { 4 | const initialPlayList = [ 5 | { 6 | id: 0, 7 | songName: 8 | "We Can't Stop - Miley Cyrus (Boyce Avenue feat. Bea Miller cover) on Spotify & Apple", 9 | singer: "Boyce Avenue", 10 | videoKey: "uzgp65UnPxA", 11 | thumbnail: "https://i.ytimg.com/vi/uzgp65UnPxA/maxresdefault.jpg", 12 | max_thumbnail: "https://i.ytimg.com/vi/uzgp65UnPxA/maxresdefault.jpg", 13 | duration: "04:30" 14 | }, 15 | { 16 | id: 1, 17 | songName: "Charlie Puth - Attention [Official Video]", 18 | singer: "Charlie Puth", 19 | videoKey: "nfs8NYg7yQM", 20 | thumbnail: "https://i.ytimg.com/vi/nfs8NYg7yQM/hqdefault.jpg", 21 | max_thumbnail: "https://i.ytimg.com/vi/nfs8NYg7yQM/hqdefault.jpg", 22 | duration: "03:52" 23 | }, 24 | { 25 | id: 2, 26 | songName: "Pharrell Williams - Happy (Official Music Video)", 27 | singer: "PharrellWilliamsVEVO", 28 | videoKey: "ZbZSe6N_BXs", 29 | thumbnail: "https://i.ytimg.com/vi/ZbZSe6N_BXs/maxresdefault.jpg", 30 | max_thumbnail: "https://i.ytimg.com/vi/ZbZSe6N_BXs/maxresdefault.jpg", 31 | duration: "04:01" 32 | } 33 | ]; 34 | 35 | const getPlayList = localStorage.getItem("localPlayList") 36 | ? JSON.parse(localStorage.getItem("localPlayList")) 37 | : initialPlayList; 38 | 39 | const [statePlayList, setStatePlayList] = useState(getPlayList); 40 | 41 | useEffect(() => { 42 | localStorage.setItem("localPlayList", JSON.stringify(statePlayList)); 43 | }); 44 | 45 | return { 46 | statePlayList, 47 | addStatePlayList: contentsData => { 48 | let beforeStateList = statePlayList; 49 | let nextStateList = beforeStateList.concat(contentsData); 50 | setStatePlayList(nextStateList); 51 | }, 52 | deleteStatePlayList: conetentsKey => { 53 | if (statePlayList.length !== 1) { 54 | const afterStatePlayList = statePlayList.filter( 55 | (val, index) => val.videoKey !== conetentsKey 56 | ); 57 | setStatePlayList(afterStatePlayList); 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | }; 64 | }; 65 | 66 | export default handleStatePlayList; 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import GlobalStyle from "./global-styles"; 5 | import { ToastContainer } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.min.css"; 7 | 8 | ReactDOM.render( 9 | <> 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | --------------------------------------------------------------------------------