yucatio@システムエンジニア

趣味で作ったものいろいろ

Firebaseアプリの公開(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

アプリの修正が完了したので、Firebaseにアプリを公開します。

ビルドします。

$ yarn run build

一旦ローカルで正常に動くか確認しましょう。

$ firebase serve --only hosting

http://localhost:5000にアクセスして、動作確認しましょう。 確認が終わったら、Firebaseにデプロイします。

$ firebase deploy --only hosting

=== Deploying to 'todo-sample-xxxxx’…

i  deploying hosting
i  hosting[todo-sample-xxxxx]: beginning deploy...
i  hosting[todo-sample-xxxxx]: found 8 files in build
✔  hosting[todo-sample-xxxxx]: file upload complete
i  hosting[todo-sample-xxxxx]: finalizing version...
✔  hosting[todo-sample-xxxxx]: version finalized
i  hosting[todo-sample-xxxxx]: releasing new version...
✔  hosting[todo-sample-xxxxx]: release complete
✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/todo-sample-xxxxx/overview
Hosting URL: https://todo-sample-xxxxx.firebaseapp.com

無事デプロイできたので、https://todo-sample-xxxxx.firebaseapp.com にアクセスします。(todo-sample-xxxxは自身のプロジェクトIDに置き換えてください。)

f:id:yucatio:20181219103049p:plain
最近の更新

f:id:yucatio:20181219103107p:plain
ログインしたユーザ自身のタスク一覧

f:id:yucatio:20181219103136p:plain
ログインしたユーザ以外のタスク一覧

アプリが公開できました! Material-UIのスタイルが適用されています。

これでSTEP 4は終了です。見た目が整うと誰かに見せたくなりますね。

★目次

yucatio.hatenablog.com

ロゴの作成と表示(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

仕上げにロゴを作成して表示してみましょう。ロゴを、自動でかつ無料で作成してくれる便利なサイトがたくさんあります!

peraichi.com

今回はLOGASTERというサービスを使用します。

www.777logos.com

ロゴ作成の前にサービス名を決めます。今回はタスク管理アプリ(TODOアプリ)ということで、TODODO(トドド)というサービス名にしました。

f:id:yucatio:20181216212619p:plain
サービス名の入力

ビジネスタイプを選択して次へ。

ロゴの候補がたくさん表示されます。

f:id:yucatio:20181216212652p:plain
ロゴデザインの選択画面

今回はチェックマークっぽいロゴにしました。 ロゴの編集でフォントの種類やエンブレムの色を変えることができます。

f:id:yucatio:20181216212754p:plain
ロゴの編集

無料では透かしが入っている小サイズのロゴをダウンロードできます。

f:id:yucatio:20181216212907p:plain
ロゴのダウンロード

ダウンロードしたファイルを解凍すると6種類のロゴファイルが入っています。

f:id:yucatio:20181216213356p:plain

ロゴをwebページに表示してみましょう。

1_Primary_logo_on_transparent_404x63.pngsrc/img/logo/Primary_logo_on_transparent_404x63.png に配置します。ファイルをリネームしています。

src/components/header/index.js

import logo from '../../img/logo/Primary_logo_on_transparent_404x63.png'  // 追加
// import Typography from '@material-ui/core/Typography'  を削除

const Header = ({ classes }) => (
  <AppBar className={classes.appBar}>
    <Toolbar>
      {/* Typography を削除
        <Typography variant="h6" color="inherit" component={Link} to="/">
          タスク管理アプリ
        </Typography>
      */}
      {/* 追加ここから */}
      <Link to='/'>
        <img src={logo} alt="TODODO(トドド)" height="36" width="auto"/>
      </Link>
      {/* 追加ここまで */}
      <div className={classes.grow}></div>
      <MenuIcons />
      <Login />
    </Toolbar>
  </AppBar>
)

ページタイトルを書き換えます。

public/index.html

<html lang="en">
  <head>
    <!— 中略 —>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
    <title>TODODO(トドド) タスク管理アプリ</title>  <!— 変更 —>
  </head>
  <!— 以下略 —>

実行結果です。ロゴが表示されました。

f:id:yucatio:20181216214537p:plain

以上でロゴの作成と表示ができました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのテーマカラーを変更する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

現在はデフォルトのテーマカラーを使用していますが、これを自分の好きな色に変えます。

f:id:yucatio:20181216135813p:plain

今回はマテリアルデザインのカラーツールを使用します。

primaryとsecondaryの色を選択します。選択すると左側の画面にすぐに反映されるので、適用イメージを確かめならが選べます。 選択した色のカラーコードが右下に表示されます。

f:id:yucatio:20181122231844p:plain

カスタマイズしたテーマ用のファイルを作成します。

src/materialui/theme.js(新規作成)

import {createMuiTheme} from '@material-ui/core/styles'
        
export const theme = createMuiTheme({  // #1
  palette: {
    primary: {
      light: '#ffff8b',
      main: '#ffee58',
      dark: '#c9bc1f',
      contrastText: '#000000',
    },
    secondary: {
      light: '#63a4ff',
      main: '#1976d2',
      dark: '#004ba0',
      contrastText: '#ffffff',
    },
  },
})
  • createMuiThemeにテーマカラーを渡すことでデフォルトの設定を上書きしています(#1)。

作成したテーマをコンポーネントに渡します。<App><MuiThemeProvider>で囲うことでカスタマイズしたテーマを使用することができます。

src/index.js

import {MuiThemeProvider} from '@material-ui/core/styles'  // 追加
import {theme} from './materialui/theme'  // 追加

// 略

render (
  <Provider store={store}>
    <MuiThemeProvider theme={theme}>  {/* 追加 */}
      <App />
    </MuiThemeProvider>  {/* 追加 */}
  </Provider>,
  document.getElementById('root')
)

実行結果です。Appbarとボタンの色が変わりました。

f:id:yucatio:20181216135836p:plain

以上でMaterial-UIのテーマカラーを変更できました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

タスクの送信状態の表示(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

タスクの送信状態を表示するように変更します。タスクの各行に送信状態を表すアイコンを表示します。

今回のタスク管理アプリの場合、送信状態を表示することは必須ではありませんが、 チャットアプリのような相手がいて即時性が求められるアプリの場合は、サーバへの送信状況を表示した方がよいでしょう。

f:id:yucatio:20181215222506p:plain

方針

タスクの送信状態を、各タスクの横にアイコンを表示することで示します。

  • サーバにデータを送信中は送信中のマークをタスクの横に表示します。今回は右上向きの矢印を表示します。
  • サーバへのデータ送信が終了後、アイコンを消します。
  • エラーが発生した場合はエクスクラメーションマークのアイコンを表示します。

stateオブジェクトの設計

各タスクの状態を持つtodoStatusesオブジェクトを用意します。todoStatusesの各プロパティはキーをtodoIdにすればよさそうです。以下の"aaabbbcccxxxfff""cccdddeeefff""gggeeefffttt"はtodoIdです。

state = {
  todoStatuses : {
      "aaabbbcccxxxfff" : {
        status : “sending},
     "cccdddeeefff" : {
        status : “success},
     "gggeeefffttt" : {
        status : “error}
  }
}

新規登録時のtodoIdの事前取得

上記のデータ構造を採用する際、 タスクの更新時にはすでにtodoIdが分かっているのでtodoIdをキーとすることは可能です。しかし新規登録では、現在の実装ではtodoIdが分かるのはタスクの登録後です。それでは送信中のステータスを表示することができません。

そこで、新規タスクを登録する前にtodoIdを取得するようにコードを変更します。また、reducerにtodoIdを渡すようにも変更します。

src/actions/todoActions.js

const addTodoRequest = (todoId) => ({  // todoIdを追加
  type: ADD_TODO_REQUEST,
  todoId,  // 追加
})

const addTodoSuccess = (todoId) => ({  // todoIdを追加
  type: ADD_TODO_SUCCESS,
  todoId,  // 追加
})

const addTodoError = (todoId, err) => ({  // todoIdを追加
  type: ADD_TODO_ERROR,
  todoId,  // 追加
  err,
})

export const addTodo = (uid, text) => {
  return (dispatch, getState, {getFirebase}) => {
    if (!uid) {
      dispatch(notAuthenticatedOnTodoAction());
      return;
    }
    // dispatch(addTodoRequest()); は下に移動
    const firebase = getFirebase();
    const id = firebase.push(`todos/${uid}`).key;  // #1
    dispatch(addTodoRequest(id));  // idを引数に追加
    const createdAt = moment().valueOf();
    firebase.set(`todos/${uid}/${id}`,{  // #2
      completed: false,
      text,
      _createdAt : createdAt,
      _updatedAt : createdAt
    })
    .then(() => {
      dispatch(addTodoSuccess(id));  // idを引数に追加
    }).catch(err => {
      dispatch(addTodoError(id, err));  // idを引数に追加
    });
  }
}
  • firebase.push(`todos/${uid}`).keyで新しいデータのkey(id)を取得することができます(#1)。この操作はサーバ通信を行わないため、すぐに結果が返ってきます。
  • 上で取得したidを使ってtodos/${uid}/${id}にデータをセットします(#2)。pushからsetに変更してあることに注意してくだい。

更新時にtodoIdをactionに渡す

更新時、現在の実装ではタスク名と完了グラグを渡していますが、todoIdを渡すように変更します。

src/actions/todoActions.js

const toggleTodoRequest = (todoId) => ({  // text, completedを削除し、todoIdを追加
  type: TOGGLE_TODO_REQUEST,
  // text, を削除
  // completed を削除
  todoId,
})

const toggleTodoSuccess = (todoId) => ({  // text, completedを削除し、todoIdを追加
  type: TOGGLE_TODO_SUCCESS,
  // text, を削除
  // completed を削除
  todoId,
})

const toggleTodoError = (todoId, err) => ({  // text, completedを削除し、todoIdを追加
  type: TOGGLE_TODO_ERROR,
  // text, を削除
  // completed, を削除
  todoId,
  err,
})

export const toggleTodo = (uid, id) => {
  return (dispatch, getState, {getFirebase}) => {
    if (!uid) {
      dispatch(notAuthenticatedOnTodoAction());
      return;
    }
    const firebase = getFirebase();
    const state = getState();
    const todo = state.firebase.data.todos[uid][id];
    dispatch(toggleTodoRequest(id));  // todo.text, !todo.completedを削除し、idを追加
    const updatedAt = moment().valueOf();
    firebase.update(`todos/${uid}/${id}`, {
      completed: ! todo.completed,
      _updatedAt : updatedAt
    })
    .then(() => {
      dispatch(toggleTodoSuccess(id));  // todo.text, !todo.completedを削除し、idを追加
    }).catch(err => {
      dispatch(toggleTodoError(id, err));  // todo.text, !todo.completedを削除し、idを追加
    });
  }
}

reducerの実装

actionを受け取った時に、todoIdごとにステータスを設定します。

src/reducers/todoStatuses.js(新規作成)

import { LOCATION_CHANGE_ON_TODOS, LOGOUT_SUCCESS,
  ADD_TODO_REQUEST, ADD_TODO_SUCCESS, ADD_TODO_ERROR,
  TOGGLE_TODO_REQUEST, TOGGLE_TODO_SUCCESS, TOGGLE_TODO_ERROR,
   }
   from '../actions/'

const INITIAL_STATE = {}

const todoStatuses = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case ADD_TODO_REQUEST:
      return { ...state, [action.todoId]: {status : 'sending'}}  // #1
    case ADD_TODO_SUCCESS:
      return { ...state, [action.todoId]: {status : 'success'}}
    case ADD_TODO_ERROR:
      return { ...state, [action.todoId]: {status : 'error'}}
    case TOGGLE_TODO_REQUEST:
      return { ...state, [action.todoId]: {status : 'sending'}}
    case TOGGLE_TODO_SUCCESS:
      return { ...state, [action.todoId]: {status : 'success'}}
    case TOGGLE_TODO_ERROR :
      return { ...state, [action.todoId]: {status : 'error'}}
    case LOCATION_CHANGE_ON_TODOS:
    case LOGOUT_SUCCESS :
      return INITIAL_STATE
    default:
      return state
  }
}

export default todoStatuses
  • stateに、todoIdをキーとしstatusプロパティを持ったオブジェクトを追加(または上書き)します(#1)。[action.todoId]action.todoIdが表す値がキーになります。

src/reducers/index.js

import todoStatuses from './todoStatuses'  // 追加

export default combineReducers({
  firebase: firebaseReducer,
  auth,
  notice,
  todoStatuses,  // 追加
  visibilityFilter
})

送信ステータスの表示

送信ステータスを表示します。

todoStatusをstateから読み込みます。

src/containers/todos/VisibleTodoList.js

const mapStateToProps = ({todoStatuses, visibilityFilter, firebase: {auth, data : {todos, users}}}, {uid}) => { // todoStatusesの追加
  return {
    todos: getVisibleTodos(todos && todos[uid], visibilityFilter),
    todoStatuses,  // 追加
  }
}

todoStatuses[key]を各todoに渡します。

src/components/todos/TodoList.js

const TodoList = ({todos, isOwnTodos, todoStatuses, onTodoClick, classes}) => {  // todoStatusesを追加
  // 略
  return (
    <List>
      {Object.keys(todos).map(
        (key) => (
          <Todo
            key={key}
            isOwnTodos={isOwnTodos}
            {...todos[key]}
            todoStatus={todoStatuses[key]}  // 追加
            onClick={isOwnTodos ? (() => onTodoClick(key)) : (() => {})} />
        )
      )}
    </List>
  )
}

TodoList.propTypes = {
  // 略
  todoStatuses: PropTypes.object.isRequired,  // 追加
}

送信ステータスによってアイコンを出し分けます。

src/components/todos/Todo.js

import Tooltip from '@material-ui/core/Tooltip'  // 追加
import CallMade from '@material-ui/icons/CallMade'  // 追加
import Error from '@material-ui/icons/Error'  // 追加

// StatusIcon関数の追加
const StatusIcon = (todoStatus) => {
  if (!todoStatus) {
    return null;
  }

  if (todoStatus.status === 'sending') {
    return (
      <Tooltip title="送信中">
        <CallMade />
      </Tooltip>
    )
  }
  if (todoStatus.status === 'error') {
    return (
      <Tooltip title="エラー">
        <Error />
      </Tooltip>
    )
  }
  return null
}

const Todo = ({isOwnTodos, onClick, completed, text, todoStatus}) => (  // todoStatusの追加
  <ListItem
    button={isOwnTodos}
    onClick={onClick}
  >
    {CheckIcon(isOwnTodos, completed)}
    <ListItemText inset>
      <span style={ {textDecoration: completed ? 'line-through' : 'none'}}>{text}</span>
    </ListItemText>
    {StatusIcon(todoStatus)}  {/* 追加 */}
   </ListItem>
)

Todo.propTypes = {
  // 略
  todoStatus: PropTypes.shape({  // 追加
    status: PropTypes.oneOf(['sending', 'success', 'error']).isRequired
  })
}

実行結果

実行結果です。

送信中は右上向きの矢印とマウスオーバー時に送信中のツールチッブが表示されます。

f:id:yucatio:20181215222506p:plain

エラー時はエクスクラメーションマークとマウスオーバー時にエラーのツールチッブが表示されます。 (通常の操作ではエラーが発生しないので、少しソースを変更してエラーを出しています。)

f:id:yucatio:20181215222521p:plain

以上でタスクの送信状態を表示することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのSnackbarを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Material-UIのSnackbarを使用して、メッセージを表示します。Snackbarは、画面の端から出てきて1行程度のお知らせを表示するエリアのことです。 未ログイン時の更新エラー(NOT_AUTHENTICATED_ON_TODO_ACTION)と通信エラー(ADD_TODO_ERRORTOGGLE_TODO_ERROR)の時にSnackbarにエラーメッセージを表示します。データの送信中と送信完了メッセージについては、メッセージの表示をやめます。(次ページでアイコンによる表示を行います。)

f:id:yucatio:20181214224609p:plain

Noticeコンポーネントの移動

Noticeコンポーネントをtodosディレクトリから1つ上のcomponentsディレクトリに移動し、todosに限らないお知らせの表示の役割を持たせます。表示する場所もAppコンポーネントに移動します。

src/components/todos/Notice.jssrc/components/Notice.jsにリネームします。

src/components/Notice.js

const Notice = ({ notice }) => {  // 変更
  // 略
}

Notice.propTypes = {  // 変更
  notice: PropTypes.string
}

export default connect(  // 変更
  mapStateToProps
)(Notice)  // 変更

src/components/todos/index.js

// import Notice from './Notice' を削除

class TodoComponent extends React.Component {
  // 略
  render() {
    const {isOwnTodos, match: { params: {uid}}, classes} = this.props;
    return (
      <div className={classes.root}>
        <div className={classes.todoListRoot}>
          <Paper className={classes.todoListContent}>
            <Title isOwnTodos={isOwnTodos} uid={uid} />
            {isOwnTodos && <AddTodo uid={uid} />}
            {/* <Notice /> を削除 */}
            <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
          </Paper>
        </div>
        <FilterNav />
      </div>
    )
  }
}

src/components/App.js

import Notice from './Notice'  // 追加

const App = ({classes}) => (
  <BrowserRouter>
    <div>
      <CssBaseline />
      <Header />
      <div className={classes.toolbar} />
      <Switch>
        <Route exact path="/" component={Dashboard} />
        <Route exact path="/users/:uid/todos" component={TodoComponent} />
        <Route component={NoMatch} />
      </Switch>
      <Notice />  {/* 追加 */}
    </div>
  </BrowserRouter>
)

Actionの作成

Snackbarが閉じられる時に呼ばれるactionを定義します。

src/actions/index.js

// notice
export const CLOSE_NOTICE = 'CLOSE_NOTICE'  // 追加

src/actions/noticeActions.js(新規作成)

import {CLOSE_NOTICE} from './'

export const closeNotice = () => ({
  type: CLOSE_NOTICE
})

Reducerの作成

新規にnotice reducerを作成します。todo reducerは削除します。

src/reducers/notice.js(新規作成)

import { NOT_AUTHENTICATED_ON_TODO_ACTION, ADD_TODO_ERROR, TOGGLE_TODO_ERROR,
  LOCATION_CHANGE_ON_TODOS, LOGOUT_SUCCESS, CLOSE_NOTICE}
   from '../actions/'

const INITIAL_STATE = { text: '', level: 'info', open: false }  // #1

const notice = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case NOT_AUTHENTICATED_ON_TODO_ACTION :
      return { text: 'タスクを追加・変更するにはログインしてください',
          level: 'warning', open: true }  // #2
    case ADD_TODO_ERROR :
    case TOGGLE_TODO_ERROR :
      return { text: 'エラーが発生しました。時間をおいて再度お試しください。',
        level: 'error', open: true }  // #3
    case CLOSE_NOTICE :
      return { ...state, open: false }  // #4
    case LOCATION_CHANGE_ON_TODOS :
    case LOGOUT_SUCCESS :
        return { ...state, open: false }  // #5
    default:
      return state
  }
}

export default notice
  • noticeのstateはtextとlevel、openの3つのプロパティを持ちます。textはSnackbarに表示する文字列、levelは{‘success', 'warning', 'error', 'info’}のいずれかで、Snackbarに表示されるアイコンと背景色をコントロールします。openはSnackbarが表示されているかどうかを示す真偽値です。初期値はそれぞれ空文字、 'info’、falseです(#1)。
  • 未ログイン時にタスクの追加・更新を行った場合はwarningレベルのSnackbarにします。openをtrueにしてSnackbarを表示します(#2)。
  • タスクの追加・更新時のエラーの時はerrorレベルにしてSnackbarを表示します(#3)。
  • CLOSE_NOTICE actionの時にopenをfalseにしてSnackbarを閉じます(#4)。
  • ページ遷移の際やログアウトの際にはSnackbarを閉じます(#5)。

src/reducers/todos.jsを削除します。

src/reducers/index.js

// import todos from './todos'  を削除
import notice from './notice' // 追加

export default combineReducers({
  firebase: firebaseReducer,
  auth,
  // todos, を削除
  notice,  // 追加
  visibilityFilter
})

レベルごとに表示を出し分ける

レベル('success', 'warning', 'error', 'info’の4つ)ごとにSnackbarの色を変えます。 公式ページのCustomized Snackbars を参考に実装を進めます。

先にSnackbarContentのラッパコンポーネントを作成します。

src/components/util/snackbar/LevelSnackbarContentWrapper.js(新規作成)

import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { withStyles } from '@material-ui/core/styles'
import IconButton from '@material-ui/core/IconButton'
import SnackbarContent from '@material-ui/core/SnackbarContent'
import CheckCircleIcon from '@material-ui/icons/CheckCircle'
import ErrorIcon from '@material-ui/icons/Error'
import InfoIcon from '@material-ui/icons/Info'
import CloseIcon from '@material-ui/icons/Close'
import WarningIcon from '@material-ui/icons/Warning'
import green from '@material-ui/core/colors/green'
import amber from '@material-ui/core/colors/amber'

const variantIcon = {
  success: CheckCircleIcon,
  warning: WarningIcon,
  error: ErrorIcon,
  info: InfoIcon,
}

const styles = theme => ({
  success: {
    backgroundColor: green[600],
  },
  error: {
    backgroundColor: theme.palette.error.dark,
  },
  info: {
    backgroundColor: theme.palette.primary.dark,
  },
  warning: {
    backgroundColor: amber[700],
  },
  icon: {
    fontSize: 20,
  },
  iconVariant: {
    opacity: 0.9,
    marginRight: theme.spacing.unit,
  },
  message: {
    display: 'flex',
    alignItems: 'center',
  },
})

const LevelSnackbarContent = ({ classes, className, message, onClose, variant, ...other }) => {
  const Icon = variantIcon[variant]
  return (
    <SnackbarContent
      className={classNames(classes[variant], className)}
      aria-describedby="client-snackbar"
      message={
        <span id="client-snackbar" className={classes.message}>
          <Icon className={classNames(classes.icon, classes.iconVariant)} />
          {message}
        </span>
      }
      action={[
        <IconButton
          key="close"
          aria-label="Close"
          color="inherit"
          className={classes.close}
          onClick={onClose}
        >
          <CloseIcon className={classes.icon} />
        </IconButton>,
      ]}
      {...other}
    />
  )
}

LevelSnackbarContent.propTypes = {
  classes: PropTypes.object.isRequired,
  className: PropTypes.string,
  message: PropTypes.node,
  onClose: PropTypes.func,
  variant: PropTypes.oneOf(['success', 'warning', 'error', 'info']).isRequired
}

export default withStyles(styles)(LevelSnackbarContent)

最後に、お知らせ(Notice)の表示部分を実装します。

src/components/Notice.js

import React from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Snackbar from '@material-ui/core/Snackbar';
import { closeNotice } from '../actions/noticeActions'
import LevelSnackbarContentWrapper from './util/snackbar/LevelSnackbarContentWrapper'

const Notice = ({ text, level, open, handleClose }) => (
  <Snackbar  // #1
    anchorOrigin={{
      vertical: 'bottom',
      horizontal: 'center',
    }}
    open={open}
    autoHideDuration={30000}
    onClose={handleClose}
  >
    <LevelSnackbarContentWrapper  // #2
        onClose={handleClose}
        variant={level}
        message={text}
      />
  </Snackbar>
)
        
Notice.propTypes = {
  text: PropTypes.string.isRequired,
  level: PropTypes.oneOf(['success', 'warning', 'error', 'info']).isRequired,
  open: PropTypes.bool.isRequired,
  handleClose: PropTypes.func.isRequired
}

const mapStateToProps = state => {
  return {
    text: state.notice.text,
    level: state.notice.level,
    open: state.notice.open,
  }
}

const mapDispatchToProps = (dispatch, {uid}) => ({
  handleClose: () => {
    dispatch(closeNotice())
  }
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notice)
  • Snackbarの設定をします(#1)。anchorOriginに{vertical: 'bottom', horizontal: 'center’}を指定して、中央下にSnackbarを表示します。表示状態(open)はstateか取得します。autoHideDurationはSnackbarの表示時間(ミリ秒)です。今回は30000を指定して30秒経過したら自動的にSnackbarが隠れるようにしました。onCloseではcloseNotice actionがdispatchされるようにしています。
  • LevelSnackbarContentWrapperの設定をします(#2)。variantにlevelを設定します。messageにtextを渡します。

実行結果

実行結果です。通常の操作ではエラーが発生しないので、少しソースを変更してエラーを出しています。

f:id:yucatio:20181214224609p:plain

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのDrawerを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Material-UIのDrawerを使用して、タスクのフィルタを行うボタンを右側に配置します。

ファイル名とコンポーネント名の変更

タスクのフィルタ用のボタンがフッターではなくなるので、Footer.jsのファイル名を変更します。また、Linkコンポーネントもより内容を表す名前に変更します。

  • src/components/todos/Footer.jssrc/components/todos/FilterNav.jsにリネームします。
  • src/components/todos/Link.jssrc/components/todos/FilterButton.jsにリネームします。
  • src/containers/todos/FilterLink.jssrc/containers/todos/FilterItem.jsにリネームします。

コンポーネント名を書き換えます。

src/components/todos/FilterNav.js({/* xxx */}のコメントは実行前に削除してください)

// import FilterLink from '../../containers/todos/FilterLink'  を削除
import FilterItem from '../../containers/todos/FilterItem'  // 追加

const FilterNav = () => (  {/* 変更 */}
  <p>
    Show:
    <FilterItem filter={VisibilityFilters.SHOW_ALL}>  {/* 変更 */}
      All
    </FilterItem>  {/* 変更 */}
    <FilterItem filter={VisibilityFilters.SHOW_ACTIVE}>  {/* 変更 */}
      Active
    </FilterItem>  {/* 変更 */}
    <FilterItem filter={VisibilityFilters.SHOW_COMPLETED}>  {/* 変更 */}
      Completed
    </FilterItem>  {/* 変更 */}
  </p>
)

export default FilterNav  // 変更

src/components/todos/FilterButton.js({/* xxx */}のコメントは実行前に削除してください)

const FilterButton = ({ active, children, onClick }) => (  {/* 変更 */}
  // 略
)

FilterButton.propTypes = {  // 変更
  // 略
}

export default FilterButton  // 変更

src/containers/todos/FilterItem.js

// import Link from '../../components/todos/Link'  を削除
import FilterButton from '../../components/todos/FilterButton'  // 追加

const FilterItem = connect( // 変更
  mapStateToProps,
  mapDispatchToProps
)(FilterButton) // 変更

export default FilterItem // 変更

src/components/todos/index.js

//import Footer from './Footer' を削除
import FilterNav from './FilterNav'

class TodoComponent extends React.Component {
  // 略
  render() {
    const {isOwnTodos, match: { params: {uid}}, classes} = this.props;
    return (
      <div className={classes.todoListRoot}>
        <Paper className={classes.todoListContent}>
          {/* 略 */}
          <FilterNav />  {/* FooterをFilterNavに変更 */}
        </Paper>
      </div>
    )
  }
}

変更後、エラーが出ていないことを確認してください。

Drawerの使用

画面右側にタスクのフィルタを行うボタンを配置します。

きちんと書くとすれば、画面の横幅に合わせて、Drawerを常時表示するかアクションがあった時のみ表示するかを切り替えるべきですが、今回は簡単のため、常時右側に表示します。 デフォルトではDrawerはAppbarよりも上に表示されますが、今回はAppbarよりも下に表示します。

Appbarよりも下に表示されるDrawerの導入

src/components/todos/index.js({/* xxx */}のコメントは実行前に削除してください)

const styles = theme => ({
  root: {  // 追加
    display: 'flex',  // #1
  },
  todoListRoot: {
    flexGrow: 1,  // #2
    padding: theme.spacing.unit * 3,
  },
  // 略
})

class TodoComponent extends React.Component {
  // 略
  render() {
    const {isOwnTodos, match: { params: {uid}}, classes} = this.props;
    return (
      <div className={classes.root}>  {/* 追加 */}
        <div className={classes.todoListRoot}>
          <Paper className={classes.todoListContent}>
            <Title isOwnTodos={isOwnTodos} uid={uid} />
            {isOwnTodos && <AddTodo uid={uid} />}
            <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
            {/* <FilterNav /> は下に移動 */}
          </Paper>
        </div>
        <FilterNav />  {/* ここに移動 */}
      </div>  {/* 追加 */}
    )
  }
}
  • display: 'flex’を使用して、タスク一覧とタスクフィルタを横に並べます(#1)。
  • flexGrow: 1を指定して、タスク一覧の幅をを可能な限り多く取ります(#2)。

src/components/todos/FilterNav.js({/* xxx */}のコメントは実行前に削除してください)

import PropTypes from 'prop-types'  // 追加
import { withStyles } from '@material-ui/core/styles'  // 追加
import Drawer from '@material-ui/core/Drawer'  // 追加

const drawerWidth = 240  // 追加

// stylesを追加
const styles = theme => ({
  drawer: {
    width: drawerWidth,
    flexShrink: 0,  // #1
  },
  drawerPaper: {
    width: drawerWidth,
  },
  toolbar: theme.mixins.toolbar,
})

const FilterNav = ({ classes }) => (  // classesを追加
  <Drawer variant="permanent" anchor="right"
                 className={classes.drawer} classes={{ paper: classes.drawerPaper, }}>  {/* #2 */}
    <div className={classes.toolbar} />  {/* #3 */}
    Show:
    <FilterItem filter={VisibilityFilters.SHOW_ALL}>
      All
    </FilterItem>
    {/* 略 */}
  </Drawer>  {/* 変更 */}
)

// propTypesを追加
FilterNav.propTypes = {
  classes: PropTypes.object.isRequired,
}

export default withStyles(styles)(FilterNav)  // 変更
  • flexShrink: 0を指定して、Drawerが縮まないようにしています(#1)。
  • Drawerでフィルタメニューを囲みます(#2)。variant="permanent"を指定することで常時表示し、anchor="right"で画面右側に表示します。
  • divを追加します(#3)。classes.toolbarを指定して、toolbarぶんの余白を設けます。

src/components/header/index.js

const styles = theme => ({
  appBar: {  // 追加
    zIndex: theme.zIndex.drawer + 1,  // #1
  },
  // 略
})

const Header = ({ classes }) => (
  <AppBar className={classes.appBar}>  {/* classNameを追加 */}
    {/* 略 */}
  </AppBar>
)
  • zIndexをdrawerより大きくすることで、AppbarをDrawarより前に表示します(#1)。

ここまでの実行結果です。画面右側にフィルタメニューが表示されました。

f:id:yucatio:20181213145240p:plain

フィルタメニューをListに変更する

reactのbuttonを使用している部分をMaterial-UIのListとListItemに変更します。

src/components/todos/FilterNav.js

import List from '@material-ui/core/List'  // 追加
import ListSubheader from '@material-ui/core/ListSubheader'  // 追加

const FilterNav = ({ classes }) => (
  <Drawer variant="permanent" anchor="right"
                 className={classes.drawer} classes={{ paper: classes.drawerPaper, }}>
    <div className={classes.toolbar} />
    <List subheader={<ListSubheader component="div">表示</ListSubheader>}>  {/* #1 */}
      <FilterLink filter={VisibilityFilters.SHOW_ALL}>
        全て  {/* 日本語に変更 */}
      </FilterLink>
      <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
        未完了  {/* 日本語に変更 */}
      </FilterLink>
      <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
        完了  {/* 日本語に変更 */}
      </FilterLink>
    </List>  {/* 追加 */}
  </Drawer>
)
  • Listコンポーネントでフィルタメニューを囲みます(#1)。subheaderでリストのヘッダを設定します。

src/components/todos/FilterButton.js({/* xxx */}のコメントは実行前に削除してください)

import ListItem from '@material-ui/core/ListItem'  // 追加
import ListItemIcon from '@material-ui/core/ListItemIcon'  // 追加
import ListItemText from '@material-ui/core/ListItemText'  // 追加
import Done from '@material-ui/icons/Done'  // 追加

const FilterButton = ({ active, children, onClick }) => (
  <ListItem button  // #1
    onClick={onClick}
    disabled={active}
    >
    {active &&
      <ListItemIcon>   {/* #2 */}
        <Done />
      </ListItemIcon>}
    <ListItemText inset primary={children} />   {/* #3 */}
  </ListItem>
)
  • buttonをListItemに変更し、buttonのインターフェースを設定します(#1)。styleは削除します。
  • activeがtrueの時は、Doneアイコン(チェックマークのアイコン)を表示します(#2)。
  • テキストはListItemText要素を使用します(#3)。insetでテキストの位置をそろえます。

実行結果です。 現在使用されているフィルタはdisabledかつ左側にチェックマークがつきました。

f:id:yucatio:20181213144722p:plain

以上でMaterial-UIのDrawerを使用してフィルタメニューを変更することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのAvatarを使用する(ログイン・タスク一覧編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

前回に引き続き、ログイン名の横とタスク一覧に表示されるユーザ名の横にAvatarを追加します。

ログイン名の横にAvatarを表示する

ログイン時、AppbarにAvatarを表示します。今後の実装のために、ユーザ名とavatarUrlをstate.firebase.profileから取得するように変更します。

src/components/header/Login.js({/* xxx */}のコメントは実行前に削除してください)

import Avatar from '@material-ui/core/Avatar'  // 追加

const styles = theme => ({  // themeを引数とする関数に変更
  // 略
  avatar: {  // 追加
    margin: theme.spacing.unit,
  },
})

class Login extends React.Component {
  // 略
  render() {
    const { auth, profile, loginWithGoogle, logout, classes } = this.props  // profileを追加
    // 略
    return (
        <React.Fragment>
          <Button color="inherit" aria-owns={anchorEl ? 'user-menu' : undefined} aria-haspopup="true"
          onClick={this.handleClick} className={classes.userName}>
            {profile.displayName} さん  {/* 変更 */}
          </Button>
          {/* 略 */}
          {profile.avatarUrl && <Avatar alt={profile.displayName} src={profile.avatarUrl} className={classes.avatar} />}  {/* 追加 */}
        </React.Fragment>
    )
  }
}

Login.propTypes = {
  // 略
  profile: PropTypes.shape({  // 追加
    displayName: PropTypes.string,
    avatarUrl: PropTypes.string
  }).isRequired,
}

const mapStateToProps = state => ({
  auth: state.firebase.auth,
  profile: state.firebase.profile,  // 追加
})

実行結果です。Avatarが画面右上の、ユーザ名の右隣に表示されました。

f:id:yucatio:20181211221022p:plain

タスク一覧にAvatarを表示する

続いてタスク一覧画面のユーザ名の横にAvatarを表示します。

src/components/todos/Title.js({/* xxx */}のコメントは実行前に削除してください)

import { withStyles } from '@material-ui/core/styles'  // 追加
import Avatar from '@material-ui/core/Avatar'  // 追加

// stylesを追加
const styles = theme => ({
  row: {
    display: 'flex',  // #1
    alignItems: 'center',  // #2
  },
  avatar: {
    margin: theme.spacing.unit,
  },
})

const Title = ({displayName, avatarUrl, isOwnTodos, classes}) => {  // avatarUrlとclassesを追加
  // 略
  return (
    <div className={classes.row}>  {/* divに変更し、classNameを追加 */}
      {avatarUrl && <Avatar alt={displayName} src={avatarUrl} className={classes.avatar} />}  {/*  追加 */}
      {displayName && <Typography variant="h5">{name}のタスク一覧</Typography>}
    </div>
  )
}

Title.propTypes = {
  // 略
  avatarUrl: PropTypes.string,  // 追加
  classes: PropTypes.object.isRequired,  // 追加
}

const firebaseQueries = ({uid}) => (
  [
    {path: `users/${uid}/displayName`, type: 'once'},
    {path: `users/${uid}/avatarUrl`, type: 'once'},  // 追加
  ]
)

const mapStateToProps = ({firebase: {data : {users}}}, {uid}) => ({
  displayName: users && users[uid] && users[uid].displayName,
  avatarUrl: users && users[uid] && users[uid].avatarUrl,  // 追加
})

export default compose(
  withStyles(styles),  // 追加
  firebaseConnect(firebaseQueries),
  connect(
    mapStateToProps
))(Title)
  • display: 'flex’を指定することで、Avaterとユーザ名を横に並べます(#1)。
  • alignItems: 'center’を指定することで、Avaterとユーザ名のを上下中央に揃えます(#2)。

実行結果です。Avatarがタスク一覧のユーザ名の左隣に表示されました。

f:id:yucatio:20181211221038p:plain

以上で、ログイン名の横とタスク一覧に表示されるユーザ名の横にAvatarを追加できました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com