Skip to content
This repository has been archived by the owner on Aug 24, 2020. It is now read-only.

提出PR #51

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b635e21
add: react + reduxの環境構築
ffjlabo Feb 20, 2020
9d1cc9a
画面遷移実装
ffjlabo Feb 23, 2020
e0ba64e
コミット漏れ: react-router-domの導入
ffjlabo Feb 24, 2020
e0267e2
youtube認証用サーバの作成
ffjlabo Feb 25, 2020
b1f7fe6
cookieのアクセストークンの有無に応じてページの表示を切り替え
ffjlabo Feb 25, 2020
ef29d28
action名を定義
ffjlabo Feb 28, 2020
1df482f
プレイリスト一覧用reducerを作成
ffjlabo Feb 29, 2020
df69b23
redux-thunk,axios,js-cookieの導入
ffjlabo Feb 29, 2020
a7da1f7
プレイリストデータ取得処理の実装
ffjlabo Feb 29, 2020
3c4923f
プレイリスト一覧画面とstoreつなぎこみ
ffjlabo Feb 29, 2020
61ead0a
プレイリストデータを用いて一覧表示できるよう修正
ffjlabo Feb 29, 2020
caf43c9
プレイリスト詳細画面へのリンクを追加
ffjlabo Feb 29, 2020
9ab25a1
プレイリスト詳細データ用のreducer作成
ffjlabo Feb 29, 2020
fd7cc7a
API取得clientをutilに切り出し
ffjlabo Feb 29, 2020
ce47d28
プレイリスト詳細データを取得する処理を別ファイルに切り出し
ffjlabo Feb 29, 2020
459045a
プレイリスト詳細画面にてAPIから取得したデータを表示できるよう修正
ffjlabo Feb 29, 2020
3c7892c
cookieの持続時間を1日に変更
ffjlabo Feb 29, 2020
bcc87c2
cookieの時間をexpairs_inを元に設定
ffjlabo Feb 29, 2020
6de837d
HeaderとHomeコンポーネントを削除
ffjlabo Feb 29, 2020
cf07edf
プレイリスト追加ロジックの実装
ffjlabo Feb 29, 2020
822e2d7
axiosをダウングレード
ffjlabo Feb 29, 2020
2741027
プレイリスト追加用コンポーネントを実装
ffjlabo Feb 29, 2020
223675b
プレイリストに動画を追加する処理を実装
ffjlabo Feb 29, 2020
4be587f
プレースホルダー設定
ffjlabo Feb 29, 2020
5594860
README.md追加
ffjlabo Feb 29, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions ffjlabo/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"presets": [
"@babel/preset-react",
[
"@babel/preset-env",
{
"targets": {
"node": true
}
}
]
]
}
4 changes: 4 additions & 0 deletions ffjlabo/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CLIENT_ID=xxxxxxxxxxxxxx.apps.googleusercontent.com
CLIENT_SECRET=xxxxxxxxxxxxx
API_ENDPOINT=http://localhost:8080
VIEW_ENDPOINT=http://localhost:3000
3 changes: 3 additions & 0 deletions ffjlabo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
/dist/
.env
33 changes: 33 additions & 0 deletions ffjlabo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 実行方法

```
// パッケージインストール
yarn install

// 環境変数用ファイル作成
cp .env.sample .env

// .envファイル編集
// この時,CLIENT_IDとCLIENT_SECRETSを書き換えてください
vim .env

// webpackサーバ起動
yarn dev

// 認証サーバ起動
node server/index.js
```

その後,`http://localhost:3000`にアクセスすると確認できます.
また,認証サーバは`http://localhost:8080`で起動します.

# 概要

現状 css などデザイン部分や UX の向上に関しては着手できていません.
プレイリスト一覧表示,動画一覧表示,プレイリストの追加,動画の追加に関して必要最低限の実装となっています.

# 技術ポイント

