🔃

ふりかえり手法をランダムで決めるアプリを作ってみたのでそのふりかえりをします

2022/08/16に公開

ふりかえり手法をランダムで決めてくれるアプリを作ったので簡単ふりかえりをします!
良かったら触ってみて遊んでみてください!
http://retro-suggestion.s3-website-ap-northeast-1.amazonaws.com/

使用した技術

  • React
  • Material-UI(MUI)

なぜ作ったか

フロントエンドのフレームワークを触ってみたかった、そしてReact流行っているので触ってみたかったので、まずはReactのチュートリアルをやってみました。すごく楽しかったので、何か簡単にWebアプリを作りたくなったのがきっかけです。
そんな時に思い付いたのが今回のふりかえり手法をランダムで決めてくれるアプリです。半年くらい前チーム内でふりかえり手法を毎回変えていたのですが、ふりかえり手法をどれ使うか決めるのがめんどくさい迷うときがあったので、ふりかえり手法をランダムで決めてくれるアプリがあればなーと思っていました。
あまり複雑なことをやらずにお手軽にできそうだなで、ちょうどいいなと思い作ってみました。

頑張らなかったこと

簡単なアプリを作ることをゴールとしました。そのためHooksや関数コンポーネントといったReactの機能、ドメイン設定やHTTPS化といったインフラ周りの設定は、今回見送りました。

よかったこと

一つは、HTMLやCSSを直に書かずに、MUIを使ってそれっぽく綺麗なレイアウトに仕上げることができた点です。今までHTML、 CSSを直で書くことが多かったので、それと比較すると便利すぎて感動しました...!
もう一つは、様々なふりかえり手法を知れた点です。ふりかえり手法はたくさんありますが、ふりかえり手法が一番まとまっている資料はふりかえりカタログと思ったため、これを基にふりかえり手法を出力しようと元ネタにさせていただきました。アプリ用のデータを作る時にふりかえりカタログとにらめっこしてたので、ふりかえりカタログで紹介されている様々なふりかえり手法を知れました。

アプリのコード
構成
src/
├── component
│   ├── AppHeader.js
│   ├── Search.js
│   └── SearchResult.js
├── index.css
├── index.js
├── retrospective.json
└── retrospectiveSceneName.json
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import AppHeader from "./component/AppHeader";
import Search from "./component/Search";
import SearchResult from "./component/SearchResult";

