- 投稿日:2020-12-12T22:57:26+09:00
イラストが簡単に変えられるカレンダー作った
お疲れ様です。
この前某カレンダーアプリを見ていたら、簡単にスタンプが押せる機能があったんですね。
これ結構使いやすくて、概要とか入力しなくて、とりあえずイラストだけ変わればいいってことあるじゃないですか。
(イメージとしては、バイトとかのシフトが近いですかね・・・)とりあえず使ってるアプリに不満はなかったんですが、その予定を共有したくなった時に不便が生じました。
結局アプリでローカルに持ってるだけなので、それを共有するってのがなかなか難しかったのです。。。というわけで、今ないものは作ってしまえというのが今回のテーマです。
初めに
レギュレーション
分解して考えてみると、以下の2点がレギュレーションになるかなと
- カレンダーでボタンを押すだけでイラストが変えられること。
- 誰でも状態を確認できること。(→必然的にWebアプリ??)
使った技術
さくっと作りたかったので、いったんReactを使ってやってみることにしました。
- React:フロントエンドフレームワーク。ベースです。
- React-Calendar:今回の肝。カレンダーのライブラリーはこれ
- Material-ui:別になくてもいいんですけど、Webアプリケーションとしての体裁を・・・
作ったもの
コードとデモ
とりあえず以下Gitで管理してます。
最初に断っときますが、動けば十分で作ってますので品質はご容赦ください・・・
https://github.com/aion0721/sharecale
デモは以下です。
https://aion0721.github.io/sharecale/
ポイント
苦労した点は以下です。
カレンダー表記
基本的には、tileContentにて、各日付の値を設定できます。dateの変数の中に日付情報が入っていますので、
これをよしなに変数に渡して何とか値を取得するようにします。<Calendar tileContent={({date}) =><p><img height="100" width="100" src={`${process.env.PUBLIC_URL}/` + showTypeValue(date) + `.png`}/>{showContent(date)}</p>} />ただし、ソースを読んでいただくとわかるように、毎回undefinedチェックをしているので、
これはこれで相当非効率だなと思います・・・いい方法ないですかねぇ・・・データの持ち方
ReactはFunctionで行くようになったので、HooksであるuseStateを使ってます。
ポイントは、配列を利用していることです。
react-calendarで、日付が一致するときに判定を行いたいため、例えば、2020/12/11の定義を行いたい場合に、
20201211の値として、typeValueを持つことによって、今どのアイコンを表示するか、を保持するようにしています。const [ item, setItem ] = useState({20201211:{holiday:true,txt:'aiueo',typeValue:1}});アイコン変更方法
typeValueがアイコンナンバーを示しているので、ボタンが押されたときに数字を変更しています。
Switchでやってますが、個人的には最高に頭悪いのでなんかいい方法ありますかね・・・switch(item[getFormatDate(e)].typeValue){ case 1: { tmpTypeValue = 2; break } case 2:{ tmpTypeValue = 3 break; } case 3:{ tmpTypeValue = 1 break; } default: { tmpTypeValue = 0 break; } }終わりに
reactを本当にさわりだけやってない人間がやると本当に非効率な書き方しかできてなくて嫌になりますね。。。
今後の展望
バックエンドAPI
とりあえず当初の目的である”共有”ができていないので、ServerlessFrameworkとかでバックエンドのAPI作って、
そこと同期できるようにしたいなとは思います。認証
今はURL知ってればだれでも閲覧できてしまうため、認証を入れたいです。
逆に
今回のレギュレーションを達成しているアプリとかもう既にあったりしたら是非教えてください。もれなく私が喜びます。
- 投稿日:2020-12-12T21:55:01+09:00
Material-UIの便利なコンポーネント
目次
1. はじめに
2. Material-UIの実装方法
3. Grid編 - コンポーネントの画面配置を決めることができる
4. Table編 - 表を扱える
5. Modal編 - クリックすると、モーダルウィンドウが開く
6. 終わりに1. はじめに
Material-UIとはGoogleが開発したReact, Typescriptで使えるUIライブラリです。それぞれのコンポーネントは、公式が設計したprops(組み込み要素)を組み合わせることで、見栄えの良いUIを作ることができます。しかし公式のライブラリの説明は、日本語対応していなかったり、そもそも説明不足でわかりにくい部分があります。最近私はMaterial-UIを使って開発をする機会が多かったので、今回はその中で私が便利だと感じたコンポーネントとpropsについてまとめて紹介していきたいと思います。
2. Material-UIの実装方法
1. Reactの開発環境を構築する。
ここでは省略しますが、以下の記事からmac, windowsでreactの環境構築ができます。
Macの場合
Windowsの場合2. Material-uiのパッケージをtermialでインストールする。
termial// with npm npm install @material-ui/core npm install @material-ui/icons // with yarn yarn add @material-ui/core yarn add @material-ui/icons3. reactのファイルで自分の使いたいコンポーネントをimportして使う
App.jsximport React from 'react'; import ReactDOM from 'react-dom'; import Button from '@material-ui/core/Button'; function App() { return ( <Button variant="contained" color="primary"> Hello World </Button> ); } ReactDOM.render(<App />, document.querySelector('#app'));3. Grid編
Gridはコンポーネントの画面配置を決めることができます。例えば、2つの要素を画面の中央に揃えたり、右端に寄せたり、等間隔で揃えることなど、、、propsを組み合わせるだけで簡単に画面配置できるため非常に強力なコンポーネントです。
以下はGrid内で使えるpropsについてまとまめました。1. container, item
containerとitemは親子関係で親がcontainerとなるように使います。
container内の要素は一列で揃えることが可能で、これがGridの基本となるためGridコンポーネントを使う場合は必須のpropsです。使用例<Grid container>//container内の要素は一列で表示 <Grid item> {/* 中身 */} </Grid> <Grid item> {/* 中身 */} </Grid> </Grid>2. justify
justifyは、containerが含まれている Gridタグ内につけます。それぞれのitemの配置を多岐にわたって調整することが可能です。下に種類 - 用途の一覧を示しています。
- 'flex-start' - 全てのアイテムを左端に揃える
- 'center' - 全てのアイテムを中心に揃える
- 'flex-end' - 全てのアイテムを右端に揃える。
- 'space-between' - itemを等間隔に配置する。
- 'space-around' - itemを等間隔に配置する。space-betweenと違い、両端に空白が入る。
- 'space-evenly' - itemを等間隔に配置する。space-aroundより、両端の空白の割合が大きい。
使用例<Grid container justify="flex-end"> <Grid item> {/* 中身 */} </Grid> <Grid item> {/* 中身 */} </Grid> </Grid> //この場合、二つのアイテムは左揃えになる。3. spacing
containerが含まれているGridタグ内につけることで、itemの間隔を調整できます。広さは{1}~{12}で決めることができるので、画面を見ながら試してみてちょうどいい間隔となるように数を調整します。
使用例<Grid container spacing={4}> <Grid item> {/* 中身 */} </Grid> <Grid item> {/* 中身 */} </Grid> </Grid> //この場合、二つのアイテムの間隔が4の幅になります。Grid応用編
今までのpropsを組み合わせることで、以下のような構成にすることも可能です。
コード<Grid container justify="space-between"> <Grid item> <Grid container spacing={2}> <Grid item> <Button variant="contained" color="secondary" startIcon={<SaveIcon />} >保存する </Button> </Grid> <Grid item> <Button variant="contained" color="secondary" startIcon={<DeleteIcon />} >削除する </Button> </Grid> </Grid> </Grid> <Grid item> <Button variant="contained" color="secondary" startIcon={<SettingsIcon />} >設定する </Button> </Grid> </Grid>結果
三つのボタンの配置を一列でいい感じに整えることができています。
4. Table編
Tableコンポーネントは、表を作ることができます。見た目だけでなく、表の並びを変えたりページ切り替えを可能にしたりするライブラリもあり、非常に便利です。Tableの基本構成は下図のようになっています。手書きで汚くてすみません。htmlの知識があればすぐに使えると思います。
サンプルコードと補足の説明を入れています。サンプルコード<TableContainer component={Paper} style={{ marginBottom: 30 }}> {/* componentにライブラリのPaperをつけることで立体感がでてよくなります */} <Table> <TableHead> <TableRow style={{ backgroundColor: "#F2F2F2" }}> <TableCell>ID</TableCell> <TableCell >氏名</TableCell> <TableCell >ステート</TableCell> </TableRow> </TableHead> <TableBody> {rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row) => ( //ページ切り替えの要素を取得 <TableRow hover key={row.id}> {/* hoverを入れることでマウスポイントが表の上に乗った時に色が変わるアクションがつきます */} <TableCell component="th" scope="row">{row.id}</TableCell> <TableCell >{row.name}</TableCell> <TableCell >{row.state}</TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TablePagination labelRowsPerPage="表示件数:" rowsPerPageOptions={[ { label: '10件', value: 10 }, { label: '50件', value: 50 }, { label: '100件', value: 100 }, { label: '全て', value: rows.length } ]} count={rows.length} rowsPerPage={rowsPerPage} page={page} SelectProps={{ native: true }} onChangePage={handleChangePage} onChangeRowsPerPage={handleChangeRowsPerPage} ActionsComponent={TablePaginationActions} /> </TableRow> </TableFooter> </Table> </TableContainer>結果
5. Modal編
Modalは、ボタンやテキストなどの指定したコンポーネントをクリックするなどの条件を満たした時に表示することができます。用途としては、inputコンポーネントを埋め込んでデータを入力できるようにしたり、注意を表示させたい時に使います。
今回は、下のような新規作成ボタンを押すと、
タイトルやコメントを入力できる画面が表示されようなものを作成する手順について順を追って説明します。
1.ボタン(Modalが開くきっかけ)と、クリックするとtrueになる関数を作成
reactimport React, {useState} from 'react'; import AddCircleIcon from '@material-ui/icons/AddCircle'; function NoticeScreen() { const [open, setOpen] = useState(false); return( <Button variant="contained" color="secondary" startIcon={<AddCircleIcon />} onClick={() => setOpen(true)} //クリックするとtrueになる style={{ marginTop: 30 }} > 新規作成 </Button> ) } export default NoticeScreen2. Modalと、Modalを閉じる関数の作成
import React, {useState} from 'react'; import AddCircleIcon from '@material-ui/icons/AddCircle'; function NoticeScreen() { const [open, setOpen] = useState(false); return( <Button variant="contained" color="secondary" startIcon={<AddCircleIcon />} onClick={() => setOpen(true)} style={{ marginTop: 30 }} > 新規作成 </Button> {/* Modal */} <Modal open={open} onClose={() => setOpen(false)}> </Modal> ) } export default NoticeScreen3. Modalの中身を作成する
import { Button, Grid, Typography, Modal, Paper, TextField } from '@material-ui/core'; . //省略 . <Modal open={open} onClose={() => setOpen(false)}> //open={open}は、openがtrueの場合にmodalが開くという意味 <Fade in={open}> <Paper style={{ width: modalWidth, position: 'absolute', top: height * 0.1, right: (width - modalWidth - 40) / 2, padding: 20 }}> <Typography style={{ marginLeft: 8 }}>タイトル</Typography> <TextField style={{ margin: 8, paddingBottom: 20 }} margin="normal" fullWidth variant="outlined" className="red-border" /> <Typography style={{ marginLeft: 8 }}>コメント</Typography> <TextField style={{ margin: 8, paddingBottom: 20 }} fullWidth margin="normal" multiline rows={4} InputLabelProps={{ shrink: true }} variant="outlined" /> <Grid container style={{ paddingTop: 30 }} justify="flex-end" direction="row"> <Grid item> <Button variant="contained" color="secondary" onClick={addNewCommentRequest} startIcon={<AddCircleIcon />} > 投稿 </Button> </Grid> </Grid> </Paper> </Fade> </Modal>完全版
import React, {useState} from 'react'; import { Button, Grid, Typography, Modal, Paper, TextField } from '@material-ui/core'; import AddCircleIcon from '@material-ui/icons/AddCircle'; function NoticeScreen() { const [open, setOpen] = useState(false); return( <Button variant="contained" color="secondary" startIcon={<AddCircleIcon />} onClick={() => setOpen(true)} style={{ marginTop: 30 }} > 新規作成 </Button> {/* Modal */} <Modal open={open} onClose={() => setOpen(false)}> <Fade in={open}> //open={open}は、openがtrueの場合にmodalが開くという意味 <Paper style={{ width: modalWidth, position: 'absolute', top: height * 0.1,right: (width - modalWidth - 40) / 2, padding: 20 }}> <Typography style={{ marginLeft: 8 }}>タイトル</Typography> <TextField style={{ margin: 8, paddingBottom: 20 }} margin="normal" fullWidth variant="outlined" className="red-border" /> <Typography style={{ marginLeft: 8 }}>コメント</Typography> <TextField style={{ margin: 8, paddingBottom: 20 }} fullWidth margin="normal" multiline rows={4} InputLabelProps={{ shrink: true }} variant="outlined" /> <Grid container style={{ paddingTop: 30 }} justify="flex-end" direction="row"> <Grid item> <Button variant="contained" color="secondary" onClick={addNewCommentRequest} startIcon={<AddCircleIcon />} > 投稿 </Button> </Grid> </Grid> </Paper> </Fade> </Modal> ) } export default NoticeScreen6. 終わりに
Material-UIには、これら以外にも便利なコンポーネントはたくさんありますが、今回は私の独断と偏見で便利なMaterial-UIを選び、その使い方について書かせていただきました。今後もMaterial-UIを使っていく中で記事にしたいと感じたら、新たに付け加えていこうと考えています。
不明点や間違っている点があれば、コメントでお知らせください。参考にさせていただいたサイト
- 投稿日:2020-12-12T21:18:58+09:00
zForce AIRとReactJSでプロトタイピングしたことの学び
この記事はGoodpatch Advent Calendar 2020の13日目となります。
グッドパッチでエンジニアリングマネージャーをしております、nktnfです。ここ最近、NeonodeのzForceを使ってプロトタイピングしており
現在進行ではありますが、そちらについて触れつつ、プロトタイピングについて
少し感じたことを記載しようと思います。話したいこと
zForce AIRとReactJSでプロトタイプというのは簡単に作れる。
が、もしタッチレスなものを作ろうとしたときは、必要とする機能があるならばLeap motionや画像認識を使い
ユーザテストをしながら作っていくのがいいということを伝えたい。前提
パッとプロトタイピングしたかったので、zForce AIRがPlug and Playに対応しているWindows10でシステム構築します。
https://support.neonode.com/docs/display/AIRTSUsersGuide/Getting+Started+with+Sensor+Evaluation+-+Plug+and+Play+with+USBzForce AIR
zForce AIRは現在digikeyなどで購入できる、タッチセンサーです。
主にDigi-Keyで販売されており、約8000円くらいで買うことができます。
IRレーザーベースのマルチタッチセンサで、あらゆるディスプレイ、任意の表面もしくは、赤外光の送受信であれば空中での使用が可能です。
そのため、タッチレスについてプロトタイピングが可能となります。
* https://products.neonode.com/us/touch-sensor-modules/#applications よりReactJS
特段ReactJSじゃなければならない、ということはないですが
さっとプロトタイプするならば、慣れ親しんだもので実装を進めていくのが良いと思います。
(今回は、タッチイベントで取れるだろうということでReactJSを選択)SyntheticEvent
基本的には、zForce AIRをWindowsにつなげた場合、タッチセンサーになるため
Touch eventsのonTouchCancel onTouchEnd onTouchMove onTouchStartの中に入ってくる形で
要素を操作することができます。タッチの場合
<button onTouchEnd={() => console.log("touch")}>タッチ</button>スワイプみたいなある範囲を動かす場合
<div onTouchMove={() => console.log("move finger")} > ほげほげ </div>なので、React Swipeableやreact-swipeable-viewsなども使うことができます。
プロトタイプはタッチなのかタッチレスなものか
ここでいうタッチは、何か物理的なものに指でタッチする。
タッチレスは、空中でタッチするということです。
タッチなものであれば、旧来の原則ルールを使って進めていくことで簡単にプロトタイプを作れると思います。もし、タッチレスなものを作成するならば
最初は世の中のタッチレスインターフェース設計ガイドラインといった先人の知恵を借りながら進めていく形が、闇雲にすすめるよりかは進めやすいと思います。
ultraleapのtouchless-interfaces( https://docs.ultraleap.com/touchless-interfaces/ )は
Leap motionといった製品向けですが、参考になります。しかし、作るものが使いやすいものなのか、扱うテクノロジーが違うのでガイドラインをそのままというわけにもいかず、
自分も含めいろんな人に作ったものを触ってもらうのが良いと思います。zForce AIRで気をつけるべき点
zForce AIRを空中面に対して活用する場合、ユーザはなれるのですが、もどかしく感じます
タッチ
- センサーが感知しないところから、センサーが感知し押し込まれたときに反応する。
- CSSでは、activeでマウスのhoverのような役割ができるので、そちらを活用する。
タッチムーブ
- センサーがどこを感知しているか、感覚を得るまで上手く操作ができない。
- どこで感知しているかチュートリアル・TIPSを見せてあげると良いと思います
インタラクションデザインをする上で
Gestural Interfaces:A Step Backward In Usabilityの下記のキーワードが参考になりました。
- Visibility
- Feedback
- Consistency and Standards
- Non-destructive operations
- Discoverability
- Scalability
- Reliability
引用: https://www.academia.edu/2850361/Gestural_interfaces_a_step_backward_in_usability
また、最近は、下記本が参考になるなーと読み進めています。
Brave NUI World: Designing Natural User Interfaces for Touch and Gesture最後に
アイディアをユーザーテストをしながら検証していこう
実際にそれを動かしてみないとどれだけ良いアイデアかわからないので、なんか思いついたり
フィードバックもらったら、作って触ってもらっています。空中でそのタッチパネルの世界をそのまま持ってくるというのも、最初にやってみて
触ってもらったときに、スムーズに操作をすることができるということが確認できました。そのため、旧来の原理を念頭におきユーザーのフラストレーションにつながらないように、
zForce AIRからLeap motionや他のテクノロジーでのインタラクションに徐々にシフトを進めていっています。いきなり新しいハンドジェスチャーなどを考えていたら、もしかしたらスルーしてしまっていたかもしれません。
小さな学びも一歩一歩大切に、早く物づくりを進めていくのが良いと思います。
モノは完成しましたら、どこかで発表できればと思います。では、またどこかで。
- 投稿日:2020-12-12T21:04:59+09:00
StrapiでCSVからデータをインポートするプラグインを作る ~その1ファイル選択機能まで~
Strapi Advent Calenderの14日目はStrapi公式ブログのCSVからデータをインポートするプラグインを作成する記事のpart1を日本語でまとめてみました。
こちらの記事を参考に作成していきます
https://strapi.io/blog/how-to-create-an-import-content-plugin-part-1-4Strapiのカスタムプラグインについては、5日目の記事をどうぞ!
StrapiのLocal Pluginsを利用して独自の画面を作成するプラグインとモデルを作成する
まず、プラグインの雛形と、プラグインの中で使用するモデルを2種類作成します。
$ strapi generate:plugin import-content $ strapi generate:model importconfig --plugin import-content $ strapi generate:model importeditem --plugin import-contentディレクトリ構造は以下のような形になります。
plugins |-- import-content |-- admin |-- config |-- controllers |-- models |-- Importconfig.js |-- Importconfig.settings.json |-- Importeditem.js |-- Importeditem.settings.json |-- servicesモデルに項目を追加する
plugins/import-content/models/Importconfig.settings.jsonに以下をコピペします。plugins/import-content/models/Importconfig.settings.json{ "connection": "default", "collectionName": "", "info": { "name": "importconfig", "description": "" }, "options": { "timestamps": true, "increments": true, "comment": "" }, "attributes": { "date": { "type": "date" }, "source": { "type": "string" }, "options": { "type": "json" }, "contentType": { "type": "string" }, "fieldMapping": { "type": "json" }, "ongoing": { "type": "boolean" }, "importeditems": { "collection": "importeditem", "via": "importconfig", "plugin": "import-content" } } }次に、
plugins/import-content/models/Importeditem.settings.jsonに以下をコピペします。plugins/import-content/models/Importeditem.settings.json{ "connection": "default", "collectionName": "", "info": { "name": "importeditem", "description": "" }, "options": { "increments": true, "timestamps": true, "comment": "" }, "attributes": { "ContentType": { "type": "string" }, "ContentId": { "type": "integer" }, "importconfig": { "model": "importconfig", "via": "importeditems", "plugin": "import-content" }, "importedFiles": { "type": "json" } } }ここまでの手順を踏んで起動すると、管理画面のサイドメニューのコレクションタイプに
ImportconfigsとImporteditemsが、プラグインにimport-contentが表示されていると思います。ここからプラグインの中身を作成していきます。
必要なパッケージをインストールする
今回のプラグインで必要になるパッケージは以下の通りです。
- content-type-parser
- csv-parse
- get-urls
- moment rss-parser
- request
- simple-statistics
- striptags
- lodashyarn add content-type-parser csv-parse get-urls moment rss-parser request simple-statistics striptags lodashコンポーネントを作成する
1. ファイル選択フォームを作成する
plugins/import-content/admin/srcの中にcomponentsディレクトリを作成します。
plugins/import-content/admin/src/componentsにUploadFileFormディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。plugins/import-content/admin/src/components/UploadFileForm/index.jsimport React, { Component } from "react"; import PropTypes from "prop-types"; class UploadFileForm extends Component { state = { file: null, type: null, options: { filename: null } }; onChangeImportFile = file => { file && this.setState({ file, type: file.type, options: { ...this.state.options, filename: file.name } }); }; render() { return <input onChange={({target:{files}}) => files && this.onChangeImportFile(files[0])} name="file_input" accept=".csv" type="file" />; } } export default UploadFileForm;次に、
plugins/import-content/admin/src/containers/HomePage/index.jsに、先ほど作成したUploadFileFormコンポーネントを読み込ませて表示させてみます。plugins/import-content/admin/src/containers/HomePage/index.jsimport React, { memo } from "react"; import pluginId from "../../pluginId"; import UploadFileForm from "../../components/UploadFileForm"; const HomePage = () => { return <UploadFileForm />; }; export default memo(HomePage);ここまでで、CSVファイルを選択するフォームができました。見栄えをよくするために、コンポーネントをいくつか追加します。
次に、
plugins/import-content/admin/src/componentsにPディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。plugins/import-content/admin/src/components/P/index.jsimport styled from "styled-components"; const P = styled.p` margin-top: 10px; text-align: center; font-size: 13px; color: #9ea7b8; u { color: #1c5de7; } `; export default P;
plugins/import-content/admin/src/componentsにRowディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。plugins/import-content/admin/src/components/Row/index.jsimport styled from "styled-components"; const Row = styled.div` padding-top: 18px; `; export default Row;
plugins/import-content/admin/src/componentsにLabelディレクトリを作成します。その中にindex.jsファイルを作成し、以下を記述します。plugins/import-content/admin/src/components/Label/index.jsimport styled, { css, keyframes } from "styled-components"; const Label = styled.label` position: relative; height: 146px; width: 100%; padding-top: 28px; border: 2px dashed #e3e9f3; border-radius: 2px; text-align: center; > input { display: none; } .icon { width: 82px; path { fill: ${({ showLoader }) => (showLoader ? "#729BEF" : "#ccd0da")}; transition: fill 0.3s ease; } } .isDragging { position: absolute; top: 0; bottom: 0; left: 0; right: 0; } .underline { color: #1c5de7; text-decoration: underline; cursor: pointer; } &:hover { cursor: pointer; } ${({ isDragging }) => { if (isDragging) { return css` background-color: rgba(28, 93, 231, 0.01) !important; border: 2px dashed rgba(28, 93, 231, 0.1) !important; `; } }} ${({ showLoader }) => { if (showLoader) { return css` animation: ${smoothBlink("transparent", "rgba(28,93,231,0.05)")} 2s linear infinite; `; } }} `; const smoothBlink = (firstColor, secondColor) => keyframes` 0% { fill: ${firstColor}; background-color: ${firstColor}; } 26% { fill: ${secondColor}; background-color: ${secondColor}; } 76% { fill: ${firstColor}; background-color: ${firstColor}; } `; export default Label;作成した3つのコンポーネントを、
UploadFileFormにインポートして、ファイルをドラッグ&ドロップで選択できるエリアも作成していきます。plugins/import-content/admin/src/components/UploadFileForm/index.jsimport React, { Component } from "react"; import PropTypes from "prop-types"; import P from "../P"; // 追加 import Row from "../Row"; // 追加 import Label from "../Label"; // 追加 import { Button } from "@buffetjs/core"; // 追加 class UploadFileForm extends Component { state = { file: null, type: null, options: { filename: null }, isDragging: false // 追加 }; onChangeImportFile = file => { file && this.setState({ file, type: file.type, options: { ...this.state.options, filename: file.name } }); }; handleDragEnter = () => this.setState({ isDragging: true }); // 追加 handleDragLeave = () => this.setState({ isDragging: false }); // 追加 handleDrop = e => { // 追加 e.preventDefault(); this.setState({ isDragging: false }); const file = e.dataTransfer.files[0]; this.onChangeImportFile(file); }; render() { return ( // 書き換え <div className={"col-12"}> <Row className={"row"}> <Label showLoader={this.props.loadingAnalysis} isDragging={this.state.isDragging} onDrop={this.handleDrop} onDragEnter={this.handleDragEnter} onDragOver={e => { e.preventDefault(); e.stopPropagation(); }} > <svg className="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104.40317 83.13328" > <g> <rect x="5.02914" y="8.63138" width="77.33334" height="62.29167" rx="4" ry="4" transform="translate(-7.45722 9.32921) rotate(-12)" fill="#fafafb" /> <rect x="5.52914" y="9.13138" width="76.33334" height="61.29167" rx="4" ry="4" transform="translate(-7.45722 9.32921) rotate(-12)" fill="none" stroke="#979797" /> <path d="M74.25543,36.05041l3.94166,18.54405L20.81242,66.79194l-1.68928-7.94745,10.2265-16.01791,7.92872,5.2368,16.3624-25.62865ZM71.974,6.07811,6.76414,19.93889a1.27175,1.27175,0,0,0- .83343.58815,1.31145,1.31145,0,0,0-.18922,1.01364L16.44028,71.87453a1.31145,1.31145,0,0,0,.58515.849,1.27176,1.27176,0,0,0,1.0006.19831L83.23586,59.06111a1.27177,1.27177,0,0,0,.83343- .58815,1.31146,1.31146,0,0,0,.18922-1.01364L73.55972,7.12547a1.31146,1.31146,0,0,0-.58514-.849A1.27177,1.27177,0,0,0,71.974,6.07811Zm6.80253- .0615L89.4753,56.35046A6.5712,6.5712,0,0,1,88.554,61.435a6.37055,6.37055,0,0,1-4.19192,2.92439L19.15221,78.22019a6.37056,6.37056,0,0,1-5.019-.96655,6.57121,6.57121,0,0,1-2.90975- 4.27024L.5247,22.64955A6.57121,6.57121,0,0,1,1.446,17.565a6.37056,6.37056,0,0,1,4.19192-2.92439L70.84779.77981a6.37055,6.37055,0,0,1,5.019.96655A6.5712,6.5712,0,0,1,78.77651,6.01661Z" transform="translate(-0.14193 -0.62489)" fill="#333740" /> <rect x="26.56627" y="4.48824" width="62.29167" height="77.33333" rx="4" ry="4" transform="translate(0.94874 87.10632) rotate(-75)" fill="#fafafb" /> <rect x="27.06627" y="4.98824" width="61.29167" height="76.33333" rx="4" ry="4" transform="translate(0.94874 87.10632) rotate(-75)" fill="none" stroke="#979797" /> <path d="M49.62583,26.96884A7.89786,7.89786,0,0,1,45.88245,31.924a7.96,7.96,0,0,1-10.94716-2.93328,7.89786,7.89786,0,0,1-.76427-6.163,7.89787,7.89787,0,0,1,3.74338- 4.95519,7.96,7.96,0,0,1,10.94716,2.93328A7.89787,7.89787,0,0,1,49.62583,26.96884Zm37.007,26.73924L81.72608,72.02042,25.05843,56.83637l2.1029- 7.84815L43.54519,39.3589l4.68708,8.26558L74.44644,32.21756ZM98.20721,25.96681,33.81216,8.71221a1.27175,1.27175,0,0,0-1.00961.14568,1.31145,1.31145,0,0,0- 10 .62878.81726L18.85537,59.38007a1.31145,1.31145,0,0,0,.13591,1.02215,1.27176,1.27176,0,0,0,.80151.631l64.39506,17.2546a1.27177,1.27177,0,0,0,1.0096-.14567,1.31146,1.31146,0,0,0,.62877-.81726l13.3184- 49.70493a1.31146,1.31146,0,0,0-.13591-1.02215A1.27177,1.27177,0,0,0,98.20721,25.96681Zm6.089,3.03348L90.97784,78.70523a6.5712,6.5712,0,0,1-3.12925,4.1121,6.37055,6.37055,0,0,1- 5.06267.70256L18.39086,66.26529a6.37056,6.37056,0,0,1-4.03313-3.13977,6.57121,6.57121,0,0,1-.654-5.12581L27.02217,8.29477a6.57121,6.57121,0,0,1,3.12925-4.11211,6.37056,6.37056,0,0,1,5.06267- .70255l64.39506,17.2546a6.37055,6.37055,0,0,1,4.03312,3.13977A6.5712,6.5712,0,0,1,104.29623,29.0003Z" transform="translate(-0.14193 -0.62489)" fill="#333740" /> </g> </svg> <P> <span> Drag & drop your file into this area or <span className={"underline"}>browse</span> for a file to upload </span> </P> <div onDragLeave={this.handleDragLeave} className="isDragging" /> <input name="file_input" accept=".csv" onChange={({ target: { files } }) => files && this.onChangeImportFile(files[0]) } type="file" /> </Label> </Row> </div> ); }} export default UploadFileForm;ここまでで、ファイルをドラッグ&ドロップで選択できるエリアができました。
次は選択したCSVファイルを解析する部分を作成します。
2. インポートソースとインポート先の選択部分を作成する
HomePageのindex.jsを書き換えます。javascriptimport React, { memo, Component } from "react"; import {request} from "strapi-helper-plugin"; // 追加 import PropTypes from "prop-types"; // 追加 import pluginId from "../../pluginId"; import UploadFileForm from "../../components/UploadFileForm"; class HomePage extends Component { state = { // 追加 analyzing: false, analysis: null }; onRequestAnalysis = async analysisConfig => { // 追加 this.analysisConfig = analysisConfig; this.setState({ analyzing: true }, async () => { try { const response = await request("/import-content/preAnalyzeImportFile", { method: "POST", body: analysisConfig }); this.setState({ analysis: response, analyzing: false }, () => { strapi.notification.success(`Analyzed Successfully`); }); } catch (e) { this.setState({ analyzing: false }, () => { strapi.notification.error(`Analyze Failed, try again`); strapi.notification.error(`${e}`); }); } }); }; render() { return ( // 書き換え <UploadFileForm onRequestAnalysis={this.onRequestAnalysis} loadingAnalysis={this.state.analyzing} /> ); } } export default memo(HomePage);次に、
UploadFileFormにファイルを解析する処理を加えます。plugins/import-content/admin/src/components/UploadFileForm/index.jsreadFileContent = file => { const reader = new FileReader(); return new Promise((resolve, reject) => { reader.onload = event => resolve(event.target.result); reader.onerror = reject; reader.readAsText(file); }); }; clickAnalyzeUploadFile = async () => { const { file, options } = this.state; const data = file && (await this.readFileContent(file)); data && this.props.onRequestAnalysis({ source: "upload", type: file.type, options, data }); };同じく
UploadFileFormのrender()内の、一番最後のRowタグの後に、解析用のボタンを設置ます。またexportの前に処理を加えます。plugins/import-content/admin/src/components/UploadFileForm/index.js~~省略~~ <input name="file_input" accept=".csv" onChange={({ target: { files } }) => files && this.onChangeImportFile(files[0]) } type="file" /> </Label> </Row> <Row className={"row"}> // 追加 <Button label={"Analyze"} color={this.state.file ? "secondary" : "cancel"} disabled={!this.state.file} onClick={this.clickAnalyzeUploadFile} /> </Row> </div> ); }} UploadFileForm.propTypes = { // 追加 onRequestAnalysis: PropTypes.func.isRequired, loadingAnalysis: PropTypes.bool.isRequired }; export default UploadFileForm;
componentsディレクトリに、Blockディレクトリを追加し、その中にindex.jsとcomponents.jsを作成し、それぞれのファイルに以下を記述しますadmin/src/components/Block/index.jsimport React, { memo } from "react"; import PropTypes from "prop-types"; import { Wrapper, Sub } from "./components"; const Block = ({ children, description, style, title }) => ( <div className="col-md-12"> <Wrapper style={style}> <Sub> {!!title && <p>{title} </p>} {!!description && <p>{description} </p>} </Sub> {children} </Wrapper> </div> ); Block.defaultProps = { children: null, description: null, style: {}, title: null }; Block.propTypes = { children: PropTypes.any, description: PropTypes.string, style: PropTypes.object, title: PropTypes.string }; export default memo(Block);admin/src/components/Block/index.jsimport React, { memo } from "react"; import PropTypes from "prop-types"; import { Wrapper, Sub } from "./components"; const Block = ({ children, description, style, title }) => ( <div className="col-md-12"> <Wrapper style={style}> <Sub> {!!title && <p>{title} </p>} {!!description && <p>{description} </p>} </Sub> {children} </Wrapper> </div> ); Block.defaultProps = { children: null, description: null, style: {}, title: null }; Block.propTypes = { children: PropTypes.any, description: PropTypes.string, style: PropTypes.object, title: PropTypes.string }; export default memo(Block);
HomePageにインポートと関数を追加し、render()の中を書き換えますimport React, { memo, Component } from "react"; import {request} from "strapi-helper-plugin"; import PropTypes from "prop-types"; import pluginId from "../../pluginId"; import UploadFileForm from "../../components/UploadFileForm"; import { // 追加 HeaderNav, LoadingIndicator, PluginHeader } from "strapi-helper-plugin"; // 追加 import Row from "../../components/Row"; // 追加 import Block from "../../components/Block"; // 追加 import { Select, Label } from "@buffetjs/core"; // 追加 import { get, has, isEmpty, pickBy, set } from "lodash"; // 追加 const getUrl = to => // 追加 to ? `/plugins/${pluginId}/${to}` : `/plugins/${pluginId}`; class HomePage extends Component { importSources = [ // 追加 { label: "External URL ", value: "url" }, { label: "Upload file", value: "upload" }, { label: "Raw text", value: "raw" } ]; state = { loading: true, // 追加 modelOptions: [], // 追加 models: [], // 追加 importSource: "upload", analyzing: false, analysis: null, selectedContentType: "" // 追加 }; selectImportDest = selectedContentType => { // 追加 this.setState({ selectedContentType }); }; componentDidMount() { // 追加 this.getModels().then(res => { const { models, modelOptions } = res; this.setState({ models, modelOptions, selectedContentType: modelOptions ? modelOptions[0].value : "" }); }); } getModels = async () => { // 追加 this.setState({ loading: true }); try { const response = await request("/content-type-builder/content-types", { method: "GET" }); // Remove non-user content types from models const models = get(response, ["data"], []).filter( obj => !has(obj, "plugin") ); const modelOptions = models.map(model => { return { label: get(model, ["schema", "name"], ""), // (name is used for display_name) value: model.uid // (uid is used for table creations) }; }); this.setState({ loading: false }); return { models, modelOptions }; } catch (e) { this.setState({ loading: false }, () => { strapi.notification.error(`${e}`); }); } return []; }; onRequestAnalysis = async analysisConfig => { this.analysisConfig = analysisConfig; this.setState({ analyzing: true }, async () => { try { const response = await request("/import-content/preAnalyzeImportFile", { method: "POST", body: analysisConfig }); this.setState({ analysis: response, analyzing: false }, () => { strapi.notification.success(`Analyzed Successfully`); }); } catch (e) { this.setState({ analyzing: false }, () => { strapi.notification.error(`Analyze Failed, try again`); strapi.notification.error(`${e}`); }); } }); }; selectImportSource = importSource => { // 追加 this.setState({ importSource }); }; render() { return ( // 書き換え <div className={"container-fluid"} style={{ padding: "18px 30px" }}> <PluginHeader title={"Import Content"} description={"Import CSV and RSS-Feed into your Content Types"} /> <HeaderNav links={[ { name: "Import Data", to: getUrl("") }, { name: "Import History", to: getUrl("history") } ]} style={{ marginTop: "4.4rem" }} /> <div className="row"> <Block title="General" description="Configure the Import Source & Destination" style={{ marginBottom: 12 }} > <Row className={"row"}> <div className={"col-4"}> <Label htmlFor="importSource">Import Source</Label> <Select name="importSource" options={this.importSources} value={this.state.importSource} onChange={({ target: { value } }) => this.selectImportSource(value) } /> </div> <div className={"col-4"}> <Label htmlFor="importDest">Import Destination</Label> <Select value={this.state.selectedContentType} name="importDest" options={this.state.modelOptions} onChange={({ target: { value } }) => this.selectImportDest(value) } /> </div> </Row> <UploadFileForm onRequestAnalysis={this.onRequestAnalysis} loadingAnalysis={this.state.analyzing} /> </Block> </div> </div> ); } } export default memo(HomePage);ここまでで、インポートソースとインポート先の選択部分が完成したと思います。
実際にインポートしていく部分は、また後日紹介したいと思います。
- 投稿日:2020-12-12T18:09:31+09:00
React Hookを使ってチャットボットを作成した
概要
React初心者です。
少し前にReactのHookを使って簡単なチャットボットを作成しました。
若干ウザめなチャットボットですが、恥を忍んで記事にさせていただきます。完成したチャットボット
某元テニスプレイヤーの方が熱い言葉で返してくれるチャットボットです。
開発環境
React Hookの学習がメインだったので、UIやサーバ側は既存のサービスに頼りました。
React 16.13.1Material UIFirebase(Firestore、 Functions、Hosting)ファイル構造
src ├── assets │ ├── image │ └── styles ├── components │ ├── Answer.jsx │ ├── AnswersList.jsx │ ├── Chat.jsx │ ├── Chats.jsx │ └── index.js //components内のjsxファイルをまとめたもの ├── App.jsx ├── dataset.json //firestoreへアップするデータ ├── firebase ├── index.js └── serviceWorker.jscomponentsの中身について
index.jsにはcomponentsで作成したjsxファイルをまとめてexportしておきます。
components/index.jsexport {default as AnswersList} from './AnswersList'; export {default as Answer} from './Answer'; export {default as Chats} from './Chats'; export {default as Chat} from './Chat';これにより、一行で複数のcomponentをimportして使えます。
App.jsximport { AnswersList, Chats } from './components/index';データベースについて
Firebaseの「
Firestore」を使いました。まず、保存したいデータをjson形式で作成します。
dataset.json{ "init": { "answers": [ {"content": "仕事で悩んでいます", "nextId": "work"}, {"content": "恋愛に悩んでいます", "nextId": "love"}, {"content": "人生に悩んでいます", "nextId": "life"}, {"content": "ただ元気がありません", "nextId": "health"} ], "question": "おい、そこの君!いつもの笑顔がないぞ!" }, "work": { "answers": [ {"content": "上司に叱られました", "nextId": "work_1"}, {"content": "ミスをしました", "nextId": "work_2"}, {"content": "イライラすることがありました", "nextId": "work_3"}, {"content": "疲れました", "nextId": "work_4"}, {"content": "辞めたいです", "nextId": "work_5"} ], "question": "仕事で何があったんだ?" }, //以下省略「answers」 :ユーザー側の回答
「content」 :ユーザーの回答選択肢
「nextId」 :回答選択後の遷移先キー
「question」 :某Mさんからの自動回答最初は「init」の内容が表示され、その後は選択された内容に応じて次の内容(nextId)が呼び出されるようになっています。
使用したHook
useState、useEffect、useCallbackを使いました。
それぞれの役割についてはこちらをご参考ください。useState
チャット内容、回答選択肢が変更されると、それらを状態として保持し管理します。
const [answers, setAnswers] = useState([]), [chats, setChats] = useState([]);useEffect
最新のチャット内容がチャット下部に常に表示されるようにしたいので、
追加される(scrollAreaの値が変化する)と自動スクロールするようにしています。useEffect(() => { const scrollArea = document.getElementById('chats-area') if(scrollArea) { scrollArea.scrollTop = scrollArea.scrollHeight } })useCallback
初期の回答選択肢を保持し、ユーザーが回答を選択すると、チャット内容を変更&次の回答選択肢を準備する動きをしています。
また、
setTimeoutを使い、ボットっぽく敢えてタイムラグが発生するような動きにしています。const selectAnswer = useCallback((selectedAnswer, nextQuestionId) => { addChats({ text: selectedAnswer, type: 'answer' }) setTimeout(() => displayNextQuestion(nextQuestionId, dataset[nextQuestionId]), 1000); },[answers])最後に
今回はチャットボットを作ってみましたが、Hookの簡単な動きは理解できたかなと思います。
ページ構造が簡易的すぎて少し物足りなさもありますが…
他にも個人開発向けでHookをふんだんに使えそうなものがあればぜひ教えていただけると嬉しいです。挑戦してみたいと思います。チャットボットはこちらのYoutubeのサイトを参考にさせていただきました。
解説が非常にわかりやすいので、特に初心者の方におすすめです。
- 投稿日:2020-12-12T17:56:01+09:00
昔懐かしのアクセスカウンターを作った話
この記事はCivictech1年目 Advent Calendar 2020 12日目の記事です。
11日目は @t-mat さんのシビックテック1年目の取組と課題でした。
私も今年入ってから初めてシビックテックというものを知ったので、この後t振り返ろうかなと思っています。TL;DR
「BADオープンデータ供養寺」建立の宮大工をしていた時に作ったアクセスカウンターが同できてるかをゆるゆる書くっていう記事です。
BADオープンデータ供養寺のソースコードは https://github.com/codeforjapan/bad-opendata-temple です!
いまだに面白いissueが残ってたりしますので皆さんもぜひ...!BADオープンデータ供養寺自体は https://bad-data.rip にありますので興味のある方はぜひ。(.ripなんてドメイン初めて聞いた...)
はじめに
BADオープンデータ供養寺とは
同じく12日目のCivictech テック好きのカレンダーに書いてらっしゃる @kaizumaki さんの BADオープンデータ供養寺、匠たちの技から引用をさせていただきまして...
オープンデータ供養寺とは
住職(※1)の「ぶっ飛んだデザインにした方が良いのでは」というひとことに、腕に覚えありの宮大工(※2)達が結集し、懐かしのインターネッツネタをモダンコーディングで再現しようと力を合わせたのでした。
※1…言わずと知れた我らがリーダー、下山紗代子氏のこと。オープンデータの普及に東へ西へと駆け回っている。すっかり「住職」と呼ばれるのが板についている。
※2…私たちの間ではエンジニアのことを「宮大工」、デザイナーのことを「宮造りデザイナー」としている。そして彼らの名前は「〜〜匠」と呼ばれる。書いてある通りではあるんですが、
- カーソルをローソクにする
- お経を流す
- お鈴を鳴らす
- 餅を投げる
だったり、
- MARQUEEタグじゃないマーキー
- CGIじゃないアクセスカウンター
など、ローテク?昔懐かし?のようなハイテクノロジー()の数々があります
私はこの
CGIじゃないアクセスカウンターを作ったのでこれについてこのあとかこうかと思います(ほかのことについては引用元をぜひ...)CGIじゃないアクセスカウンターってどゆこと????
タイトルにも書いてる通り、今回のアクセスカウンターにCGIを使っていません!
正真正銘Reactのコンポーネントです。
(このタイトルで読みに来る方は大体CGIが何かわかってると思うのでそこは書きません)(なぜcgiじゃないのかというとそれをホストするばしょがないからですね)
アクセスカウンターなのでアクセス数を記録しないといけない。
しかもそれは共通させなきゃいけないのでどこかにDBを持つ必要がある。
じゃあどうしてるのか....
実はGoogle Spreadsheet + Google App Scriptでできています。React側
accessCounter.tsximport React, { ReactElement, useEffect, useState, } from 'react'; import styled from 'styled-components'; const AccessCounterContent = styled.div` font-size: 30px; `; const NumberText = styled.span` color: #ebff00; background-color: #000000; padding: 2px 4px; margin: 0 2px; box-sizing: border-box; `; const AccessCounter: React.FC = (): ReactElement => { const [accessStr, setAccessStr] = useState('LOADING'); useEffect(() => { fetch( 'https://script.google.com/macros/s/えーぴーあいの/exec', ) .then((response) => { return response.json(); }) .then((json) => { setAccessStr( String(json.your_access).padStart(4, '0'), ); return json; }) .catch((_) => { setAccessStr('ERROR'); }); }, []); return ( <AccessCounterContent> あなたは {[...accessStr].map((str, index) => { return ( <NumberText key={str + String(index)}> {str} </NumberText> ); })} 人目の参拝者です </AccessCounterContent> ); }; export default AccessCounter;分けて解説をすると..
const AccessCounterContent = styled.div` font-size: 30px; `; const NumberText = styled.span` color: #ebff00; background-color: #000000; padding: 2px 4px; margin: 0 2px; box-sizing: border-box; `;ここでデザインをかいていて...
const AccessCounter: React.FC = (): ReactElement => { const [accessStr, setAccessStr] = useState('LOADING'); useEffect(() => { fetch( 'https://script.google.com/macros/s/えーぴーあいのid/exec', ) .then((response) => { return response.json(); }) .then((json) => { setAccessStr( String(json.your_access).padStart(4, '0'), ); return json; }) .catch((_) => { setAccessStr('ERROR'); }); }, []);React FCを使っているので、useStateでStateを作成(初期値にLOADINGと文字列を入れておきます)
useEffectでcomponentDidMountedでこの後作るAPIにアクセスしてアクセス数を読み取ります。
ちなみにString(json.your_access).padStart(4, '0'),これは23とかで帰ってきた数字を0023とかにするものです
return ( <AccessCounterContent> あなたは {[...accessStr].map((str, index) => { return ( <NumberText key={str + String(index)}> {str} </NumberText> ); })} 人目の参拝者です </AccessCounterContent> );これは、表示を
こうしたかったので、
[...accessStr].map((str, index) => {で一文字ずつに分割してmapで回して、<NumberText key={str + String(index)}> {str} </NumberText>で一文字ずつ分割したコンポーネントとしています。
これが、Reactコンポーネントの動きです。
App Scriptのスクリプトくん
function doGet(e) { var sheet = SpreadsheetApp.getActive().getSheetByName('シート1'); var accessNumber = Number(sheet.getRange("A1").getValues()) + 1; sheet.getRange("A1").setValue(accessNumber); var result = { "your_access": accessNumber } var out = ContentService.createTextOutput(); //Mime TypeをJSONに設定 out.setMimeType(ContentService.MimeType.JSON); //JSONテキストをセットする out.setContent(JSON.stringify(result)); return out; }簡単な内容ですのでほぼ解説しませんが....
スプシはつないでいるのでvar sheet = SpreadsheetApp.getActive().getSheetByName('シート1'); var accessNumber = Number(sheet.getRange("A1").getValues()) + 1; sheet.getRange("A1").setValue(accessNumber);でA1に格納している数値を取得して
var result = { "your_access": accessNumber } var out = ContentService.createTextOutput(); //Mime TypeをJSONに設定 out.setMimeType(ContentService.MimeType.JSON); //JSONテキストをセットする out.setContent(JSON.stringify(result)); return out;でgetへの返り値としてます
あとはこれを
ウェブアプリケーションとして導入をすればアクセスカウンターのAPIの出来上がりです!おわりに
結構前からいんたーねっとにいる学生ではあるのでアクセスカウンターのcgiは見たことはあるし、使ったこともあるんですが実際に作ったのは初めてでした...
そしてそれ以上にcgiじゃないアクセスカウンターを作ることになるとは.....こんな面白いものがいろいろそろったBADオープンデータ供養寺をぜひご覧ください!(https://bad-data.rip)
- 投稿日:2020-12-12T17:22:10+09:00
簡単にDockerでreact環境を速攻で作る
こんにちは、IT技術者の田中です。
簡単にDockerでreact環境を速攻で作る背景
Dockerでreact環境を速攻で作る最新(2020年12月12日)情報がなく困ったので、記事を書きました!
(色々惜しい記事はありましたが、ちょこちょこミスがあり困りました)前提としてはmacbook proで構築しました!
フォルダ構成
下記のフォルダ構成で作ります。
react
├ Dockerfile
├ docker-compose.yml
└ react-sample/Dockerfile
Dockerfileの内容
FROM node:15.3.0-alpine WORKDIR /usr/src/appdocker-compose.yml
docker-compose.ymlの内容
version: '3' services: node: build: context: . dockerfile: Dockerfile volumes: - ./:/usr/src/app command: sh -c "cd react-sample && yarn start" ports: - "3000:3000"dockerビルドを実行する
docker-compose.ymlのフォルダに移動します。
> docker-compose build上記が完了するまで待ちます。
docker-composeでreactをプロジェクト作成
下記のコマンドで、reactのプロジェクトを作成する。
> docker-compose run --rm node sh -c "npm install -g create-react-app && create-react-app react-sample"dockerのコンテナを起動する
下記のコマンドでdockerコンテナを起動する。
docker-compose upreactの起動を確認する
ブラウザーで下記のURLにアクセスする
http://localhost:3000終わり!
- 投稿日:2020-12-12T16:04:42+09:00
ReactとPHP 初めての連携
はじめに
今回はReactで簡単なフォームを作って、PHPの方で受け取るプログラムを書いてみました。Laravelではやったことがあるのですが、フレームワーク無しのPHPでやったことがなくて、フレームワーク無しのPHPをだいぶ忘れてしまったのでやってみました。
テキストを入力して送信したら、そのテキストが表示されて、何も入力しなければエラーが表示される感じのシンプルなフォームです。
以下を参考にさせていただきました。
参考:Create a Contact Form With PHP and React in 3 MinReact側
package.json{ "name": "simple-form", "version": "1.0.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.2", "@types/axios": "^0.14.0", "@types/node": "^14.14.12", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "axios": "^0.21.0", "react": "^16.11.0", "react-dom": "^16.11.0", "react-hook-form": "^6.13.0", "react-scripts": "^4.0.1", "styled-components": "^5.2.1", "ts-node": "^9.1.1", "typescript": "^4.1.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] }↑使用したpackageです。react-hook-formとmaterial-uiを使用していますが、これらの説明は割愛します。
↓react-hook-formの記事も書いているので良ければ是非!
react-hook-formの使い方を解説!V6.12.0追加
index.tsximport React from "react"; import ReactDOM from "react-dom"; import { App } from "./App"; ReactDOM.render(<App />, document.getElementById("root"));index.tsxでApp.tsxからインポートしています。見慣れた形だと思います。
import React, { useState } from "react"; import axios from "axios"; import { Button, CircularProgress, styled, TextField } from "@material-ui/core"; import { useForm } from "react-hook-form"; type FormData = { text: string; }; export const App: React.FC = () => { const [message, setMessage] = useState(""); const [error, setError] = useState(false); const { register, handleSubmit, formState: { isSubmitting }, } = useForm<FormData>(); const onSubmit = async (data: FormData) => { try { const res = await axios.post("http://localhost:8080/index.php", data); setError(res.data.error); setMessage(res.data.message); } catch { setError(true); setMessage("通信に失敗しました。"); } }; return ( <> <Form onSubmit={handleSubmit(onSubmit)}> <TextField defaultValue="" margin="normal" variant="outlined" name="text" error={error} inputRef={register} helperText={message} /> <Button type="submit" variant="contained" color="primary" disabled={isSubmitting}> {isSubmitting ? <CircularProgress size={24} /> : "送信"} </Button> </Form> </> ); }; const Form = styled("form")({ display: "flex", flexDirection: "column", width: 300, margin: "0 auto", });axiosで
http://localhost:8080/index.phpにフォームの値を送信し、レスポンスとして、errorとmessageが入った連想配列を受け取ります。PHPの方のコードを見ればイメージがしやすいと思います。PHP側
PHPはDockerでnginxを使い、
http://localhost:8080で立ち上げました。MAMPとかを使っても簡単にできると思います。index.php<?php header("Access-Control-Allow-Origin: *"); header('Access-Control-Allow-Headers: Content-Type'); $rest_json = file_get_contents("php://input"); // JSONでPOSTされたデータを取り出す $_POST = json_decode($rest_json, true); // JSON文字列をデコード if(empty($_POST['text'])) { echo json_encode( [ "error" => true, "message" => "Error: 入力してください。", ] ); } else { echo json_encode( [ "error" => false, "message" => 'Success: 入力されたテキスト→'.$_POST['text'], ] ); }
- Access-Control-Allow-Originで異なるオリジンからのアクセスを可能に
- Access-Control-Allow-Headersで使用可能なHTTPヘッダーを設定
- file_get_contentsでJSONでPOSTされたデータを取り出す
- POSTされたデータがなければ、errorがtrueでエラーメッセージを返し、POSTされたデータがあれば、errorがfalseでPOSTされたデータを返す感じです
終わりに
ここまで読んでいただきありがとうございます!細かい説明があまりできていませんが、初めてフロントエンドとサーバーサイドで連携する時のサンプルとしてみていただければと思います。次はデータベースを使って簡単なCRUDをできるようにしたいなと思います。文章力あまりないのでわかりにくいかもしれませんが、少しずつ続けながら上げていきたいと思います。
- 投稿日:2020-12-12T15:48:09+09:00
Gatsbyでフォントをセルフホスティングする方法
はじめに
GatsbyでWebフォントをセルフホスティングする方法を紹介します。
セルフホスティングをすると、Google Fontsなどのサービスからフォントを取得する場合と比べて、パフォーマンスが向上するというメリットがあります。フォントのセルフホスティングとは
セルフホスティングとは、自前のサーバに置いたフォントファイルをwebページのフォントとして使用することです。
Google Fontsなどのサービスがもつサーバからフォントを取得する方法とは異なります。セルフホスティングするメリット
セルフホスティングをするメリットは、Webページのパフォーマンスが向上することです。
サービスがもつサーバからWEBフォントを取得する場合は、そのサーバにHTTPリクエストをする必要があり、日本語などの文字の種類が多いフォントの場合は、大きなボトルネックになってしまいます。
しかしながら、セルフホスティングをすると、上記のフォント取得による遅延を軽減することができます。使用するパッケージ
Fontsourceというモノレポから好きなフォントを選び、インストールします。
自前でフォントファイルを、デュレクトリに配置するという方法もありますが、設定が面倒臭いのでライブラリに頼ることとします。実装方法
Google Fontsにも存在する「Noto Sans JP」をインストールすることを例に実装方法を説明します。
パッケージのインストール
まずパッケージをインストールします。
yarn add fontsource-open-sansパッケージマネージャーにnpmを使用している場合は下記の通りです。
npm install fontsource-open-sans
Layout.jsにimportする
Layout.jsに下記のようにimportします。import 'fontsource-noto-sans-jp';上記では、デフォルトのフォントウェイトに、全てのスタイル(Boldなど)が含まれる状態でimportしてしまうため、下記のように使用するフォントウェイトとスタイルを指定すると、ペイロードサイズを削減できます。
import "fontsource-noto-sans-jp/700.css" import "fontsource-noto-sans-jp/900-normal.css"あとは、CSSで
font-familyを指定してあげれば、使用ができるようになります。まとめ
Fontsourceというライブラリを使用して、GatsbyでWebフォントをセルフホスティングする方法を紹介しました。
参考になれば幸いです!
- 投稿日:2020-12-12T03:48:04+09:00
CreateReactAppにWebVitals計測ライブラリが入ったので試してみた
この記事はReact #2 Advent Calendar 2020の12日目の記事です
概要
- 最近はNext.jsばかり使ってますが久しぶりにCreateReactAppを使ってみたら
reportWebVitals.tsという見慣れないファイルが生成されていたのを見たのが今日の記事のきっかけです- Reactアプリの雛形生成ツールであるCreateReactAppはv4でWebVitalsの計測ライブラリがデフォルトで組み込まれるようになりました
- せっかくなのでドキュメントに沿って少し試してみたいと思います
ファイルの確認
- まずは生成されたコードを見てみましょう
create-react-appコマンドで雛形を生成しますnpx create-react-app --template typescript web-vitals-sample
- 生成されたファイルの一覧はこんな感じです
. ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock
reportWebVitals.tsが今回注目するファイルですね- 中身はこんな感じです
src/reportWebVitals.tsimport { ReportHandler } from 'web-vitals'; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals;
web-vitalsというライブラリを使っていろいろ設定してくれていますweb-vitalsはGoogleChromeが提供するWebVitals計測ライブラリですね- このライブラリの追加とセットアップを事前にやってくれているということのようです
使い方
- 使い方はCreateReactAppのドキュメントに載っています
reportWebVitals.tsでexportされたreportWebVitalsを呼び出して引数に関数を渡して使います
- 渡した関数が内部で呼び出され各種メトリクスが渡されます
- サンプルにあるように
console.logを引数として渡してみますsrc/index.tsximport React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root'), ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(console.log);
- ページを表示するとコンソールにLCPなどのWebVitalsの指標が表示されました!
function sendToAnalytics(metric: Metrics) { const body = JSON.stringify(metric); const url = 'https://example.com/analytics'; // Use `navigator.sendBeacon()` if available, falling back to `fetch()` if (navigator.sendBeacon) { navigator.sendBeacon(url, body); } else { fetch(url, { body, method: 'POST', keepalive: true }); } } reportWebVitals(sendToAnalytics);function sendToAnalytics({ id, name, value }) { ga('send', 'event', { eventCategory: 'Web Vitals', eventAction: name, eventValue: Math.round(name === 'CLS' ? value * 1000 : value), // values must be integers eventLabel: id, // id unique to current page load nonInteraction: true, // avoids affecting bounce rate }); } reportWebVitals(sendToAnalytics);試してみた
- 送信されたデータをDBに保存するAPIを作って適当にグラフ表示してみました
- どういう風に集計するのが有用かはもうちょっとちゃんと考えないといけないですね・・
まとめ
- CreateReactAppに新しく入ったWebVitalの計測機能はweb-vitalsライブラリのセットアップを事前にしてくれているというものでした
- 取得できるメトリクスを収集するのは簡単なのでそれをどう活用していくか考えていけばよさそうですね
- 投稿日:2020-12-12T01:42:59+09:00
【React】一定時間で消える表示を作る
前回作ったサクセスメッセージがいつまでも表示されるのは如何なものかと思い、今回は表示後に何秒か経ったら消えるようにしました。
前回
【React】Railsでsaveされた時に、Reactでサクセスメッセージを表示今回の完成形
reactで表示したメッセージをn秒後に消えるようにしました pic.twitter.com/H3YI09obJZ
— きたやまかつや (@kitakatsu1992) December 11, 2020思った以上に簡単、と言うか思いっきりJavaScriptの書き方だったので勉強不足を感じました。
setTimeout()を使ってstateを変更する
前回、state の値が true か false かでメッセージを表示させる、という風に作成しました。
初期値がfalseのstateをtrueでメッセージを表示させたので、逆にtrueをfalseに戻せばOKということです。
まずは初期値。
state = { books: [], successCreate: false }createBook = (title, body) => { axios .post('http://localhost:3001/books', { title: title, body: body}) .then((response) => { const newData = update(this.state.books, { $push: [response.data]}) this.setState({books: newData, successCreate: true}) }) }rails側のcreateが呼ばれ、データがsaveされるとfalseがtrueに変わる、というものです。
このtrueを何秒後かにfalseにsetStateするようにします。
createBook = (title, body) => { axios .post('http://localhost:3001/books', { title: title, body: body}) .then((response) => { const newData = update(this.state.books, { $push: [response.data]}) this.setState({books: newData, successCreate: true}) // ここから追記 setTimeout(function(){ this.setState({successCreate: false}) }.bind(this),3000) }) }これは3秒後にstateを変更するようにしています。
stateがfalseになるので表示されていたメッセージは消えるようになります。
おそらくあまりに初歩的な内容なので調べても全然出てこなかった!!
もっと精進してさくっと実装できる力を付けたいものです
- 投稿日:2020-12-12T01:05:58+09:00
【React】Railsでsaveされた時に、Reactでサクセスメッセージを表示
個人のブログで、ReactとRailsでDBとやりとりする記事を作りました
フロントReact、バックRailsでデータベースに保存する
某プログラミングスクール(◯MM)で課題で出されたものをReactで作ってみて、そこにサクセスメッセージを出そう、と言うのが今回の目的です。
メモ書き程度で書いていきます。
完成後の動作
Reactでサクセスメッセージを出せるようにしました pic.twitter.com/TjBkS0008y
— きたやまかつや (@kitakatsu1992) December 11, 2020フォーム内容の保存に成功するとサクセスメッセージが呼び出されます。
流れとしては、
- stateを作る
- createが呼ばれsaveされたらstateを変更する
- stateをコンポーネントに渡してメッセージを表示させる
こんな感じです。
stateを作る
state = { books: [], successCreate: false }successCreateと言うstateを作りました。
ここは初期値はfalseとして、createが呼ばれたらtrueとします。createが呼ばれsaveされたらstateを変更する
createBook = (title, body) => { axios .post('http://localhost:3001/books', { title: title, body: body}) .then((response) => { const newData = update(this.state.books, { $push: [response.data]}) this.setState({books: newData, successCreate: true}) }) }Rails側でbooksコントローラーのcreateを呼び出し、保存されたデータはbooksに追加し、successCreateはtureに変更します。
ちなみにbooksにはBookモデルのデータが全て入っています。stateをコンポーネントに渡してメッセージを表示させる
このsuccessCreateの値をコンポーネントに渡します。
<BookCreateForm createBook={this.createBook} successCreate={this.state.successCreate} />受け取った先で、stateがtrueの時にメッセージを表示するよう記述します。
{this.props.successCreate && ( <p style={{color: '#16de69', fontSize: 6}}>Book was successfully created.</p> )}これでBookがcreateされ、stateが変更した時にメッセージが出るようになりました。
react側とrails側でバリデーションをかけてより安全性を高めれば、思いがけないメッセージの表示はなくなるかと。
忘備録的なものなのでとりあえず動いた時点で記事にしております、あしからずです。
- 投稿日:2020-12-12T00:28:20+09:00
Reactを書く時に気をつけたいこと
はじめに
- Reactを使っての開発経験が約1年となったので私がコーディングする上で気をつけていること、これから気をつけていきたいことをこの記事にまとめたいと思います。
コンポーネントのスタイリングでインラインスタイルは避ける
- style属性でコンポーネントのスタイリングを行うと小さなコンポーネントはまだ大丈夫なのですが大きなコンポーネントになってくる程、可読性が下がってくる気がしています。
- cssに切り出す、もしくはstyled-componentを使うとかが良さそうな気がしています(まだ試したこと無い)。
- プロジェクトではmaterial-uiを使っているので、試しにmakeStylesを使ってスタイリングを行ってみましたが感想は「うーん...」でした。
- Reactの公式でもstyle属性を用いた方法は推奨されていないようです。
不要なdivタグは避ける
- Reactでは複数タグをreturnすることはできないので、致し方なく
<div>を使うことが多いと思います。// pタグbuttonタグの2つは返せないのでdivタグで囲わざるを得ない const Component = () => { return( <div> <p>へい</p> <button type='button'>ボタン</button> </div> ); };
- こういった時はフラグメントを使います。
- フラグメントを使うことで不要なタグがrenderされません。
// <> はFlagmentの省略記法 const Component = () => { return( <> <p>へい</p> <button type='button'>ボタン</button> </> ); };export default を使わない
- 少し前までは積極的に
export defaultを使用していたのですがどうやらあまりよろしくないらしいのでこれからはexportを使っていきたいと思います。
export defaultとexportが混在しているとimport文で{}が必要だったり要らなかったりといった細かな違いもあるのでexportで統一していきたい所存。// OK export const Component = () => { return( . . . ); }; // NG export default Component;.tsx(.jsx) と .ts(.js)を使い分ける
- Reactを使用しているファイルには
.tsxを、使用していないファイルには.tsを付けるようにしています。
- こうすることでコンポーネントに関するファイルかどうかを拡張子で見分けがつきやすいです。
- 拡張子の機能の違いはstack overflowに詳しく書いてありました。
コンポーネントのプロパティに直接ごちゃごちゃ書かない
- 例えば、以下のようなコンポーネントがあったとします。
interface SubmitButtonProps { label: string; onSubmit: () => void; }; const SubmitButton: React.FC<SubmitButtonProps> = (props: SubmitButtonProps) => { return ( <button type='submit' name='submit-button'>{props.label}</button> ); }; const SimpleForm: React.FC<SimpleFormProps> = (props: SimpleFormProps) => { return ( <div className='simple-form'> <input type='text' name='login-id-field'/> <input type='password' name='password-field'/> <SubmitButton label={送信} onSubmit={() => { . . // 複数行 . }} /> </div> ); };
onSubmitプロパティに直接書くのではなくhandlerメソッドを定義してそれを呼ぶようにする
- こうするとプロパティ内がごちゃごちゃせずスッキリする。
const SimpleForm: React.FC<SimpleFormProps> = (props: SimpleFormProps) => { return ( <div className='simple-form'> <input type='text' name='login-id-field'/> <input type='password' name='password-field'/> <SubmitButton label={送信} onSubmit={handleSubmit} /> </div> ); }; const handleSubmit = () => { // ここにonSubmitで行いたいことを書く };import文の順序
- import文が下のようにまとまりがなくごちゃごちゃしていると「ぬぉおおおお…」となるタイプです。
import React from 'react'; import { hogeGetAction } from '../action/HogeAction'; import { ChildComponentA } from '../ChildComponentA'; import { Button } from 'material-ui/@core' import { connect } from 'react-redux'; import { ChildComponentB } from '../ChildComponentB'; import { fooGetAction } from '../action/FooAction'; const SampleComponent = () => {...};
- 私は
サードパーティライブラリ → UIフレームワーク → 自作コンポーネント → その他の順で書くようにしています。import React from 'react' import { connect } from 'react-redux'; import { Button } from 'material-ui/@core'; import { ChildComponentA } from '../ChildComponentA'; import { ChildComponentB } from '../ChildComponentB'; import { hogeGetAction } from '../action/HogeAction'; import { fooGetAction } from '../action/FooAction'; const SampleComponent = () => {...};
- スッキリ。自己満。
![]()
その他
- React Hooks APIをふんだんに使って何か作りたい。




















