React+AWS環境を使ってアンテナサイト(RSS)を作成①

Reactでアンテナサイトを作成してみます。

Ishiguro Suguru

いわゆるアンテナサイトをReactで作ってみよう思い、その時のメモを公開しておきます。 まずはフロントエンド部分を作っていきます。

環境

  • node: v17.0.0
  • npm: 8.1.0
  • yarn: 1.22.19
  • react: 18.2.0

※reactのバージョンはpackage.json参照

参考

React + Material-UIで管理画面を作成してみた

本記事で出来上がるもの

AWSのS3にビルドしたファイルをアップロードしています。

デモサイト

1.React環境構築

まずはReact環境を作成します。

# 新規アプリ作成(TypeScript)
create-react-app --template typescript anntena-rss
# 作成したアプリのディレクトリに移動
cd anntena-rss/
# コンポーネントを追加
yarn add react-router-dom
yarn add @material-ui/core @material-ui/icons

エラーが出たので以下の対応を行っています。

# 以下のエラーが出た場合はエラーメッセージに従ってnodeのバージョンを更新
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template-typescript...

yarn add v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
error babel-jest@27.5.1: The engine "node" is incompatible with this module. Expected version "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0". Got "14.2.0"
error Found incompatible module.
info Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command.

Aborting installation.
  yarnpkg add --exact react react-dom react-scripts cra-template-typescript --cwd /Users/yishigami/react/anntena-rss has failed.

Deleting generated file... package.json
Deleting generated file... yarn.lock
Deleting anntena-rss/ from /Users/yishigami/react
Done.

# ノード管理にnodebrewを入れている場合は以下のコマンドでバージョン確認、インストールを行う
# バージョン確認
nodebrew ls
# インストール(なるべく新しいものを入れておく)
nodebrew install v17.0.0
# バージョン切り替え
nodebrew use v17.0.0
# バージョンが切り替わっていることを確認
nodebrew ls
# yarnを使う場合は再インストール
npm install -g yarn

最初は以下のようなファイルが出来上がっていると思います。

# 初期イントール時srcフォルダ
.
├── App.css
├── App.test.tsx
├── App.tsx
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
└── setupTests.ts

とりあえず参考情報のサイトで紹介されているAtom設計のフォルダを作っておきます。

# srcフォルダ
.
├── App.css
├── App.test.tsx
├── App.tsx
├── components
│   ├── atoms
│   ├── molecules
│   ├── organisms
│   ├── pages
│   └── templates
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
└── setupTests.ts

2.Reactページ作成

参考情報のサイトにあるとおりReactのページを作っていきます。 Reactのバージョンによって微妙に異なるため注意してください。

src > components > pages > HomePage.tsx

import { makeStyles, Paper, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TablePagination, TableRow } from "@material-ui/core";
import TablePaginationActions from "@material-ui/core/TablePagination/TablePaginationActions";
import React from "react";
import GenericTemplate from "../templates/GenericTemplate";