- React + Redux で store でのデータ一括管理
- できるかぎり Presentational Component と Container Component を意識
- 認証データをサーバサイドで取得し,Cookie としてブラウザ側に保持
33 changes: 33 additions & 0 deletions ffjlabo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"scripts": {
"dev": "webpack-dev-server --mode development --hot --inline",
"prod": "webpack-dev-server --mode production --hot --inline",
"build": "webpack --mode production"
},
"dependencies": {
"axios": "0.18.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"js-cookie": "^2.2.1",
"passport": "^0.4.1",
"passport-youtube-v3": "^2.1.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/preset-env": "^7.8.4",
"@babel/preset-es2015": "^7.0.0-beta.53",
"@babel/preset-react": "^7.8.3",
"babel-loader": "^8.0.6",
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
}
}
9 changes: 9 additions & 0 deletions ffjlabo/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="app"></div>
</body>
</html>
53 changes: 53 additions & 0 deletions ffjlabo/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const passport = require('passport');
const YoutubeV3Strategy = require('passport-youtube-v3').Strategy;
require('dotenv').config();
const app = require('express')();

apiEndpoint = process.env['API_ENDPOINT'];
viewEndpoint = process.env['VIEW_ENDPOINT'];

passport.use(
new YoutubeV3Strategy(
{
clientID: process.env['CLIENT_ID'],
clientSecret: process.env['CLIENT_SECRET'],
callbackURL: `${apiEndpoint}/auth/callback`,
scope: ['https://www.googleapis.com/auth/youtube'],
},
(accessToken, refreshToken, params, profile, done) => {
const {expires_in} = params;
return done(null, {accessToken, expires_in});
}
)
);

passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});

app.use(passport.initialize());

app.get('/auth', (req, res, next) => {
passport.authenticate('youtube')(req, res, next);
});

app.get(
'/auth/callback',
passport.authenticate('youtube', {
failureRedirect: `${viewEndpoint}`,
}),
(req, res) => {
const {accessToken, expires_in} = req.user;
res.cookie('access_token', accessToken, {
expires: new Date(Date.now() + expires_in * 1000),
maxAge: expires_in * 1000,
httpOnly: false,
});
res.redirect(viewEndpoint);
}
);

app.listen(8080, () => console.log('listen on 8080'));
5 changes: 5 additions & 0 deletions ffjlabo/src/actions/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const GET_PLAYLISTS = 'GET_PLAYLISTS';
export const ADD_PLAYLIST = 'ADD_PLAYLIST';

export const GET_PLAYLIST_ITEMS = 'GET_PLAYLIST_ITEMS';
export const ADD_PLAYLIST_ITEM = 'ADD_PLAYLIST_ITEM';
17 changes: 17 additions & 0 deletions ffjlabo/src/components/pages/Login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';

const Login = () => {
const login = () => {
const url = `${API_ENDPOINT}/auth`;
window.location.href = url;
};

return (
<div>
<p>YouTube認証が必要です</p>
<button onClick={login}>認証</button>
</div>
);
};

export default Login;
60 changes: 60 additions & 0 deletions ffjlabo/src/components/pages/Playlist/Playlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, {useState, useEffect} from 'react';
import {useSelector} from 'react-redux';
import {Link} from 'react-router-dom';

const AddPlaylistForm = ({addPlaylist}) => {
const [isForm, setIsForm] = useState(false);
const [text, setText] = useState('');

const handleSubmit = e => {
e.preventDefault();
addPlaylist(text);
};

return isForm ? (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="プレイリスト名"
onChange={e => {
setText(e.target.value);
}}
/>
<button type="submit">追加</button>
<button
onClick={e => {
e.preventDefault();
setIsForm(false);
}}
>
キャンセル
</button>
</form>
) : (
<div
onClick={() => {
setIsForm(true);
}}
>
+プレイリストを追加する
</div>
);
};

const Playlist = ({playlists, addPlaylist}) => {
return (
<div>
<AddPlaylistForm {...{addPlaylist}} />
{playlists.map(({id, snippet}) => (
<div key={id}>
<Link to={`/playlist/${id}`}>
<img src={snippet.thumbnails.default.url} />
{snippet.title}
</Link>
</div>
))}
</div>
);
};

