React+AWS環境を使ってアンテナサイト(RSS)を作成①
Reactでアンテナサイトを作成してみます。
いわゆるアンテナサイトを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でバックエンド処理を作って、最後に細かいデザイン調整をしていく予定です。
Share this post
Twitter
Facebook
Email