const createData = (
  id: number,
  date: string,
  title: string,
  urlTitle: String,
  site: string,
  urlSite: String,
) => {
  return { date, title, site };
};
  
  const rows = [
    createData(1, "08/10 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(2, "08/11 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(3, "08/12 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(4, "08/13 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(5, "08/14 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(6, "08/15 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(7, "08/16 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(8, "08/17 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(9, "08/18 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(10, "08/19 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
    createData(11, "08/20 07:00", "ページのタイトル", "https://www.google.com/", "サイトの名前", "https://www.google.com/"),
  ];
  
  const useStyles = makeStyles({
    table: {
      // minWidth: 650,
    },
  });

const HomePage: React.FC = () => {
    const classes = useStyles();

    const [page, setPage] = React.useState(0);
    const [rowsPerPage, setRowsPerPage] = React.useState(10);
  
    // Avoid a layout jump when reaching the last page with empty rows.
    const emptyRows =
      page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;
  
    const handleChangePage = (
      event: React.MouseEvent<HTMLButtonElement> | null,
      newPage: number,
    ) => {
      setPage(newPage);
    };
  
    const handleChangeRowsPerPage = (
      event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
    ) => {
      setRowsPerPage(parseInt(event.target.value, 10));
      setPage(0);
    };


    return (
        <GenericTemplate title="トップページ">
          <TableContainer component={Paper}>
            <Table className={classes.table} aria-label="simple table">
              <TableHead>
                <TableRow>
                  <TableCell>日時</TableCell>
                  <TableCell>タイトル</TableCell>
                  <TableCell>サイト</TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {(
                  rowsPerPage > 0
                  ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
                  : rows
                ).map((row) => (
                  <TableRow key={row.date}>
                    <TableCell component="th" scope="row">
                      {row.date}
                    </TableCell>
                    <TableCell>
                      <a href="a">{row.title}</a>
                      </TableCell>
                    <TableCell>{row.site}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
              <TableFooter>
                <TableRow>
                  <TablePagination
                    labelRowsPerPage="表示件数:"
                    rowsPerPageOptions={[10, 30, 50, { label: 'All', value: -1 }]}
                    colSpan={3}
                    count={rows.length}
                    rowsPerPage={rowsPerPage}
                    page={page}
                    SelectProps={{
                      inputProps: {
                        'aria-label': 'rows per page',
                      },
                      native: true,
                    }}
                    onPageChange={handleChangePage}
                    onRowsPerPageChange={handleChangeRowsPerPage}
                    ActionsComponent={TablePaginationActions}
                  />
                </TableRow>
              </TableFooter>
            </Table>
          </TableContainer>
        </GenericTemplate>
    );
};

export default HomePage;

src > components > pages > AboutPage.tsx

import { Box, Typography } from "@material-ui/core";
import React from "react";
import GenericTemplate from "../templates/GenericTemplate";

const AboutPage: React.FC = () => {
    return (
        <GenericTemplate title="サイトについて">
            <Box component="main">
                <Typography variant="h3">サイトについて</Typography>
                    <Typography variant="body1">当サイトは2ちゃんねるまとめブログのアンテナサイトです</Typography>
                <Typography variant="h3">免責事項</Typography>
                    <Typography variant="body1">当サイトまたはリンク先のサイトを利用したことにより何らかの不都合や損害が発生したとしても当方は何らの責任を負うものではありません</Typography>
                    <Typography variant="body1">サイト内に転載されている画像等の著作権は各権利所有者に帰属します</Typography>
                    <Typography variant="body1">記事の削除依頼は掲載されているブログ様へ直接ご連絡ください</Typography>
                <Typography variant="h3">個人情報</Typography>
                    <Typography variant="body1">当サイトの入力フォームにて入力された個人情報は何らかの理由で連絡をとる必要が生じた場合にのみ使用し法令等に定められた以外に個人情報を事前の同意なく第三者に提供することはありません</Typography>
                <Typography variant="h3">プライバシーポリシー</Typography>
                    <Typography variant="h4">Cookieの利用について</Typography>
                        <Typography variant="body1">Cookieは当サイトや他サイトへのアクセスに関する情報が含まれており多くのサイトで利用者に有益な機能を提供する目的で使用されていますCookieにはサイト利用者の個人情報氏名住所メールアドレス電話番号は一切含まれません</Typography>
                        <Typography variant="body1">当サイトは第三者配信事業者がCookieを使用してサイト利用者が当サイトや他のサイトに過去にアクセスした際の情報に基づいて広告を配信します</Typography>
                        <Typography variant="body1">Googleが広告Cookieを使用することによりサイト利用者が当サイトや他のサイトにアクセスした際の情報に基づいてGoogleやそのパートナーは適切な広告をサイト利用者に対して表示します</Typography>
                        <Typography variant="body1">広告設定でパーソナライズ広告を無効にすることができます</Typography>
                    <Typography variant="h4">アクセス解析ツールについて</Typography>
                        <Typography variant="body1">当サイトではアクセス解析ツールGoogle Analyticsを利用しています。「Google Analyticsはトラフィックデータの収集のためにCookieを使用していますこのトラフィックデータは匿名で収集されており個人を特定するものではありません</Typography>
                        <Typography variant="body1">この機能はCookieを無効にすることで収集を拒否することができますのでご利用のブラウザ設定をご確認のうえ拒否設定を行ってください</Typography>

            </Box>
        </GenericTemplate>
    );
};

export default AboutPage;

src > App.tsx

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";

import AboutPage from "./components/pages/AboutPage";
import HomePage from "./components/pages/HomePage";

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path={`/`} element={<HomePage />} />
        <Route path={`/about/`} element={<AboutPage />} />
      </Routes>
    </BrowserRouter>
  );
};

export default App;

src > components > templates > GenericTemplate.tsx

createTheme内でサイト共通のCSSデザイン設定をするみたいです。 テーブルのpadding要素を変更したりしてます。

細かい調整はこれからやってこうと思います。

import React from "react";
import clsx from "clsx";
import { createTheme } from '@material-ui/core/styles'
// import { createMuiTheme } from "@material-ui/core/styles";
import * as colors from "@material-ui/core/colors";
import { makeStyles, createStyles, Theme } from "@material-ui/core/styles";
import { ThemeProvider } from "@material-ui/styles";
import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer";
import Box from "@material-ui/core/Box";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import List from "@material-ui/core/List";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import Container from "@material-ui/core/Container";
import { Link } from "react-router-dom";
import MenuIcon from "@material-ui/icons/Menu";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import IconButton from "@material-ui/core/IconButton";
import HomeIcon from "@material-ui/icons/Home";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import WebIcon from "@material-ui/icons/Web";

const drawerWidth = 240;

const theme = createTheme({
  typography: {
    fontFamily: [
      "Noto Sans JP",
      "Lato",
      "游ゴシック Medium",
      "游ゴシック体",
      "Yu Gothic Medium",
      "YuGothic",
      "ヒラギノ角ゴ ProN",
      "Hiragino Kaku Gothic ProN",
      "メイリオ",
      "Meiryo",
      "MS Pゴシック",
      "MS PGothic",
      "sans-serif",
    ].join(","),
  },
  palette: {
    primary: { main: colors.blue[800] }, // テーマの色
  },
  overrides: {
    MuiTableCell: {
      root: {
        padding: 0,
      }
    },
    MuiTablePagination: {
      toolbar: {
        minHeight: `30px`,
      }
    }
  },
});

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      display: "flex",
    },
    toolbar: {
      paddingRight: 24,
    },
    toolbarIcon: {
      display: "flex",
      alignItems: "center",
      justifyContent: "flex-end",
      padding: "0 8px",
      ...theme.mixins.toolbar,
    },
    appBar: {
      zIndex: theme.zIndex.drawer + 1,
      transition: theme.transitions.create(["width", "margin"], {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
    },
    appBarShift: {
      marginLeft: drawerWidth,
      width: `calc(100% - ${drawerWidth}px)`,
      transition: theme.transitions.create(["width", "margin"], {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.enteringScreen,
      }),
    },
    menuButton: {
      marginRight: 36,
    },
    menuButtonHidden: {
      display: "none",
    },
    title: {
      flexGrow: 1,
    },
    pageTitle: {
      marginBottom: theme.spacing(1),
    },
    drawerPaper: {
      position: "relative",
      whiteSpace: "nowrap",
      width: drawerWidth,
      transition: theme.transitions.create("width", {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.enteringScreen,
      }),
    },
    drawerPaperClose: {
      overflowX: "hidden",
      transition: theme.transitions.create("width", {
        easing: theme.transitions.easing.sharp,
        duration: theme.transitions.duration.leavingScreen,
      }),
      width: theme.spacing(7),
      [theme.breakpoints.up("sm")]: {
        width: theme.spacing(9),
      },
    },
    appBarSpacer: theme.mixins.toolbar,
    content: {
      flexGrow: 1,
      height: "100vh",
      overflow: "auto",
    },
    container: {
      paddingTop: theme.spacing(4),
      paddingBottom: theme.spacing(4),
    },
    paper: {
      padding: theme.spacing(2),
      display: "flex",
      overflow: "auto",
      flexDirection: "column",
    },
    link: {
      textDecoration: "none",
      color: theme.palette.text.secondary,
    },
  })
);

const Copyright = () => {
  return (
    <Typography variant="body2" color="textSecondary" align="center">
      {"Copyright © "}
      <Link color="inherit" to="/">
        イロイロまとめアンテナ
      </Link>{" "}
      {new Date().getFullYear()}
      {"."}
    </Typography>
  );
};

export interface GenericTemplateProps {
  children: React.ReactNode;
  title: string;
}

const GenericTemplate: React.FC<GenericTemplateProps> = ({
  children,
  title,
}) => {
  const classes = useStyles();
  const [open, setOpen] = React.useState(true);
  const handleDrawerOpen = () => {
    setOpen(true);
  };
  const handleDrawerClose = () => {
    setOpen(false);
  };

  return (
    <ThemeProvider theme={theme}>
      <div className={classes.root}>
        <CssBaseline />
        <AppBar
          position="absolute"
          className={clsx(classes.appBar, open && classes.appBarShift)}
        >
          <Toolbar className={classes.toolbar}>
            <IconButton
              edge="start"
              color="inherit"
              aria-label="open drawer"
              onClick={handleDrawerOpen}
              className={clsx(
                classes.menuButton,
                open && classes.menuButtonHidden
              )}
            >
              <MenuIcon />
            </IconButton>
            <Typography
              component="h1"
              variant="h6"
              color="inherit"
              noWrap
              className={classes.title}
            >
              イロイロまとめアンテナ
            </Typography>
          </Toolbar>
        </AppBar>
        <Drawer
          variant="permanent"
          classes={{
            paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
          }}
          open={open}
        >
          <div className={classes.toolbarIcon}>
            <IconButton onClick={handleDrawerClose}>
              <ChevronLeftIcon />
            </IconButton>
          </div>
          <Divider />
          <List>
            <Link to="/" className={classes.link}>
              <ListItem button>
                <ListItemIcon>
                  <HomeIcon />
                </ListItemIcon>
                <ListItemText primary="トップページ" />
              </ListItem>
            </Link>
            <Link to="/about" className={classes.link}>
              <ListItem button>
                <ListItemIcon>
                  <WebIcon />
                </ListItemIcon>
                <ListItemText primary="サイトについて" />
              </ListItem>
            </Link>
          </List>
        </Drawer>
        <main className={classes.content}>
          <div className={classes.appBarSpacer} />
          <Container maxWidth="lg" className={classes.container}>
            <Typography
              component="h2"
              variant="h5"
              color="inherit"
              noWrap
              className={classes.pageTitle}
            >
              {title}
            </Typography>
            {children}
            <Box pt={4}>
              <Copyright />
            </Box>
          </Container>
        </main>
      </div>
    </ThemeProvider>
  );
};

export default GenericTemplate;

とりあえず次はAWSでバックエンド処理を作って、最後に細かいデザイン調整をしていく予定です。

comments powered by Disqus