export default Playlist;
23 changes: 23 additions & 0 deletions ffjlabo/src/components/pages/Playlist/PlaylistContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, {useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import Playlist from './Playlist';
import {getPlaylists, addPlaylist} from '../../../models';

const PlaylistContainer = () => {
const dispatch = useDispatch();
const playlists = useSelector(state => state.playlistReducer);
useEffect(() => {
dispatch(getPlaylists());
}, [JSON.stringify(playlists)]);

const _props = {
...playlists,
addPlaylist: (title = '') => {
dispatch(addPlaylist(title));
},
};

return <Playlist {..._props} />;
};

export default PlaylistContainer;
3 changes: 3 additions & 0 deletions ffjlabo/src/components/pages/Playlist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PlaylistContainer from './PlaylistContainer';

export default PlaylistContainer;
58 changes: 58 additions & 0 deletions ffjlabo/src/components/pages/PlaylistItems/PlaylistItems.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, {useState, useEffect} from 'react';
import {useSelector} from 'react-redux';

const AddPlaylistItemForm = ({addPlaylistItem}) => {
const [isForm, setIsForm] = useState(false);
const [videoId, setVideoId] = useState('');

const handleSubmit = e => {
e.preventDefault();
addPlaylistItem(videoId);
};

return isForm ? (
<form onSubmit={handleSubmit}>
<label>https://www.youtube.com/watch?v=</label>
<input
type="text"
placeholder=""
onChange={e => {
setVideoId(e.target.value);
}}
/>
<button type="submit">追加</button>
<button
onClick={e => {
e.preventDefault();
setIsForm(false);
}}
>
キャンセル
</button>
</form>
) : (
<div
onClick={() => {
setIsForm(true);
}}
>
+動画を追加する
</div>
);
};

const PlaylistItems = ({playlistItems, addPlaylistItem}) => {
return (
<div>
<AddPlaylistItemForm {...{addPlaylistItem}} />
{playlistItems.map(({id, snippet}) => (
<div key={id}>
<img src={snippet.thumbnails.default.url} />
{snippet.title}
</div>
))}
</div>
);
};

export default PlaylistItems;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, {useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import PlaylistItems from './PlaylistItems';
import {getPlaylistItems, addPlaylistItem} from '../../../models';

const PlaylistItemsContainer = ({match}) => {
const dispatch = useDispatch();
const playlistId = match.params.playlistId;
const playlistItems = useSelector(state => state.playlistItemsReducer);
useEffect(() => {
dispatch(getPlaylistItems(playlistId));
}, [JSON.stringify(playlistItems)]);

const _props = {
...playlistItems,
addPlaylistItem: (videoId = '') => {
dispatch(addPlaylistItem(playlistId, videoId));
},
};
return <PlaylistItems {..._props} />;
};

export default PlaylistItemsContainer;
3 changes: 3 additions & 0 deletions ffjlabo/src/components/pages/PlaylistItems/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PlaylistItemsContainer from './PlaylistItemsContainer';

export default PlaylistItemsContainer;
39 changes: 39 additions & 0 deletions ffjlabo/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, {useState, useEffect} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import {createStore, applyMiddleware} from 'redux';
import Cookies from 'js-cookie';

import Reducers from './reducers';
import Playlist from './components/pages/Playlist';
import PlaylistItems from './components/pages/PlaylistItems';
import Login from './components/pages/Login';

const store = createStore(Reducers, applyMiddleware(thunk));

const App = () => {
const accessToken = Cookies.get('access_token');

if (accessToken === undefined) {
return <Login />;
}

return (
<Router>
<Switch>
<Route exact path="/" component={Playlist} />
<Route exact path="/playlist" component={Playlist} />
<Route path="/playlist/:playlistId" component={PlaylistItems} />
</Switch>
</Router>
);
};

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);
4 changes: 4 additions & 0 deletions ffjlabo/src/models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {getPlaylists, addPlaylist} from './playlist';
import {getPlaylistItems, addPlaylistItem} from './playlistItems';

export {getPlaylists, addPlaylist, getPlaylistItems, addPlaylistItem};
Loading