import { Container } from "@mui/material";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      determinedRetrospective: {
        title: null,
        easyToUseScenes: null,
        wayOfProceeding: null,
        reference: null,
      },
    };
  }

  render() {
    return (
      <div>
        <AppHeader />
        <Container sx={{ mb: 1}}>
          <Search
            onClick={(obj) => this.setState({ determinedRetrospective: obj })}
          />
          <SearchResult {...this.state.determinedRetrospective} />
        </Container>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
AppHeader.js
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';


export default function AppHeader() {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar position="static" color="inherit">
        <Toolbar>
          <Typography variant="h5" component="div" sx={{ flexGrow: 1 }}>
            ふりかえり手法抽選
          </Typography>
        </Toolbar>
      </AppBar>
    </Box>
  );
}
Search.js
import React from "react";

import { Box } from "@mui/material";
import FormGroup from "@mui/material/FormGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import Checkbox from "@mui/material/Checkbox";
import Button from "@mui/material/Button";
import SearchIcon from "@mui/icons-material/Search";
import Alert from "@mui/material/Alert";

import retrospectiveData from "../retrospective.json";
import retrospectiveSceneName from "../retrospectiveSceneName.json";

export default class Search extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      checkedScenes: [],
      errorMessage: null,
    };
  }

  handleChangeCheckedScenes(event) {
    const checkedScenes = this.state.checkedScenes;
    const targetValue = parseInt(event.target.value, 10);
    const changedCheckedScenes = event.target.checked
      ? [...checkedScenes, targetValue]
      : checkedScenes.filter(
          (checkedScene) => checkedScene !== targetValue
        );

    this.setState({
      checkedScenes: changedCheckedScenes,
    });
  }

  handleClickSearch(func) {
    if (this.state.errorMessage !== null) {
      this.setState({ errorMessage: null });
    }
    if (this.state.checkedScenes.length === 0) {
      this.setState({
        errorMessage: "チェックを入れて検索してください",
      });
      return;
    }

    const matchedRetrospectives = retrospectiveData.retrospectives.filter(
      (retrospective) => {
        //チェックボックスはAND条件で検索する
        return this.state.checkedScenes.every((checkedScene) => {
          return retrospective.easyToUseScenes.includes(checkedScene);
        });
      }
    );

    const determinedRetrospective =
      matchedRetrospectives[
        Math.floor(Math.random() * matchedRetrospectives.length)
      ];
    func(determinedRetrospective);
  }

  render() {
    const alert =
      this.state.errorMessage === null ? null : (
        <Alert severity="error">{this.state.errorMessage}</Alert>
      );

    const checkBoxes = Object.entries(retrospectiveSceneName).map(
      ([num, name]) => {
        return (
          <FormControlLabel
            control={<Checkbox />}
            label={name}
            onChange={(e) => this.handleChangeCheckedScenes(e)}
            value={num}
            key={num}
          />
        );
      }
    );

    const checkBoxesArea = (
      <FormControl component="fieldset">
        <FormLabel component="legend">場面ごとで使いやすいふりかえり手法</FormLabel>
        <FormGroup aria-label="position" row>
          {checkBoxes}
        </FormGroup>
      </FormControl>
    );

    const searchButton = (
      <Button
        variant="contained"
        startIcon={<SearchIcon />}
        onClick={() => this.handleClickSearch(this.props.onClick)}
      >
        検索
      </Button>
    );

    return (
      <>
        <Box sx={{ m: 1.5 }}>
          {alert}
          {checkBoxesArea}
          <div>{searchButton}</div>
        </Box>
      </>
    );
  }
}
SearchResult.js
import React from "react";

import { Card, Box } from "@mui/material";
import Button from "@mui/material/Button";
import CardActions from "@mui/material/CardActions";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";

import retrospectiveSceneName from "../retrospectiveSceneName.json";

export default class SearchResult extends React.Component {
  render() {
    const scenes = this.props.scenes;
    const wayOfProceedings = this.props.wayOfProceeding;

    const displayScene =
      scenes === null || scenes === undefined
        ? null
        : scenes
            .map((val, _) => {
              //コードからふりかえりの使いやすい場面の名称変換
              return retrospectiveSceneName[String(val)];
            })
            .join("、");

    const displayWayOfProceedings =
      wayOfProceedings === null || wayOfProceedings === undefined
        ? null
        : wayOfProceedings.split("\n").map((val, idx) => {
            return <li key={idx}>{val}</li>;
          });

    //未選択時は非表示
    const title = this.props.title;
    if (title === null || title === undefined) {
      return null;
    }

    return (
      <Box>
        <Card sx={{ minWidth: 275 }}>
          <CardContent>
            <Typography variant="h4" component="div">
              {this.props.title}
            </Typography>
            <Typography
              sx={{ fontSize: 14 }}
              color="text.secondary"
              gutterBottom
            >
              {displayScene}
            </Typography>
            <Typography variant="h5" component="div">
              進め方
            </Typography>
            <Typography
              variant="body2"
              style={{ whiteSpace: "pre-line" }}
              component={"div"}
            >
              <ul>{displayWayOfProceedings}</ul>
            </Typography>
          </CardContent>
          <CardActions>
            <Button size="small" href={this.props.reference} target="_blank">
              引用元リンク
            </Button>
          </CardActions>
        </Card>
      </Box>
    );
  }
}

参考にさせていただいた情報元リンク

https://qiita.com/viva_tweet_x/items/cc3bad3bd298406b6cc7
https://speakerdeck.com/viva_tweet_x/retrospective-catalog-59bd3a29-314c-45dd-911b-f8e5f1308333

Discussion