/__mocks__/styleMock.js"
116 | },
117 | "setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
118 | "collectCoverage": true,
119 | "collectCoverageFrom": [
120 | "src/**/*.js",
121 | "!src/**/*.stories.js"
122 | ],
123 | "testPathIgnorePatterns": [
124 | "/node_modules/"
125 | ]
126 | },
127 | "eslintConfig": {
128 | "extends": [
129 | "universe",
130 | "universe/native",
131 | "universe/web"
132 | ]
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-cssnext': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Redux Atomic Design
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CatchError from './Error';
3 | import Root from './router/Root';
4 |
5 | class App extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 | }
14 |
15 | export default App;
--------------------------------------------------------------------------------
/src/Error.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class CatchError extends Component {
4 | state = {
5 | hasError: false,
6 | error: null,
7 | };
8 | componentDidCatch(error, info) {
9 | this.setState({ hasError: true, error });
10 | console.log('hey', error, info);
11 | }
12 |
13 | render() {
14 | if (this.state.hasError) {
15 | return (
16 |
17 |
Something went wrong.
18 |
19 | );
20 | }
21 | return this.props.children;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/actions/NotificationListPageActions.js:
--------------------------------------------------------------------------------
1 | import request from 'axios';
2 | import * as types from '../types/NotificationListPageTypes.js';
3 |
4 | export function change() {
5 | // return {
6 | // type: types.CHANGE_SEARCH_WORD,
7 | // searchWord,
8 | // };
9 | }
10 |
11 | export function deleteNotification(id) {
12 | // return {
13 | // type: types.CHANGE_SEARCH_WORD,
14 | // searchWord,
15 | // };
16 | }
17 |
18 | /**
19 | * リクエストobject作成処理
20 | * @param {string} url APIのURL
21 | * @param {string} method REST区分(GET、POST、PUT、DELETE)
22 | * @param {object} data 送信データ
23 | */
24 | function makeRequest(url, method, data) {
25 | return request({
26 | url,
27 | method,
28 | data,
29 | });
30 | }
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 |
5 | render((
6 |
7 | ), document.getElementById('app'));
8 |
--------------------------------------------------------------------------------
/src/components/atoms/Anchor/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Anchor = ({ ...props }) => (
4 |
5 | );
6 | export default Anchor;
7 |
--------------------------------------------------------------------------------
/src/components/atoms/Anchor/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Anchor from './index.js';
3 | import { LinkTxt } from '../Txt/index.js';
4 |
5 | export default stories => stories
6 | .add('デフォルト', () => (
7 | アンカー
8 | ))
9 | .add('LinkTxtとの組み合わせ', () => (
10 | LinkTxtとの組み合わせ
11 | ));
12 |
--------------------------------------------------------------------------------
/src/components/atoms/Balloon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import HoverTipInteraction, { Tip, Marker } from '../HoverTipInteraction/index.js';
4 |
5 | const Balloon = ({ children, className, ...props }) => (
6 |
7 | { children }
8 | );
9 |
10 | export default Balloon;
11 |
12 | export const BalloonTip = ({ children, label, className, ...props }) => (
13 |
14 | { children }
15 | { label }
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/components/atoms/Balloon/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withNotes } from '@storybook/addon-notes';
3 | import Balloon, { BalloonTip } from './index.js';
4 | import { TrashCanIcon } from '../Icon/index.js';
5 | import { withStyle } from '../../utils/decorators.js';
6 |
7 | const note = withNotes('読みやすさを考慮すると 10 文字までが適当。11 文字以上を表示したい場合は別のデザインを検討すること。');
8 |
9 | export default stories => stories
10 | .add('2文字ラベル', () => 次へ )
11 | .add('4文字ラベル', () => 削除する )
12 | .add('10文字ラベル', note(() => 削除したかったらする ))
13 | .add('20文字ラベル', note(() => 削除したかったらするけど、どうしたいかな ))
14 | .add('30文字ラベル', note(() => 削除したかったらするけど、どうしたいかな。嫌なら、やめようか ))
15 | .add('30文字ラベル改行', () => 削除したかったらするけど、どうしたいかな。 嫌なら、やめようか )
16 | .add('絶対座標配置', () => 左上から 200px に配置 )
17 | .add('アイコンラベル', () => )
18 | .add('絵文字', () => ❌ )
19 | .add('バルーンチップ', () => withStyle({ marginTop: '50px' })(
20 | ここにバルーンチップ を表示
21 | ))
22 | .add('長文中のバルーンチップ', () => (
23 |
24 | 専門的なことを説明する文章の場合、文章中のある言葉が一般的に使われるものでない場合などに注釈を表示したいときがあります。たとえばバルーンチップ のようなUIを使うことでそれが可能です。
25 |
26 | ))
27 | .add('BalloonTip in a long sentence', () => (
28 |
29 | When it comes to terminology, you would like to add an note to that in order to describe the meaning. That is when BalloonTip comes to the resque. It only shows up when a user puts his or her mouse cursor on the terminology.
30 |
31 | ));
32 |
--------------------------------------------------------------------------------
/src/components/atoms/Balloon/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .balloon {
4 | background-color: var(--color-tip);
5 | border-radius: var(--radius);
6 | color: var(--color-text-outlined);
7 | display: inline-block;
8 | font-size: var(--font-size-s);
9 | padding: 0.4rem 0.5rem;
10 | position: relative;
11 | }
12 |
13 | .balloon::after {
14 | border-color: var(--color-tip) transparent transparent transparent;
15 | border-style: solid;
16 | border-width: 3px 3px 0 3px;
17 | bottom: 0;
18 | content: "";
19 | display: block;
20 | height: 0;
21 | left: 50%;
22 | position: absolute;
23 | transform: translate(-50%, 100%);
24 | width: 0;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 |
4 | function buttonFactory(type) {
5 | return ({ children, className, ...props }) => (
6 | { children }
7 | );
8 | }
9 |
10 | export const Button = buttonFactory('default');
11 | export const PrimaryButton = buttonFactory('primary');
12 | export const WarningButton = buttonFactory('warning');
13 |
14 | export default Button;
15 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button, { PrimaryButton, WarningButton } from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => デフォルト )
6 | .add('プライマリ', () => プライマリ )
7 | .add('警告', () => 警告 );
8 |
--------------------------------------------------------------------------------
/src/components/atoms/Button/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .button {
4 | border-radius: var(--radius);
5 | border-width: 0;
6 | display: inline-block;
7 | font-weight: var(--font-weight-bold);
8 | font-size: var(--font-size-m);
9 | line-height: 1;
10 | padding: .8rem;
11 | text-decoration: none;
12 | transition: opacity var(--hover-animation);
13 | }
14 |
15 | .button:hover {
16 | opacity: .7;
17 | }
18 |
19 | .default {
20 | background-color: inherit;
21 | border: var(--border);
22 | color: var(--color-info);
23 | }
24 |
25 | .primary {
26 | background-color: var(--color-primary);
27 | color: var(--color-text-outlined);
28 | }
29 |
30 | .warning {
31 | background-color: var(--color-warning);
32 | color: var(--color-text-outlined);
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/atoms/Card/index.js:
--------------------------------------------------------------------------------
1 | import React, { cloneElement } from 'react';
2 | import styles from './styles.css';
3 |
4 | const Card = ({ tag:Tag = 'section', className, ...props }) => (
5 |
6 | );
7 | export default Card;
8 |
9 | export const CardHeader = ({ className, ...props }) => (
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/atoms/Card/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card, { CardHeader } from './index.js';
3 | import { wrapWithStyle } from '../../utils/decorators.js';
4 | import { HeadingOutlined } from '../../atoms/Heading/index.js';
5 |
6 | const withBackground = wrapWithStyle({ padding: '50px', backgroundColor: 'gray', height: '100vh' });
7 |
8 | export default stories => stories
9 | .add('デフォルト', () => withBackground(
10 | カード
11 | ))
12 | .add('見出し付き', () => withBackground(
13 |
14 | 見出し
15 | カード
16 |
17 | ));
18 |
--------------------------------------------------------------------------------
/src/components/atoms/Card/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .card {
4 | background-color: var(--color-card);
5 | padding: calc(var(--space) * 3);
6 | }
7 |
8 | .header {
9 | background-color: var(--color-card-header);
10 | margin: calc(var(--space) * -3) calc(var(--space) * -3) calc(var(--space) * 3);
11 | padding: var(--space) calc(var(--space) * 3);
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/atoms/Heading/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { containPresenter } from '../../utils/HoC.js';
4 |
5 | export const HeadingPresenter = ({
6 | tag:Tag,
7 | visualLevel,
8 | className,
9 | ...props,
10 | }) => (
11 |
12 | );
13 |
14 | export const HeadingUnderlinedPresenter = ({
15 | tag:Tag,
16 | visualLevel,
17 | className,
18 | ...props,
19 | }) => (
20 |
21 | );
22 |
23 | export const HeadingOutlinedPresenter = ({
24 | tag:Tag,
25 | visualLevel,
26 | className,
27 | ...props,
28 | }) => (
29 |
30 | );
31 |
32 | export const HeadingContainer = ({
33 | presenter,
34 | level = 2,
35 | visualLevel,
36 | ...props,
37 | }) => {
38 | level = Math.max(1, Math.min(6, level));
39 | visualLevel = Math.max(1, Math.min(6, (typeof visualLevel !== 'undefined') ? visualLevel : level));
40 | const tag = `h${ level }`;
41 |
42 | return presenter({ tag, visualLevel, ...props });
43 | };
44 |
45 | const Heading = containPresenter(HeadingContainer, HeadingPresenter);
46 | export default Heading;
47 |
48 | export const HeadingUnderlined = containPresenter(HeadingContainer, HeadingUnderlinedPresenter);
49 | export const HeadingOutlined = containPresenter(HeadingContainer, HeadingOutlinedPresenter);
50 |
51 | Object.assign(Heading, { displayName: 'Heading' });
52 |
--------------------------------------------------------------------------------
/src/components/atoms/Heading/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Heading, { HeadingUnderlined, HeadingOutlined } from './index.js';
3 | import { withStyle } from '../../utils/decorators.js';
4 |
5 | const withDarkBg = withStyle({ backgroundColor: 'black' });
6 |
7 | export default stories => stories
8 | .add('デフォルト', () => 見出し )
9 | .add('レベル1', () => 見出しレベル1 )
10 | .add('レベル1、見た目2', () => 見出しレベル1、見た目2 )
11 | .add('下線付き', () => 下線付き )
12 | .add('下線付き、レベル1', () => 見出しレベル1 )
13 | .add('下線付き、レベル1、見た目2', () => 下線付き、見出しレベル1、見た目2 )
14 | .add('白抜き', () => withDarkBg(下線付き ))
15 | .add('白抜き、レベル1', () => withDarkBg(見出しレベル1 ))
16 | .add('白抜き、レベル1、見た目2', () => withDarkBg(下線付き、見出しレベル1、見た目2 ));
17 |
--------------------------------------------------------------------------------
/src/components/atoms/Heading/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HeadingContainer } from './index.js';
3 |
4 | describe('HeadingContainer', () => {
5 | const presenter = props => props;
6 |
7 | it('見た目レベルが指定されていないとき見出しレベルに合わせる', () => {
8 | const { visualLevel } = HeadingContainer({
9 | presenter,
10 | level: 1,
11 | });
12 | expect(visualLevel).toBe(1);
13 | });
14 |
15 | it('見た目レベルが指定されているときは見出しレベルに合わせない', () => {
16 | const { visualLevel } = HeadingContainer({
17 | presenter,
18 | level: 1,
19 | visualLevel: 2,
20 | });
21 | expect(visualLevel).toBe(2);
22 | });
23 |
24 | it('1 未満のレベルは 1 とする', () => {
25 | const { tag, visualLevel } = HeadingContainer({
26 | presenter,
27 | level: 0,
28 | visualLevel: 0,
29 | });
30 | expect(tag).toBe('h1');
31 | expect(visualLevel).toBe(1);
32 | });
33 |
34 | it('7 以上のレベルは 6 とする', () => {
35 | const { tag, visualLevel } = HeadingContainer({
36 | presenter,
37 | level: 7,
38 | visualLevel: 7,
39 | });
40 | expect(tag).toBe('h6');
41 | expect(visualLevel).toBe(6);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/components/atoms/Heading/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .h {
4 | font-weight: var(--font-weight-bold);
5 | line-height: 1.5;
6 | }
7 |
8 | .h1 { font-size: var(--font-size-xxxxl); }
9 | .h2 { font-size: var(--font-size-xxxl); }
10 | .h3 { font-size: var(--font-size-xxl); }
11 | .h4 { font-size: var(--font-size-xl); }
12 | .h5 { font-size: var(--font-size-l); }
13 | .h6 { font-size: var(--font-size-m); }
14 |
15 | .underlined {
16 | border-bottom: var(--border);
17 | padding-bottom: var(--space);
18 | }
19 |
20 | .outlined {
21 | color: var(--color-text-outlined);
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/atoms/HolyGrailLayout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { containPresenter } from '../../utils/HoC.js';
4 |
5 | const HolyGrailLayoutPresenter = ({ tag:Tag = 'div', parts, className, ...props }) => {
6 | const { top, bottom, main, left, right } = parts;
7 | return (
8 |
9 | { top }
10 |
11 | { main }
12 | { left }
13 | { right }
14 |
15 | { bottom }
16 |
17 | );
18 | };
19 |
20 | const HolyGrailLayoutContainer = ({ presenter, children, ...props }) => {
21 | const parts = mapParts(children);
22 | return presenter({ parts, ...props });
23 | };
24 |
25 | const partTypes = [
26 | 'HolyGrailTop',
27 | 'HolyGrailBottom',
28 | 'HolyGrailMain',
29 | 'HolyGrailLeft',
30 | 'HolyGrailRight',
31 | ];
32 |
33 | function mapParts(elems) {
34 | const parts = [];
35 | elems.map(elem => {
36 | const idx = partTypes.indexOf(elem.type.displayName);
37 | if (!~idx) return;
38 | parts[idx] = elem.props.children;
39 | });
40 | const [ top, bottom, main, left, right ] = parts;
41 | return { top, bottom, main, left, right };
42 | }
43 |
44 | const HolyGrailLayout = containPresenter(HolyGrailLayoutContainer, HolyGrailLayoutPresenter);
45 | export default HolyGrailLayout;
46 |
47 | export const HolyGrailTop = () => これはレンダリングされないもの
;
48 | export const HolyGrailBottom = () => これはレンダリングされないもの
;
49 | export const HolyGrailMain = () => これはレンダリングされないもの
;
50 | export const HolyGrailLeft = () => これはレンダリングされないもの
;
51 | export const HolyGrailRight = () => これはレンダリングされないもの
;
52 |
--------------------------------------------------------------------------------
/src/components/atoms/HolyGrailLayout/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HolyGrailLayout, {
3 | HolyGrailTop,
4 | HolyGrailBottom,
5 | HolyGrailMain,
6 | HolyGrailLeft,
7 | HolyGrailRight,
8 | } from './index.js';
9 |
10 | export default function (stories) {
11 | return stories
12 | .add(
13 | 'デフォルト',
14 | () => (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | main
24 |
25 |
26 | nav
27 |
28 |
29 |
30 |
31 |
32 | )
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/atoms/HolyGrailLayout/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .root {
4 | display: flex;
5 | min-height: 100vh;
6 | flex-direction: column;
7 | }
8 |
9 | .body {
10 | display: flex;
11 | flex-direction: column;
12 | }
13 |
14 | .body > :nth-child(2) {
15 | order: -1;
16 | }
17 |
18 | @media (--breakpoint-s) {
19 | .body {
20 | flex: 1;
21 | flex-direction: row;
22 | }
23 |
24 | .body > :nth-child(2),
25 | .body > :last-child {
26 | flex: 0 0 12em;
27 | }
28 |
29 | .body > :first-child {
30 | flex: 1;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/atoms/HoverTipInteraction/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styles from './styles.css';
3 | import { containPresenter } from '../../utils/HoC.js';
4 |
5 | const HoverTipInteractionPresenter = ({ children, className, ...props }) => (
6 |
7 | { children }
8 |
9 | );
10 |
11 | const HoverTipInteractionContainer = ({ presenter, children, ...props }) => {
12 | children = React.Children.map(children, child => {
13 | if (child.type.displayName === 'Tip') {
14 | const grandChild = React.Children.only(child.props.children);
15 | return React.cloneElement(grandChild, {
16 | className: [ styles.tip, grandChild.props.className ].join(' '),
17 | });
18 | } else if (child.type.displayName === 'Marker') {
19 | const grandChild = child.props.children;
20 | return React.cloneElement(grandChild, {
21 | className: [ styles.marker, grandChild.props.className ].join(' '),
22 | });
23 | }
24 | return child;
25 | });
26 | return presenter({ children, ...props });
27 | };
28 |
29 | const HoverTipInteraction = containPresenter(HoverTipInteractionContainer, HoverTipInteractionPresenter);
30 |
31 | export default HoverTipInteraction;
32 |
33 | export const Tip = () => これはレンダリングされないもの ;
34 | export const Marker = () => これはレンダリングされないもの ;
35 |
--------------------------------------------------------------------------------
/src/components/atoms/HoverTipInteraction/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HoverTipInteraction, { Tip, Marker } from './index.js';
3 | import { withStyle } from '../../utils/decorators.js';
4 |
5 | export default stories => stories
6 | .add('デフォルト', () => withStyle({ display: 'inline-block', margin: '50px' })(
7 |
8 | ホバーしてね
9 | チップだよ
10 |
11 | ))
12 | .add('マーカー', () => withStyle({ display: 'inline-block', margin: '50px' })(
13 |
14 | ホバーしてね
15 | チップだよ
16 |
17 | ));
18 |
--------------------------------------------------------------------------------
/src/components/atoms/HoverTipInteraction/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | @keyframes fade {
4 | from {
5 | opacity: 0;
6 | }
7 | to {
8 | opacity: 1;
9 | }
10 | }
11 |
12 | @keyframes marker {
13 | to {
14 | background-color: var(--color-selected);
15 | }
16 | }
17 |
18 | .root {
19 | position: relative;
20 | }
21 |
22 | .tip {
23 | display: none;
24 | left: 50%;
25 | position: absolute;
26 | top: 0;
27 | transform: translate(-50%, -100%) translateY(-12px);
28 | white-space: nowrap;
29 | }
30 |
31 | .root:hover > .tip {
32 | display: inline-block;
33 | animation: fade var(--fade-animation);
34 | }
35 |
36 | .root:hover > .marker {
37 | background-color: var(--color-selected);
38 | animation: marker var(--fade-animation) forwards;
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/atoms/Icon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { containPresenter } from '../../utils/HoC.js';
4 |
5 | export const IconSVGList = {
6 | TrashCan: 'trash-can',
7 | ChevronRight: 'chevron-right',
8 | Alarm: 'alarm',
9 | Bluetooth: 'bluetooth',
10 | Cached: 'cached',
11 | Call: 'call',
12 | Cast: 'cast',
13 | CheckCircle: 'check-circle',
14 | CloudDownload: 'cloud-download',
15 | Computer: 'computer',
16 | Email: 'email',
17 | Face: 'face',
18 | Favorite: 'favorite',
19 | GetApp: 'get-app',
20 | Headset: 'headset',
21 | Home: 'home',
22 | Image: 'image',
23 | Mic: 'mic',
24 | Notifications: 'notifications',
25 | PermIdentity: 'perm-identity',
26 | PhotoCamera: 'photo-camera',
27 | QuestionAnswer: 'question-answer',
28 | Repeat: 'repeat',
29 | Room: 'room',
30 | Search: 'search',
31 | Settings: 'settings',
32 | ShoppingCart: 'shopping-cart',
33 | ThumbUp: 'thumb-up',
34 | Visibility: 'visibility',
35 | VolumeUp: 'volume-up',
36 | ZoomIn: 'zoom-in',
37 | };
38 |
39 | export const IconPresenter = ({
40 | iconName,
41 | height = 20,
42 | width = 20,
43 | ...props,
44 | }) => (
45 |
50 |
51 |
52 | );
53 |
54 | export const IconContainer = ({
55 | presenter,
56 | onClick,
57 | className = '',
58 | ...props,
59 | }) => {
60 | if (onClick) className += ` ${ styles.clickable }`;
61 | return presenter({ onClick, className, ...props });
62 | };
63 |
64 | export const iconFactory = iconName => props => {
65 | const Icon = containPresenter(IconContainer, IconPresenter);
66 | return ;
67 | };
68 |
69 | export const TrashCanIcon = iconFactory(IconSVGList.TrashCan);
70 | export const ChevronRightIcon = iconFactory(IconSVGList.ChevronRight);
71 | export const AlarmIcon = iconFactory(IconSVGList.Alarm);
72 | export const BluetoothIcon = iconFactory(IconSVGList.Bluetooth);
73 | export const CachedIcon = iconFactory(IconSVGList.Cached);
74 | export const CallIcon = iconFactory(IconSVGList.Call);
75 | export const CastIcon = iconFactory(IconSVGList.Cast);
76 | export const CheckCircleIcon = iconFactory(IconSVGList.CheckCircle);
77 | export const CloudDownloadIcon = iconFactory(IconSVGList.CloudDownload);
78 | export const ComputerIcon = iconFactory(IconSVGList.Computer);
79 | export const EmailIcon = iconFactory(IconSVGList.Email);
80 | export const FaceIcon = iconFactory(IconSVGList.Face);
81 | export const FavoriteIcon = iconFactory(IconSVGList.Favorite);
82 | export const GetAppIcon = iconFactory(IconSVGList.GetApp);
83 | export const HeadsetIcon = iconFactory(IconSVGList.Headset);
84 | export const HomeIcon = iconFactory(IconSVGList.Home);
85 | export const ImageIcon = iconFactory(IconSVGList.Image);
86 | export const MicIcon = iconFactory(IconSVGList.Mic);
87 | export const NotificationsIcon = iconFactory(IconSVGList.Notifications);
88 | export const PermIdentityIcon = iconFactory(IconSVGList.PermIdentity);
89 | export const PhotoCameraIcon = iconFactory(IconSVGList.PhotoCamera);
90 | export const QuestionAnswerIcon = iconFactory(IconSVGList.QuestionAnswer);
91 | export const RepeatIcon = iconFactory(IconSVGList.Repeat);
92 | export const RoomIcon = iconFactory(IconSVGList.Room);
93 | export const SearchIcon = iconFactory(IconSVGList.Search);
94 | export const SettingsIcon = iconFactory(IconSVGList.Settings);
95 | export const ShoppingCartIcon = iconFactory(IconSVGList.ShoppingCart);
96 | export const ThumbUpIcon = iconFactory(IconSVGList.ThumbUp);
97 | export const VisibilityIcon = iconFactory(IconSVGList.Visibility);
98 | export const VolumeUpIcon = iconFactory(IconSVGList.VolumeUp);
99 | export const ZoomInIcon = iconFactory(IconSVGList.ZoomIn);
100 |
--------------------------------------------------------------------------------
/src/components/atoms/Icon/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions';
3 | import Icon, {
4 | IconSVGList,
5 | TrashCanIcon,
6 | ChevronRightIcon,
7 | AlarmIcon,
8 | BluetoothIcon,
9 | CachedIcon,
10 | CallIcon,
11 | CastIcon,
12 | CheckCircleIcon,
13 | CloudDownloadIcon,
14 | ComputerIcon,
15 | EmailIcon,
16 | FaceIcon,
17 | FavoriteIcon,
18 | GetAppIcon,
19 | HeadsetIcon,
20 | HomeIcon,
21 | ImageIcon,
22 | MicIcon,
23 | NotificationsIcon,
24 | PermIdentityIcon,
25 | PhotoCameraIcon,
26 | QuestionAnswerIcon,
27 | RepeatIcon,
28 | RoomIcon,
29 | SearchIcon,
30 | SettingsIcon,
31 | ShoppingCartIcon,
32 | ThumbUpIcon,
33 | VisibilityIcon,
34 | VolumeUpIcon,
35 | ZoomInIcon,
36 | } from './index.js';
37 |
38 | export default stories => stories
39 | .add('TrashCanIcon', () => )
40 | .add('ChevronRightIcon', () => )
41 | .add('SearchIcon', () => )
42 | .add('SettingsIcon', () => )
43 | .add('クリッカブル', () => )
44 | .add('一覧', () => (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | ));
79 |
--------------------------------------------------------------------------------
/src/components/atoms/Icon/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { TrashCanIcon } from './index.js';
4 |
5 | describe('TrashCanIcon', () => {
6 | it('クリックをコールバックする', () => {
7 | const onClick = jest.fn();
8 | const wrapper = shallow( );
9 | wrapper.simulate('click');
10 | expect(onClick.mock.calls.length).toBe(1);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/atoms/Icon/styles.css:
--------------------------------------------------------------------------------
1 | .clickable {
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/atoms/Img/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { containPresenter } from '../../utils/HoC.js';
3 |
4 | const ImgPresenter = ({ src, srcSet, webpSrcSet, alt, width, height, ...props }) => (
5 |
6 |
7 |
12 |
13 | );
14 |
15 | const riasRegexp = /images\/([0-9]+)\/([0-9]+)/;
16 |
17 | function createSrc(src, width, height) {
18 | if (!width || !height) return src;
19 |
20 | const ratio = window.devicePixelRatio || 1;
21 | const w = width * ratio;
22 | const h = height * ratio;
23 | return src.replace(riasRegexp, (match, p1, p2) => `images/${ w }/${ h }`);
24 | }
25 |
26 | function createSrcSet(src, width, height, extension) {
27 | if (extension) {
28 | src = src.replace(/\.[a-z0-9]+[^#\?]?/, `.${ extension }`);
29 | }
30 | if (
31 | !riasRegexp.test(src) ||
32 | !width ||
33 | !height
34 | ) return src;
35 |
36 | const [ path, rest ] = src.split('images/');
37 | const file = rest.match(".+/(.+?)([\?#;].*)?$")[1];
38 |
39 | return [ 1, 1.5, 2, 3, 4 ]
40 | .map(dpr => `${ path }images/${ width * dpr }/${ height * dpr }/${ file } ${ dpr }x`)
41 | .join(', ');
42 | }
43 |
44 | const ImgContainer = ({ presenter, src, width, height, ...props }) => {
45 | const srcSet = createSrcSet(src, width, height);
46 | const webpSrcSet = createSrcSet(src, width, height, 'webp');
47 | src = createSrc(src, width, height);
48 | return presenter({ src, srcSet, webpSrcSet, width, height, ...props });
49 | };
50 |
51 | const Img = containPresenter(ImgContainer, ImgPresenter);
52 | export default Img;
53 |
54 | Object.assign(Img, { displayName: 'Img' });
55 |
--------------------------------------------------------------------------------
/src/components/atoms/Img/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Img from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => )
7 | .add('適切なサイズ指定', () => )
8 | .add('20倍の画像', () => );
9 |
--------------------------------------------------------------------------------
/src/components/atoms/MediaObjectLayout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 |
4 | const MediaObjectLayout = ({ children, className, tag:Tag = 'div' }) => (
5 |
6 | { children[0] }
7 | { children.slice(1) }
8 |
9 | );
10 |
11 | export default MediaObjectLayout;
12 |
--------------------------------------------------------------------------------
/src/components/atoms/MediaObjectLayout/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MediaObjectLayout from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => (
6 |
7 |
8 |
9 |
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam ut dictum purus. Praesent id pulvinar sem, eu congue velit. Etiam porta luctus tellus, quis finibus quam condimentum eu. Cras vestibulum mauris non tempus congue.
11 | Sed pellentesque suscipit ex sed consequat. Fusce lobortis tincidunt euismod. Etiam sollicitudin molestie semper. Donec mi sem, molestie at molestie id, posuere ac lectus. Duis mollis, mauris venenatis sagittis porta, quam velit dictum diam, non aliquam nunc elit ut ex.
12 |
13 | ))
14 | .add('section 指定', () => (
15 |
16 |
17 |
18 |
19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam ut dictum purus. Praesent id pulvinar sem, eu congue velit. Etiam porta luctus tellus, quis finibus quam condimentum eu. Cras vestibulum mauris non tempus congue.
20 | Sed pellentesque suscipit ex sed consequat. Fusce lobortis tincidunt euismod. Etiam sollicitudin molestie semper. Donec mi sem, molestie at molestie id, posuere ac lectus. Duis mollis, mauris venenatis sagittis porta, quam velit dictum diam, non aliquam nunc elit ut ex.
21 |
22 | ));
23 |
--------------------------------------------------------------------------------
/src/components/atoms/MediaObjectLayout/styles.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | }
4 |
5 | .body {
6 | flex: 1;
7 | min-width: 0;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/atoms/StickyHeaderLayout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 |
4 | const StickyHeaderLayout = ({ tag:Tag = 'div', children, ...props }) => (
5 |
6 | { children[0] }
7 | { children[1] }
8 |
9 | );
10 |
11 | export default StickyHeaderLayout;
12 |
--------------------------------------------------------------------------------
/src/components/atoms/StickyHeaderLayout/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StickyHeaderLayout from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => (
6 |
7 | ヘッダー
8 | 本体
9 |
10 | ));
11 |
--------------------------------------------------------------------------------
/src/components/atoms/StickyHeaderLayout/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .sticky {
4 | position: fixed;
5 | width: 100%;
6 | z-index: var(--z-header);
7 | }
8 |
9 | .body {
10 | min-height: 100vh;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/atoms/TextBox/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 |
4 | const TextBox = ({ className, ...props }) => (
5 |
6 | );
7 |
8 | export default TextBox;
9 |
--------------------------------------------------------------------------------
/src/components/atoms/TextBox/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TextBox from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => );
6 |
--------------------------------------------------------------------------------
/src/components/atoms/TextBox/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .textbox {
4 | border: var(--border);
5 | border-radius: var(--radius);
6 | color: var(--color-text);
7 | font-size: var(--font-size-s);
8 | padding: var(--space);
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/atoms/Time/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 | import 'moment/locale/ja';
4 | import { containPresenter } from '../../utils/HoC.js';
5 |
6 | export const TimePresenter = props => ;
7 |
8 | export const TimeContainer = ({
9 | presenter,
10 | children:value,
11 | dateTime,
12 | format = 'MM月DD日(ddd)HH:mm',
13 | ...props,
14 | }) => {
15 | value = parseInt(value, 10);
16 |
17 | var children;
18 | if (!isValid(value)) {
19 | children = '有効な時間表現ではありません';
20 | } else {
21 | children = formatDatetime(value, format);
22 | }
23 |
24 | if (!dateTime) {
25 | dateTime = formatDatetime(value);
26 | }
27 |
28 | return presenter({ children, dateTime, ...props });
29 | };
30 |
31 | const Time = containPresenter(TimeContainer, TimePresenter);
32 | Object.assign(Time, { displayName: 'Time' });
33 |
34 | export default Time;
35 |
36 | moment.locale();
37 |
38 | function isValid(unixtime) {
39 | return moment(unixtime, 'x', true).isValid();
40 | }
41 |
42 | function formatDatetime(datetime, format = 'YYYY-MM-DDTHH:mm') {
43 | return moment(datetime).format(format);
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/atoms/Time/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Time from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => 1507032000000 )
6 | .add('HH:mm', () => 1507032000000 )
7 | .add('無効な時間表現', () => 無効な時間表現 );
8 |
--------------------------------------------------------------------------------
/src/components/atoms/Txt/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 |
4 | const txtFactory = role => ({ tag:Tag = 'p', size = 'm', className, ...props }) => (
5 |
6 | );
7 |
8 | const Txt = txtFactory('default');
9 | export default Txt;
10 |
11 | export const InfoTxt = txtFactory('info');
12 | export const WarningTxt = txtFactory('warning');
13 | export const LinkTxt = txtFactory('link');
14 |
15 | Object.assign(InfoTxt, { displayName: 'InfoTxt' });
16 |
--------------------------------------------------------------------------------
/src/components/atoms/Txt/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Txt, { InfoTxt, WarningTxt } from './index.js';
3 |
4 | export default stories => stories
5 | .add('テキスト - S', () => テキストを表示 )
6 | .add('テキスト - M', () => テキストを表示 )
7 | .add('テキスト - L', () => テキストを表示 )
8 | .add('情報テキスト - S', () => 情報テキストを表示 )
9 | .add('情報テキスト - M', () => 情報テキストを表示 )
10 | .add('情報テキスト - L', () => 情報テキストを表示 )
11 | .add('警告テキスト - S', () => 警告テキストを表示 )
12 | .add('警告テキスト - M', () => 警告テキストを表示 )
13 | .add('警告テキスト - L', () => 警告テキストを表示 );
14 |
--------------------------------------------------------------------------------
/src/components/atoms/Txt/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .default { color: var(--color-text); }
4 | .info { color: var(--color-info); }
5 | .warning { color: var(--color-warning); }
6 | .link { color: var(--color-link); }
7 | .s { font-size: var(--font-size-s); }
8 | .m { font-size: var(--font-size-m); }
9 | .l { font-size: var(--font-size-l); }
10 |
--------------------------------------------------------------------------------
/src/components/examples/LayoutThrashing/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import styles from './styles.css';
3 |
4 | export default class LayoutThrashing extends PureComponent {
5 | componentDidMount() {
6 | this.resizeItems();
7 | }
8 |
9 | componentDidUpdate() {
10 | this.resizeItems();
11 | }
12 |
13 | render() {
14 | const { items } = this.props;
15 | return (
16 |
17 | { items.map((item, idx) => (
18 | { item.value * 100 }%
19 | )) }
20 |
21 | );
22 | }
23 |
24 | resizeItems() {
25 | if (!this.refs.list) return;
26 | const { items } = this.props;
27 | const width = this.refs.list.offsetWidth;
28 | items.forEach((item, idx) => {
29 | const itemEl = this.refs[`item_${ idx }`];
30 | if (itemEl) {
31 | itemEl.style.width = `${ width * item.value }px`;
32 | }
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/examples/LayoutThrashing/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LayoutThrashing from './index.js';
3 | import withPerf from 'react-perf-container';
4 |
5 | const addItems = createManyItems();
6 |
7 | export default stories => stories
8 | .add('デフォルト', () => {
9 | return withPerf({
10 | props: {
11 | items: [],
12 | },
13 | actions: {
14 | 'アイテム追加': function (end) {
15 | const items = this.state.items.concat(addItems);
16 | this.setState({ items }, end);
17 | }
18 | }
19 | })(({ items }) => (
20 |
21 | ));
22 | })
23 |
24 | function createManyItems() {
25 | var items = [];
26 | const baseItems = [
27 | { value: 0.76 },
28 | { value: 0.25 },
29 | { value: 0.98 },
30 | { value: 0.5 },
31 | { value: 0.88 },
32 | { value: 0.12 },
33 | { value: 1 },
34 | { value: 0.35 },
35 | { value: 0.3 },
36 | { value: 0.48 },
37 | ];
38 | for (let i = 0; i < 100; i++) {
39 | items = items.concat(baseItems);
40 | }
41 | return items;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/examples/LayoutThrashing/styles.css:
--------------------------------------------------------------------------------
1 | .item {
2 | background-color: #777;
3 | color: white;
4 | display: block;
5 | font-size: 0.8rem;
6 | padding: 0.8rem;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/examples/Update/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PureComponent } from 'react';
2 | import { pure } from 'recompose';
3 |
4 | export class Update extends Component {
5 | render() {
6 | return { this.props.something }
;
7 | }
8 | }
9 |
10 | export class NeverUpdate extends Component {
11 | shouldComponentUpdate(nextProps, nextState) {
12 | return false;
13 | }
14 | render() {
15 | return { this.props.something }
;
16 | }
17 | }
18 |
19 | export class ShouldUpdate extends Component {
20 | shouldComponentUpdate(nextProps, nextState) {
21 | return nextProps.something !== this.props.something;
22 | }
23 | render() {
24 | return { this.props.something }
25 | }
26 | }
27 |
28 | export class PureUpdate extends PureComponent {
29 | render() {
30 | return { this.props.something }
;
31 | }
32 | }
33 |
34 | export const PureFunctionalUpdate = pure(
35 | props => { props.something }
36 | );
37 | Object.assign(PureFunctionalUpdate, { displayName: 'PureFunctionalUpdate' });
38 |
--------------------------------------------------------------------------------
/src/components/examples/Update/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Update, NeverUpdate, ShouldUpdate, PureUpdate, PureFunctionalUpdate } from './index.js';
3 | import withPerf from 'react-perf-container';
4 |
5 | const actions = {
6 | '意味がない更新': function (end) {
7 | const { something } = this.state;
8 | this.setState({ something }, end);
9 | },
10 | '意味がある更新': function (end) {
11 | const something = this.state.something ? '' : this.props.something;
12 | this.setState({ something }, end);
13 | },
14 | };
15 |
16 | export default stories => stories
17 | .add('毎回更新される例', () => {
18 | return withPerf({
19 | props: { something: '毎回更新される例' },
20 | actions,
21 | defaultPrintTypes: { printInclusive: true },
22 | })(({ something }) => );
23 | })
24 | .add('絶対更新されない例', () => {
25 | return withPerf({
26 | props: { something: '絶対更新されない例' },
27 | actions,
28 | defaultPrintTypes: { printInclusive: true },
29 | })(({ something }) => );
30 | })
31 | .add('shouldComponentUpdate() を使った例', () => {
32 | return withPerf({
33 | props: { something: 'shouldComponentUpdate() を使った例' },
34 | actions,
35 | defaultPrintTypes: { printInclusive: true },
36 | })(({ something }) => );
37 | })
38 | .add('PureComponent を使った例', () => {
39 | return withPerf({
40 | props: { something: 'PureComponent を使った例' },
41 | actions,
42 | defaultPrintTypes: { printInclusive: true },
43 | })(({ something }) => );
44 | })
45 | .add('recompose pure を使った例', () => {
46 | return withPerf({
47 | props: { something: 'recompose pure を使った例' },
48 | actions,
49 | defaultPrintTypes: { printInclusive: true },
50 | })(({ something }) => );
51 | });
52 |
--------------------------------------------------------------------------------
/src/components/molecules/Breadcrumb/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import Anchor from '../../atoms/Anchor/index.js';
4 | import { LinkTxt } from '../../atoms/Txt/index.js';
5 |
6 | const defaultItems = [];
7 |
8 | const Breadcrumb = ({ items = defaultItems }) => (
9 |
10 | { items.map((item, idx) => (
11 | {
12 | items.length - 1 > idx ? { item.label } : { item.label }
13 | }
14 | )) }
15 |
16 | );
17 | export default Breadcrumb;
18 |
--------------------------------------------------------------------------------
/src/components/molecules/Breadcrumb/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions'
3 | import Breadcrumb from './index.js';
4 |
5 | const items = [
6 | { label: 'トップ', url: '#' },
7 | { label: '通知番組', url: '#' },
8 | ];
9 |
10 | export default stories => stories
11 | .add('デフォルト', () => (
12 |
13 | ));
14 |
--------------------------------------------------------------------------------
/src/components/molecules/Breadcrumb/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .root {
4 | display: flex;
5 | }
6 |
7 | .item {
8 | font-size: var(--font-size-s);
9 | }
10 |
11 | .item + .item {
12 | margin-left: calc(var(--space) * 3);
13 | position: relative;
14 | }
15 |
16 | .item + .item::after {
17 | content: ">";
18 | position: absolute;
19 | top: 50%;
20 | left: -1em;
21 | transform: translateY(-50%);
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/molecules/Copyright/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { InfoTxt } from '../../atoms/Txt/index.js';
3 |
4 | const Copyright = ({ tag = 'p', children, ...props }) => (
5 | Copyright 息 { children } All Rights Reserved.
6 | );
7 | export default Copyright;
8 |
--------------------------------------------------------------------------------
/src/components/molecules/Copyright/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Copyright from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => (
6 | Yusuke Goto
7 | ));
8 |
--------------------------------------------------------------------------------
/src/components/molecules/DeleteButton/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { TrashCanIcon } from '../../atoms/Icon/index.js';
4 | import Balloon from '../../atoms/Balloon/index.js';
5 | import HoverTipInteraction, { Tip } from '../../atoms/HoverTipInteraction/index.js';
6 |
7 | const DeleteButton = ({ className, onClick, ...props }) => (
8 |
9 |
10 | 削除する
11 |
12 | );
13 |
14 | export default DeleteButton;
15 |
--------------------------------------------------------------------------------
/src/components/molecules/DeleteButton/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions';
3 | import DeleteButton from './index.js';
4 | import { withStyle } from '../../utils/decorators.js';
5 |
6 | export default stories => stories
7 | .add('デフォルト', () => withStyle({ margin: '50px' })(
8 |
9 | ));
10 |
--------------------------------------------------------------------------------
/src/components/molecules/DeleteButton/styles.css:
--------------------------------------------------------------------------------
1 | .root {
2 | display: inline-block;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/molecules/MailAuthForm/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { PrimaryButton } from '../../atoms/Button/index.js';
4 | import TextBox from '../../atoms/TextBox/index.js';
5 |
6 | const MailAuthForm = ({ onSubmit, ...props }) => (
7 |
14 | );
15 |
16 | export default MailAuthForm;
17 |
--------------------------------------------------------------------------------
/src/components/molecules/MailAuthForm/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions';
3 | import MailAuthForm from './index.js';
4 |
5 | export default stories => stories
6 | .add('デフォルト', () => (
7 |
8 | ));
9 |
--------------------------------------------------------------------------------
/src/components/molecules/MailAuthForm/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .controls {
4 | display: flex;
5 | align-items: center;
6 | }
7 |
8 | .textbox {
9 | flex: 1;
10 | margin-right: var(--space);
11 | }
12 |
13 | .label {
14 | font-size: var(--font-size-s);
15 | font-weight: var(--font-weight-bold);
16 | line-height: 1;
17 | padding-bottom: calc(var(--space) * 2);
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/molecules/Menu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { HeadingOutlined } from '../../atoms/Heading/index.js';
4 | import Txt from '../../atoms/Txt/index.js';
5 | import Card, { CardHeader } from '../../atoms/Card/index.js';
6 |
7 | const Menu = ({ tag = 'section', heading, children = [], ...props }) => (
8 |
9 |
10 | { heading }
11 |
12 |
13 | { children.map((item, idx) => (
14 |
15 | { item.label }
16 |
17 | )) }
18 |
19 |
20 | );
21 | export default Menu;
22 |
--------------------------------------------------------------------------------
/src/components/molecules/Menu/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Menu from './index.js';
3 | import { channels } from '../../../mock/data.js';
4 |
5 | export default stories => stories
6 | .add('デフォルト', () => { channels } );
7 |
--------------------------------------------------------------------------------
/src/components/molecules/Menu/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .list {
4 | margin-top: calc(var(--space) * -1);
5 | margin-left: calc(var(--space) * -3);
6 | margin-right: calc(var(--space) * -3);
7 | }
8 |
9 | .link {
10 | display: block;
11 | padding: var(--space) calc(var(--space) * 3);
12 | font-weight: 700;
13 | }
14 |
15 | .link:hover {
16 | background-color: var(--color-info-layer1);
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/molecules/Navigation/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import { withStyle } from '../../utils/decorators.js';
4 |
5 | const Navigation = ({ items, ...props }) => (
6 |
7 |
8 | { items.map((item, idx) => (
9 |
10 | { !item.current ?
11 | { item.label }
12 | :
13 | { item.label }
14 | }
15 |
16 | )) }
17 |
18 |
19 | );
20 | export default Navigation;
21 |
--------------------------------------------------------------------------------
/src/components/molecules/Navigation/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Navigation from './index.js';
3 | import { withStyle } from '../../utils/decorators.js';
4 |
5 | const items = [
6 | { label: 'ホーム', url: '#' },
7 | { label: '番組表', url: '#' },
8 | { label: '通知番組', url: '#', current: true },
9 | { label: 'お知らせ', url: '#' },
10 | { label: '設定', url: '#' },
11 | ];
12 |
13 | export default stories => stories
14 | .add('デフォルト', () => withStyle({ backgroundColor: 'black', padding: '1rem' })(
15 |
16 | ));
17 |
--------------------------------------------------------------------------------
/src/components/molecules/Navigation/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .list {
4 | display: flex;
5 | flex-direction: rows;
6 | font-size: .8rem;
7 | }
8 |
9 | .item + .item {
10 | padding: 0 0 0 2em;
11 | }
12 |
13 | .link {
14 | color: #fff;
15 | padding: calc(var(--space) * 0.5) 0;
16 | }
17 |
18 | .link:hover, .current {
19 | border-color: var(--color-primary);
20 | border-style: solid;
21 | border-width: 0 0 .1rem 0;
22 | color: #fff;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/organisms/ChannelList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Menu from '../../molecules/Menu/index.js';
3 |
4 | const ChannelList = ({ heading="チャンネル一覧", channels, ...props }) => (
5 | { channels }
6 | );
7 | export default ChannelList;
8 |
--------------------------------------------------------------------------------
/src/components/organisms/ChannelList/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ChannelList from './index.js';
3 | import { channels } from '../../../mock/data.js';
4 |
5 | export default stories => stories
6 | .add('デフォルト', () => );
7 |
--------------------------------------------------------------------------------
/src/components/organisms/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import Copyright from '../../molecules/Copyright/index.js';
4 |
5 | const Footer = ({ tag:Tag = 'footer', className, ...props }) => (
6 |
7 | Yusuke Goto
8 |
9 | );
10 | export default Footer;
11 |
--------------------------------------------------------------------------------
/src/components/organisms/Footer/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Footer from './index.js';
3 |
4 | export default stories => stories
5 | .add('デフォルト', () => );
6 |
--------------------------------------------------------------------------------
/src/components/organisms/Footer/styles.css:
--------------------------------------------------------------------------------
1 | .root {
2 | text-align: center;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/organisms/Header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import Navigation from '../../molecules/Navigation/index.js';
4 |
5 | const Header = ({ className, navigations, ...props }) => (
6 |
7 |
8 |
9 | );
10 | export default Header;
11 |
--------------------------------------------------------------------------------
/src/components/organisms/Header/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './index.js';
3 |
4 | const navigations = [
5 | { label: 'ホーム', url: '#' },
6 | { label: '番組表', url: '#' },
7 | { label: '通知番組', url: '#', current: true },
8 | { label: 'お知らせ', url: '#' },
9 | { label: '設定', url: '#' },
10 | ];
11 |
12 | export default stories => stories
13 | .add('デフォルト', () => (
14 |
15 | ));
16 |
--------------------------------------------------------------------------------
/src/components/organisms/Header/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .root {
4 | background-color: var(--color-header);
5 | color: var(--color-text-outlined);
6 | padding: calc(var(--space) * 2);
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/organisms/Notification/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styles from './styles.css';
3 | import Img from '../../atoms/Img/index.js';
4 | import Heading from '../../atoms/Heading/index.js';
5 | import { InfoTxt } from '../../atoms/Txt/index.js';
6 | import Time from '../../atoms/Time/index.js';
7 | import MediaObjectLayout from '../../atoms/MediaObjectLayout/index.js';
8 | import DeleteButton from '../../molecules/DeleteButton/index.js';
9 | import { containPresenter } from '../../utils/HoC.js';
10 |
11 | export class NotificationContainer extends Component {
12 | constructor() {
13 | super();
14 | this.onClickDelete = this.onClickDelete;
15 | }
16 |
17 | render() {
18 | const { presenter, onClickDelete:propsOnClickDelete, ...props } = this.props;
19 | const onClickDelete = propsOnClickDelete ? this.onClickDelete : null;
20 | const presenterProps = { onClickDelete, ...props };
21 | return presenter(presenterProps);
22 | }
23 |
24 | onClickDelete(...args) {
25 | const { onClickDelete, program } = this.props;
26 | onClickDelete(...args, program);
27 | }
28 | }
29 |
30 | export const NotificationPresenter = ({
31 | program,
32 | className,
33 | onClickDelete,
34 | ...props,
35 | }) => (
36 |
37 |
38 | { program.title }
39 | { program.channelName }
40 |
41 | { program.startAt } ~
42 | { program.endAt }
43 |
44 |
45 |
46 | );
47 |
48 | const Notification = containPresenter(NotificationContainer, NotificationPresenter);
49 |
50 | export default Notification;
51 |
--------------------------------------------------------------------------------
/src/components/organisms/Notification/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions'
3 | import withPerf from 'react-perf-container';
4 | import Notification from './index.js';
5 | import { notifications } from '../../../mock/data.js';
6 |
7 | const notification = notifications[0];
8 |
9 | export default stories => stories
10 | .add('デフォルト', () => (
11 |
12 | ))
13 | .add('性能確認:タイトル変更', () => {
14 | const actions = {
15 | 'タイトル変更': function (end) {
16 | this.setState({ program: { ...this.state.program, ...{ title: 'a' } } }, end);
17 | },
18 | };
19 | return withPerf({
20 | props: {
21 | program: notification,
22 | },
23 | actions: {
24 | 'タイトル変更': function (end) {
25 | this.setState({ program: { ...this.state.program, ...{ title: '【新】コンポーネント指向で UI を設計しよう!第1話' } } }, end);
26 | },
27 | }
28 | })(({ program }) => (
29 |
30 | ));
31 | });
32 |
--------------------------------------------------------------------------------
/src/components/organisms/Notification/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .root {
4 | padding: calc(var(--space) * 2);
5 | position: relative;
6 | }
7 |
8 | .media {
9 | padding-right: calc(var(--space) * 2);
10 | }
11 |
12 | .time {
13 | margin-top: var(--space);
14 | }
15 |
16 | .del {
17 | display: none !important;
18 | position: absolute !important;
19 | right: calc(var(--space) * 3) !important;
20 | top: 50% !important;
21 | transform: translateY(-50%) !important;
22 | }
23 |
24 | .root:hover .del {
25 | display: inline-block !important;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/organisms/NotificationList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import Notification from '../Notification/index.js';
4 |
5 | const NotificationList = ({
6 | programs,
7 | onClickDelete,
8 | ...props,
9 | }) => (
10 |
11 | { programs.map((program, idx) => (
12 |
18 | )) }
19 |
20 | );
21 | export default NotificationList;
22 |
--------------------------------------------------------------------------------
/src/components/organisms/NotificationList/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions'
3 | import NotificationList from './index.js';
4 |
5 | const notifications = [{
6 | id: 0,
7 | thumbnail: '/mock/images/192/108/img01.jpg',
8 | title: 'コンポーネント指向で UI を設計しよう!第1話',
9 | channelName: 'UI チャンネル',
10 | startAt: 1507032000000,
11 | endAt: 1507035600000,
12 | }, {
13 | id: 1,
14 | thumbnail: '/mock/images/192/108/img02.jpg',
15 | title: 'コンポーネント指向で UI を設計しよう!第2話',
16 | channelName: 'UI チャンネル',
17 | startAt: 1507035600000,
18 | endAt: 1507039200000,
19 | }, {
20 | id: 2,
21 | thumbnail: '/mock/images/192/108/img01.jpg',
22 | title: 'コンポーネント指向で UI を設計しよう!第1話',
23 | channelName: 'UI チャンネル',
24 | startAt: 1507032000000,
25 | endAt: 1507035600000,
26 | }, {
27 | id: 3,
28 | thumbnail: '/mock/images/192/108/img02.jpg',
29 | title: 'コンポーネント指向で UI を設計しよう!第2話',
30 | channelName: 'UI チャンネル',
31 | startAt: 1507035600000,
32 | endAt: 1507039200000,
33 | }];
34 |
35 | export default stories => stories
36 | .add('デフォルト', () => (
37 |
38 | ));
39 |
--------------------------------------------------------------------------------
/src/components/organisms/NotificationList/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .item:hover {
4 | background-color: var(--color-selected);
5 | }
6 |
7 | .item + .item {
8 | border-top: var(--border);
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/organisms/PageHeader/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import Breadcrumb from '../../molecules/Breadcrumb/index.js';
4 |
5 | const PageHeader = ({ navigations, className, ...props }) => (
6 |
7 |
8 |
9 | );
10 | export default PageHeader;
11 |
--------------------------------------------------------------------------------
/src/components/organisms/PageHeader/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PageHeader from './index.js';
3 |
4 | const navigations = [
5 | { label: 'トップ', url: '#' },
6 | { label: '通知番組', url: '#' },
7 | ];
8 |
9 | export default stories => stories
10 | .add('デフォルト', () => (
11 |
12 | ));
13 |
--------------------------------------------------------------------------------
/src/components/organisms/PageHeader/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .root {
4 | padding: calc(var(--space) * 2);
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/pages/NotificationListPage.js:
--------------------------------------------------------------------------------
1 | // TODO: あとで色々変更する
2 |
3 | import React, { Component } from 'react';
4 | import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import { bindActionCreators } from 'redux';
7 | import NotificationListTemplate from '../templates/NotificationListTemplate/index.js';
8 |
9 | /** Actions */
10 | import * as NotificationListPageAction from '../../actions/NotificationListPageActions';
11 |
12 |
13 | class NotificationListPage extends Component {
14 | constructor(props) {
15 | super(props);
16 | /** ActionBinds */
17 | this.handleOnChange = this.handleOnChange.bind(this);
18 | this.handleOnClickDeleteNotification = this.handleOnClickDeleteNotification.bind(this);
19 | }
20 |
21 | handleOnChange(e) {
22 | const { NotificationListPageActionBind } = this.props;
23 | // 検索ボックス内の値を引数に渡す
24 | NotificationListPageActionBind.change();
25 | }
26 |
27 | handleOnClickDeleteNotification(e, notification) {
28 | const { NotificationListPageActionBind } = this.props;
29 | NotificationListPageActionBind.deleteNotification(notification.id);
30 | }
31 |
32 | render() {
33 | const { notifications, navigations, breadcrumb } = this.props;
34 | return (
35 |
41 | );
42 | }
43 |
44 |
45 | }
46 | NotificationListPage.propTypes = {
47 | notifications: PropTypes.any,
48 | navigations: PropTypes.any,
49 | breadcrumb: PropTypes.any,
50 | };
51 |
52 | function mapStateToProps(state) {
53 | const {
54 | notifications,
55 | navigations,
56 | breadcrumb,
57 | } = state.NotificationListPageReducer;
58 | return {
59 | notifications,
60 | navigations,
61 | breadcrumb,
62 | };
63 | }
64 |
65 | function mapDispatchToProps(dispatch) {
66 | return {
67 | NotificationListPageActionBind: bindActionCreators(NotificationListPageAction, dispatch),
68 | };
69 | }
70 |
71 | export default connect(mapStateToProps, mapDispatchToProps)(NotificationListPage);
--------------------------------------------------------------------------------
/src/components/properties.css:
--------------------------------------------------------------------------------
1 | :root {
2 |
3 | /* ***** バリュー(値)層 ***** */
4 |
5 | /* 色:色相名とカラーコードの紐づけ */
6 | --color-white: #fff;
7 | --color-black: #000;
8 | --color-gray: #8c8c8c;
9 | --color-gray-dk: #1a1a1a; /* dk = dark */
10 | --color-gray-lt: #ddd; /* lt = light */
11 | --color-gray-p: #f6f6f6; /* p = pale */
12 | --color-green: #51c300;
13 | --color-red: #f0163a;
14 |
15 | /* ***** ブランディング層 ***** */
16 |
17 | /* 色:色の意味などをサービス全体で統一 */
18 | --color-base: var(--color-white);
19 | --color-link: var(--color-green);
20 | --color-link-visited: none;
21 | --color-link-hover: none;
22 | --color-link-active: none;
23 | --color-success: none;
24 | --color-danger: none;
25 | --color-warning: var(--color-red);
26 | --color-info: var(--color-gray);
27 | --color-primary: var(--color-green);
28 | --color-secondary: none;
29 | --color-accent: none;
30 | --color-selected: var(--color-gray-p);
31 |
32 | /* ***** メディア(媒体)層 ***** */
33 |
34 | /* 色:媒体固有で使用する色 */
35 | --color-text: var(--color-black);
36 | --color-text-outlined: var(--color-white);
37 | --color-tip: var(--color-gray-dk);
38 | --color-line: var(--color-gray-lt);
39 | --color-info-layer1: var(--color-gray-p);
40 | --color-info-layer2: var(--color-white);
41 | --color-header: var(--color-black);
42 | --color-card: var(--color-white);
43 | --color-card-header: var(--color-black);
44 |
45 | /* 文字:サイズ(媒体に最適化した文字サイズ) */
46 | --font-size-xxs: none;
47 | --font-size-xs: .6rem;
48 | --font-size-s: .8rem;
49 | --font-size-m: 1rem;
50 | --font-size-l: 1.2rem;
51 | --font-size-xl: 1.4rem;
52 | --font-size-xxl: 1.6rem;
53 | --font-size-xxxl: 1.8rem;
54 | --font-size-xxxxl: 2.0rem;
55 |
56 | /* 文字:太さ(媒体に最適化した文字の太さ) */
57 | --font-weight-default: 400;
58 | --font-weight-bold: 700;
59 |
60 | /* 余白(媒体に最適化した余白単位) */
61 | --space: .5rem;
62 |
63 | /* 枠に関する要素(媒体に最適化した枠デザイン要素) */
64 | --line-width: 1px;
65 | --line-style: solid;
66 | --radius: 2px;
67 | --border: var(--line-width) var(--line-style) var(--color-line);
68 |
69 | /* フィードバックの定義(媒体に最適化したフィードバック・デザイン要素) */
70 | --hover-feedback-opacity: .7;
71 |
72 | /* アニメーションの定義(媒体に最適化したアニメーション・デザイン要素) */
73 | --hover-animation-duration: .1s;
74 | --hover-animation-timing: ease-out;
75 | --hover-animation: var(--hover-animation-duration) var(--hover-animation-timing);
76 | --fade-animation-duration: .2s;
77 | --fade-animation-timing: linear;
78 | --fade-animation: var(--fade-animation-duration) var(--fade-animation-timing);
79 |
80 | /* Z 座標の管理(Web 媒体/CSS では Z 座標を管理する) */
81 | --z-header: 10;
82 |
83 | /* レスポンシブ・デザイン用のカスタム・メディア(Web ブラウザ用) */
84 | @custom-media --breakpoint-s (min-width: 768px);
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/storyshots.test.js:
--------------------------------------------------------------------------------
1 | import initStoryshots from '@storybook/addon-storyshots';
2 |
3 | initStoryshots();
4 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationList2Template/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import HolyGrailLayout, {
4 | HolyGrailTop,
5 | HolyGrailBottom,
6 | HolyGrailMain,
7 | HolyGrailLeft,
8 | HolyGrailRight,
9 | } from '../../atoms/HolyGrailLayout/index.js';
10 | import Card from '../../atoms/Card/index.js';
11 | import PageHeader from '../../organisms/PageHeader/index.js';
12 | import Header from '../../organisms/Header/index.js';
13 | import Footer from '../../organisms/Footer/index.js';
14 | import ChannelList from '../../organisms/ChannelList/index.js';
15 | import NotificationList from '../../organisms/NotificationList/index.js';
16 |
17 | const NotificationList2Template = ({
18 | notifications,
19 | navigations,
20 | breadcrumb,
21 | channels,
22 | onClickDeleteNotification
23 | }) => (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 | );
49 | export default NotificationList2Template;
50 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationList2Template/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions'
3 | import NotificationList2Template from './index.js';
4 | import {
5 | notifications,
6 | navigations,
7 | breadcrumb,
8 | channels,
9 | } from '../../../mock/data.js';
10 |
11 | export default stories => stories
12 | .add('デフォルト', () => {
13 | return (
14 |
21 | );
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationList2Template/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .root {
4 | background-color: var(--color-info-layer1);
5 | }
6 |
7 | .header {
8 | margin-bottom: calc(var(--space) * 4) !important;
9 | }
10 |
11 | .footer {
12 | padding-bottom: calc(var(--space) * 4) !important;
13 | padding-top: calc(var(--space) * 4) !important;
14 | }
15 |
16 | .main {
17 | padding: calc(var(--space) * 2) calc(var(--space) * 4) !important;
18 | margin-bottom: calc(var(--space) * 2) !important;
19 | }
20 |
21 | .nav {
22 | margin-bottom: calc(var(--space) * 2) !important;
23 | padding-left: calc(var(--space) * 2) !important;
24 | padding-right: calc(var(--space) * 2) !important;
25 | }
26 |
27 | @media (--breakpoint-s) {
28 | .main {
29 | margin-bottom: 0 !important;
30 | }
31 |
32 | .aside {
33 | padding-left: calc(var(--space) * 2) !important;
34 | padding-right: calc(var(--space) * 2) !important;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationListTemplate/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles.css';
3 | import StickyHeaderLayout from '../../atoms/StickyHeaderLayout/index.js';
4 | import PageHeader from '../../organisms/PageHeader/index.js';
5 | import Header from '../../organisms/Header/index.js';
6 | import NotificationList from '../../organisms/NotificationList/index.js';
7 |
8 | const NotificationListTemplate = ({ notifications, navigations, breadcrumb, onClickDeleteNotification }) => (
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 | );
21 |
22 | export default NotificationListTemplate;
23 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationListTemplate/index.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions'
3 | import NotificationListTemplate from './index.js';
4 | import {
5 | notifications,
6 | navigations,
7 | breadcrumb,
8 | } from '../../../mock/data.js';
9 |
10 | export default stories => stories
11 | .add('デフォルト', () => {
12 | return (
13 |
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationListTemplate/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { testA11y } from '../../utils/a11y.js';
3 | import NotificationListTemplate from './index.js';
4 | import {
5 | notifications,
6 | navigations,
7 | breadcrumb,
8 | } from '../../../mock/data.js';
9 |
10 | describe('NotificationListTemplate', () => {
11 | it('アクセシビリティに問題がない', () => {
12 | const config = {
13 | rules: {
14 | 'color-contrast': { enabled: false },
15 | },
16 | };
17 | return expect(
18 | testA11y(
19 | {} }
24 | />, config)
25 | .then(results => results.violations.length)
26 | ).resolves.toBe(0);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/templates/NotificationListTemplate/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../properties.css";
2 |
3 | .main {
4 | box-sizing: border-box;
5 | padding: calc(var(--space) * 8) calc(var(--space) * 2) var(--space);
6 | }
7 |
8 | .notifications {
9 | border: var(--border);
10 | border-width: 1px 0;
11 | margin-top: calc(var(--space) * 2);
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/utils/HoC.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function containPresenter(Container, Presenter) {
4 | return props => (
5 | } { ...props } />
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/utils/a11y.js:
--------------------------------------------------------------------------------
1 | import axe from 'axe-core';
2 | import { findDOMNode } from 'react-dom';
3 | import { mount } from 'enzyme';
4 |
5 | export function testA11y(component, config) {
6 | return new Promise((resolve, reject) => {
7 | const div = document.createElement('div');
8 | document.body.appendChild(div);
9 |
10 | const wrapper = mount(component, { attachTo: div });
11 | const node = findDOMNode(wrapper.component);
12 |
13 | const originalNode = global.Node;
14 | global.Node = node.ownerDocument.defaultView.Node;
15 |
16 | axe.run(node, config, (err, results) => {
17 | global.Node = originalNode;
18 | if (err) {
19 | reject(err);
20 | return;
21 | }
22 | resolve(results);
23 | });
24 |
25 | document.body.removeChild(div);
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/utils/decorators.js:
--------------------------------------------------------------------------------
1 | import React, { cloneElement } from 'react';
2 |
3 | export const withStyle = style => component => cloneElement(component, { style });
4 | export const wrapWithStyle = style => component => { component }
;
5 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | コンポーネント・ベースUIアプリケーション
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/mock/ActionCreator.js:
--------------------------------------------------------------------------------
1 | import http from './http.js';
2 |
3 | export default class ActionCreator {
4 | constructor(dispatcher) {
5 | this.dispatcher = dispatcher;
6 | }
7 |
8 | async fetch() {
9 | const data = await http.get('/api/data');
10 | this.dispatcher.emit('data', data);
11 | }
12 |
13 | async deleteNotification(id) {
14 | await http.delete('/api/data');
15 | this.dispatcher.emit('deleteNotification', id);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/mock/EventEmitter.js:
--------------------------------------------------------------------------------
1 | export default class EventEmitter {
2 | constructor() {
3 | this.handlers = {};
4 | }
5 |
6 | on(type, handler) {
7 | if (typeof this.handlers[type] === 'undefined') {
8 | this.handlers[type] = [];
9 | }
10 | this.handlers[type].push(handler);
11 | }
12 |
13 | off(type, handler) {
14 | const idx = this.handlers.findIndex(h => h === handler);
15 | this.handlers.splice(idx, 1);
16 | }
17 |
18 | emit(type, data) {
19 | (this.handlers[type] || [])
20 | .forEach(handler => handler.call(this, data));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/mock/Store.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from './EventEmitter.js';
2 |
3 | export default class Store extends EventEmitter {
4 | constructor(dispatcher) {
5 | super();
6 | this.notifications = [];
7 | this.navigations = [];
8 | this.breadcrumb = [];
9 | dispatcher.on('data', ::this.onChange);
10 | dispatcher.on('deleteNotification', ::this.onDeleteNotification);
11 | }
12 |
13 | get() {
14 | const { notifications, navigations, breadcrumb } = this;
15 | return {
16 | notifications,
17 | navigations,
18 | breadcrumb,
19 | };
20 | }
21 |
22 | onChange({ notifications, navigations, breadcrumb }) {
23 | notifications && (this.notifications = notifications);
24 | navigations && (this.navigations = navigations);
25 | breadcrumb && (this.breadcrumb = breadcrumb);
26 | this.emit('change', this.get());
27 | }
28 |
29 | onDeleteNotification(id) {
30 | const idx = this.notifications.findIndex(noti => noti.id === id);
31 | const notifications = [ ...this.notifications ];
32 | notifications.splice(idx, 1);
33 | this.notifications = notifications;
34 | this.emit('change', this.get());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/mock/data.js:
--------------------------------------------------------------------------------
1 | export const notifications = [{
2 | id: 0,
3 | thumbnail: '/mock/images/img01.jpg',
4 | title: 'コンポーネント指向で UI を設計しよう!第1話',
5 | channelName: 'UI チャンネル',
6 | startAt: 1507032000000,
7 | endAt: 1507035600000,
8 | }, {
9 | id: 1,
10 | thumbnail: '/mock/images/img02.jpg',
11 | title: 'コンポーネント指向で UI を設計しよう!第2話',
12 | channelName: 'UI チャンネル',
13 | startAt: 1507035600000,
14 | endAt: 1507039200000,
15 | }, {
16 | id: 2,
17 | thumbnail: '/mock/images/img01.jpg',
18 | title: 'コンポーネント指向で UI を設計しよう!第1話',
19 | channelName: 'UI チャンネル',
20 | startAt: 1507032000000,
21 | endAt: 1507035600000,
22 | }, {
23 | id: 3,
24 | thumbnail: '/mock/images/img02.jpg',
25 | title: 'コンポーネント指向で UI を設計しよう!第2話',
26 | channelName: 'UI チャンネル',
27 | startAt: 1507035600000,
28 | endAt: 1507039200000,
29 | }, {
30 | id: 4,
31 | thumbnail: '/mock/images/img01.jpg',
32 | title: 'コンポーネント指向で UI を設計しよう!第1話',
33 | channelName: 'UI チャンネル',
34 | startAt: 1507032000000,
35 | endAt: 1507035600000,
36 | }, {
37 | id: 5,
38 | thumbnail: '/mock/images/img02.jpg',
39 | title: 'コンポーネント指向で UI を設計しよう!第2話',
40 | channelName: 'UI チャンネル',
41 | startAt: 1507035600000,
42 | endAt: 1507039200000,
43 | }, {
44 | id: 6,
45 | thumbnail: '/mock/images/img01.jpg',
46 | title: 'コンポーネント指向で UI を設計しよう!第1話',
47 | channelName: 'UI チャンネル',
48 | startAt: 1507032000000,
49 | endAt: 1507035600000,
50 | }, {
51 | id: 7,
52 | thumbnail: '/mock/images/img02.jpg',
53 | title: 'コンポーネント指向で UI を設計しよう!第2話',
54 | channelName: 'UI チャンネル',
55 | startAt: 1507035600000,
56 | endAt: 1507039200000,
57 | }];
58 |
59 | export const navigations = [
60 | { label: 'ホーム', url: '#' },
61 | { label: '番組表', url: '#' },
62 | { label: '通知番組', url: '#', current: true },
63 | { label: 'お知らせ', url: '#' },
64 | { label: '設定', url: '#' },
65 | ];
66 |
67 | export const breadcrumb = [
68 | { label: 'トップ', url: '#' },
69 | { label: '通知番組', url: '#' },
70 | ];
71 |
72 | export const channels = [
73 | { label: 'ドラマ', url: '#' },
74 | { label: 'アニメ', url: '#' },
75 | { label: 'スポーツ', url: '#' },
76 | { label: '麻雀', url: '#' },
77 | { label: '釣り', url: '#' },
78 | { label: 'CM', url: '#' },
79 | ];
80 |
--------------------------------------------------------------------------------
/src/mock/http.js:
--------------------------------------------------------------------------------
1 | import {
2 | notifications,
3 | navigations,
4 | breadcrumb,
5 | } from './data.js';
6 |
7 | export default {
8 | async get(endPoint) {
9 | switch (endPoint) {
10 | case '/api/data':
11 | return {
12 | notifications,
13 | navigations,
14 | breadcrumb,
15 | };
16 | default:
17 | return {};
18 | }
19 | },
20 |
21 | async delete(endPoint) {
22 | switch (endPoint) {
23 | case '/api/data':
24 | return {};
25 | default:
26 | return {};
27 | }
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/reducers/NotificationListPage/NotificationListPageReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../../types/NotificationListPageTypes';
2 |
3 | // TODO: あとで色々と変更する
4 |
5 | const defaultState = {
6 | text: '行を選択',
7 | delete: '削除',
8 | deleteAria: '選択行を削除',
9 | };
10 |
11 | const NotificationListPageReducer = (state = defaultState, action) => {
12 | switch (action.type) {
13 | // 検索文字列変更時
14 | case types.CHANGE_SEARCH_WORD:
15 | return {
16 | ...state,
17 | searchWord: action.searchWord,
18 | };
19 | default:
20 | return state;
21 | }
22 | };
23 |
24 | export default NotificationListPageReducer;
25 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import NotificationListPageReducer from './NotificationListPage/NotificationListPageReducer';
3 |
4 | const rootReducer = combineReducers({
5 | NotificationListPageReducer,
6 | });
7 |
8 | export default rootReducer;
--------------------------------------------------------------------------------
/src/router/Root.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { BrowserRouter as Router, Route } from 'react-router-dom';
3 | import { Provider } from 'react-redux';
4 | import { hot } from 'react-hot-loader';
5 | import store from '../store';
6 | import { NotificationListPage } from '../components/pages/NotificationListPage.js';
7 |
8 | class Root extends Component {
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default hot(module)(Root);
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import promiseMiddleWare from 'redux-promise-middleware';
3 | import { composeWithDevTools } from 'redux-devtools-extension';
4 | import thunk from 'redux-thunk';
5 | import logger from 'redux-logger';
6 | import rootReducer from '../reducers';
7 |
8 | const middleWare = applyMiddleware(promiseMiddleWare(), thunk, logger);
9 |
10 | const store = createStore(rootReducer, composeWithDevTools(middleWare));
11 | //const store = createStore(composeWithDevTools(middleWare));
12 |
13 | export default store;
14 |
--------------------------------------------------------------------------------
/src/types/NotificationListPageTypes.js:
--------------------------------------------------------------------------------
1 | // TODO: あとで色々変更する
2 |
3 | export const CHANGE_SEARCH_WORD = 'CHANGE_SEARCH_WORD';
4 | export const REQUEST_PROCESS = 'REQUEST_PROCESS';
5 | export const SUCCESS_SEARCH = 'SUCCESS_SEARCH';
6 | export const FAILED_SEARCH = 'FAILED_SEARCH';
7 | export const CHANGE_ALERT_MESSAGE = 'CHANGE_ALERT_MESSAGE';
--------------------------------------------------------------------------------
/webpack.common.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
4 |
5 | module.exports = {
6 | entry: [path.join(__dirname, 'src/client.js')],
7 | optimization: {
8 | minimizer: [
9 | new UglifyJsPlugin({
10 | cache: true,
11 | parallel: true,
12 | sourceMap: true, // set true is you want JS source map
13 | uglifyOptions: {
14 | ecma: 6,
15 | compress: {
16 | drop_console: process.env.NODE_ENV === 'production',
17 | },
18 | },
19 | }),
20 | new OptimizeCSSAssetsPlugin(),
21 | ],
22 | splitChunks: {
23 | chunks: 'async',
24 | minSize: 30000,
25 | minChunks: 1,
26 | maxAsyncRequests: 5,
27 | maxInitialRequests: 3,
28 | automaticNameDelimiter: '~',
29 | name: true,
30 | cacheGroups: {
31 | vendors: {
32 | test: /[\\/]node_modules[\\/]/,
33 | priority: -10,
34 | },
35 | styles: {
36 | name: 'styles',
37 | test: /\.(scss|css)$/,
38 | chunks: 'all',
39 | minChunks: 1,
40 | reuseExistingChunk: true,
41 | enforce: true,
42 | },
43 | default: {
44 | minChunks: 2,
45 | priority: -20,
46 | reuseExistingChunk: true,
47 | },
48 | },
49 | },
50 | },
51 | resolve: {
52 | extensions: ['.js', '.json', '.css', '.less', '.scss', '.sass', '.jsx', '.png', '.jpg',],
53 | alias: {
54 | components: path.resolve(__dirname, 'src/components'),
55 | containers: path.resolve(__dirname, 'src/containers'),
56 | action: path.resolve(__dirname, 'src/action'),
57 | asset: path.resolve(__dirname, 'asset'),
58 | constants: path.resolve(__dirname, 'src/constants'),
59 | },
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const webpack = require('webpack');
4 | const webpackNotifier = require('webpack-notifier');
5 | const merge = require('webpack-merge');
6 | const common = require('./webpack.common.config.js');
7 |
8 | module.exports = merge(common, {
9 | cache: true, //for rebuilding faster
10 | output: {
11 | path: path.join(__dirname, 'build'),
12 | filename: 'static/js/bundle.js',
13 | publicPath: '/',
14 | pathinfo: true,
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.jsx?$/,
20 | exclude: /node_modules/,
21 | use: ['babel-loader'],
22 | },
23 | {
24 | test: /\.css$/,
25 | use: ['style-loader', 'css-loader', 'postcss-loader'],
26 | },
27 | {
28 | test: /\.scss$/,
29 | exclude: /node_modules/,
30 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
31 | },
32 | {
33 | test: /\.(eot|svg|ttf|woff|woff2)$/,
34 | use: ['url-loader'],
35 | },
36 | {
37 | test: /\.js$/,
38 | exclude: /node_modules/,
39 | use: {
40 | loader: 'babel-loader',
41 | options: {
42 | presets: ['es2015', 'react'],
43 | cacheDirectory: true,
44 | plugins: ['react-hot-loader/babel', 'react-html-attrs', 'transform-decorators-legacy', 'transform-class-properties'],
45 | },
46 | },
47 | },
48 | {
49 | test: /\.svg$/,
50 | use: ['svg-inline-loader'],
51 | },
52 | {
53 | test: /\.(png|jpg)$/,
54 | use: [
55 | {
56 | loader: 'url-loader',
57 | options: { name: 'image/[name]-[hash:8].[ext]' },
58 | },
59 | ],
60 | },
61 | ],
62 | },
63 | devtool: 'cheap-module-source-map',
64 | devServer: {
65 | historyApiFallback: true,
66 | contentBase: 'build',
67 | port: 8000,
68 | hot: true,
69 | watchOptions: {
70 | ignored: /node_modules/,
71 | },
72 | compress: true,
73 | },
74 | plugins: [
75 | new webpackNotifier(),
76 | new HtmlWebpackPlugin({
77 | inject: true,
78 | template: path.join(__dirname, 'public/index.html'),
79 | minify: {
80 | removeComments: true,
81 | collapseWhitespace: true,
82 | removeRedundantAttributes: true,
83 | useShortDoctype: true,
84 | removeEmptyAttributes: true,
85 | minifyJS: true,
86 | minifyCSS: true,
87 | },
88 | }),
89 | new webpack.DefinePlugin({
90 | 'process.env': {
91 | NODE_ENV: JSON.stringify('development'),
92 | PLATFORM_ENV: JSON.stringify('web'),
93 | SERVER_URL: JSON.stringify('http://localhost:3001/api'),
94 | },
95 | }),
96 | new webpack.optimize.OccurrenceOrderPlugin(),
97 | new webpack.HotModuleReplacementPlugin(),
98 | new webpack.NoEmitOnErrorsPlugin(),
99 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
100 | ],
101 | });
102 |
--------------------------------------------------------------------------------
/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
5 | const merge = require('webpack-merge');
6 | const HtmlWebpackPlugin = require('html-webpack-plugin');
7 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
8 | const autoprefixer = require('autoprefixer');
9 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
10 | const eslintFormatter = require('react-dev-utils/eslintFormatter');
11 | const common = require('./webpack.common.config.js');
12 |
13 | const prodEnv = {
14 | NODE_ENV: JSON.stringify('production'),
15 | PLATFORM_ENV: JSON.stringify('web'),
16 | SERVER_URL: JSON.stringify('https://server.com/api'),
17 | };
18 |
19 | const stagingEnv = {
20 | NODE_ENV: JSON.stringify('staging'),
21 | PLATFORM_ENV: JSON.stringify('web'),
22 | SERVER_URL: JSON.stringify('https://staging-server.com/api'),
23 | };
24 |
25 | const cssFilename = 'static/css/[name].[hash:8].css';
26 | const extractCSS = new ExtractTextPlugin('static/css/[name]-[hash:8].css');
27 | module.exports = merge(common, {
28 | cache: false,
29 | devServer: {
30 | historyApiFallback: true, //always true
31 | contentBase: '../dist',
32 | },
33 | output: {
34 | path: path.resolve(__dirname, 'build'),
35 | chunkFilename: 'static/js/[name]-[hash:8].bundle.js',
36 | filename: 'static/js/[name].[hash:8].js',
37 | publicPath: '/',
38 | },
39 | devtool: 'cheap-source-map',
40 | stats: {
41 | //need it
42 | entrypoints: false,
43 | children: false,
44 | },
45 | module: {
46 | strictExportPresence: true, //need this
47 | rules: [
48 | {
49 | test: /\.(js|jsx)$/,
50 | enforce: 'pre',
51 | use: [
52 | {
53 | options: {
54 | formatter: eslintFormatter,
55 | eslintPath: require.resolve('eslint'),
56 | },
57 | loader: require.resolve('eslint-loader'),
58 | },
59 | ],
60 | include: path.join(__dirname, 'src'),
61 | },
62 | {
63 | oneOf: [
64 | {
65 | test: /\.(png|svg|jpg|gif)$/,
66 | include: path.join(__dirname, 'src'),
67 | loader: 'file-loader',
68 | options: {
69 | limit: 10000,
70 | name: '[name].[hash:8].[ext]',
71 | publicPath: '/static/media',
72 | outputPath: 'static/media',
73 | },
74 | },
75 | {
76 | test: /\.(js|jsx)$/,
77 | include: path.join(__dirname, 'src'),
78 | loader: require.resolve('babel-loader'),
79 | exclude: /node_modules/,
80 | options: {
81 | plugins: ['react-html-attrs', 'transform-decorators-legacy', 'transform-class-properties'],
82 | compact: true,
83 | },
84 | },
85 | {
86 | test: /\.css$/,
87 | use: extractCSS.extract({
88 | fallback: 'style-loader',
89 | use: [{ loader: 'css-loader' }, { loader: 'postcss-loader' }],
90 | }),
91 | },
92 | {
93 | test: /\.(scss)$/,
94 | include: path.resolve(__dirname, 'src'),
95 | exclude: /node_modules/,
96 | loader: ExtractTextPlugin.extract(
97 | Object.assign({
98 | fallback: require.resolve('style-loader'),
99 | use: [
100 | {
101 | loader: require.resolve('css-loader'),
102 | options: {
103 | importLoaders: 1,
104 | minimize: true,
105 | sourceMap: true,
106 | publicPath: path.resolve(__dirname, 'build'),
107 | },
108 | },
109 | {
110 | loader: require.resolve('postcss-loader'),
111 | options: {
112 | ident: 'postcss',
113 | plugins: () => [
114 | require('postcss-flexbugs-fixes'),
115 | autoprefixer({
116 | browsers: [
117 | '>1%',
118 | 'last 4 versions',
119 | 'Firefox ESR',
120 | 'not ie < 9', // React doesn't support IE8 anyway
121 | ],
122 | flexbox: 'no-2009',
123 | }),
124 | ],
125 | },
126 | },
127 | {
128 | loader: require.resolve('sass-loader'),
129 | },
130 | ],
131 | publicPath: path.resolve(__dirname, 'build'),
132 | })
133 | ),
134 | },
135 | {
136 | loader: require.resolve('file-loader'),
137 | exclude: [/\.js$/, /\.html$/, /\.json$/],
138 | options: {
139 | name: 'static/media/[name].[hash:8].[ext]',
140 | },
141 | },
142 | ],
143 | },
144 | ],
145 | },
146 | plugins: [
147 | new CleanWebpackPlugin(path.join(__dirname, 'build')),
148 | // new FaviconsWebpackPlugin({
149 | // logo: path.join(__dirname, 'public/favicon.png'),
150 | // prefix: 'static/media/icon[hash:8]/',
151 | // icons: { favicons: true },
152 | // }),
153 | new HtmlWebpackPlugin({
154 | sinject: true,
155 | template: path.join(__dirname, 'public/index.html'),
156 | minify: {
157 | removeComments: true,
158 | collapseWhitespace: true,
159 | removeRedundantAttributes: true,
160 | useShortDoctype: true,
161 | removeEmptyAttributes: true,
162 | removeStyleLinkTypeAttributes: true,
163 | keepClosingSlash: true,
164 | minifyJS: true,
165 | minifyCSS: true,
166 | minifySCSS: true,
167 | minifyURLs: true,
168 | },
169 | }),
170 | extractCSS,
171 | new ExtractTextPlugin({
172 | filename: cssFilename,
173 | }),
174 | new webpack.DefinePlugin({
175 | 'process.env': process.env.NODE_ENV === 'production' ? prodEnv : stagingEnv,
176 | }),
177 | new webpack.optimize.AggressiveMergingPlugin(),
178 | new webpack.NoEmitOnErrorsPlugin(),
179 | ],
180 | });
181 |
--------------------------------------------------------------------------------