Skip to content

Commit 970a8a7

Browse files
committed
feat: 🎸 ログイン画面を実装
1 parent fef0f17 commit 970a8a7

File tree

9 files changed

+314
-85
lines changed

9 files changed

+314
-85
lines changed

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"react": "^16.13.0",
1414
"react-dom": "^16.13.0",
1515
"react-scripts": "3.4.0",
16-
"typescript": "~3.7.2"
16+
"typescript": "^4.0.2"
1717
},
1818
"scripts": {
1919
"start": "react-scripts start",
@@ -35,5 +35,8 @@
3535
"last 1 firefox version",
3636
"last 1 safari version"
3737
]
38+
},
39+
"devDependencies": {
40+
"prettier": "^2.1.0"
3841
}
39-
}
42+
}

src/App.css

+1-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ nav, aside {
129129
}
130130

131131
nav, aside, main {
132-
text-transform: uppercase;
133132
color: lightslategray;
134133
display: flex;
135134
align-items: center;
@@ -171,4 +170,4 @@ footer {
171170
nav, aside {
172171
margin: 0;
173172
}
174-
}
173+
}

src/App.tsx

+26-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
import React from 'react';
2-
import './App.css';
3-
import { Side } from './Side';
4-
import { Main } from './Main';
1+
import React from "react";
2+
import "./App.css";
3+
import { Side } from "./Side";
4+
import { Main } from "./Main";
5+
import { LoginProvider, useLoginContext } from "./contexts/login";
6+
import { StartChatPage } from "./pages/startChatPage";
57

68
export const App = () => {
79
return (
8-
<div className="container">
9-
<header></header>
10-
11-
<Side></Side>
12-
<Main></Main>
13-
</div>
10+
<LoginProvider>
11+
<Foo />
12+
</LoginProvider>
1413
);
15-
}
14+
};
15+
16+
const Foo = () => {
17+
const { loggedIn } = useLoginContext();
18+
if (loggedIn) {
19+
return (
20+
<div className="container">
21+
<header></header>
22+
23+
<Side></Side>
24+
<Main></Main>
25+
</div>
26+
);
27+
} else {
28+
return <StartChatPage />;
29+
}
30+
};

src/Chat/Chat.tsx

+45-45
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,59 @@
1-
import React, {useEffect, useState} from 'react';
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { Message, MessageAPI } from "../api/messageAPI";
3+
import { useLoginContext } from "../contexts/login";
24

3-
type Message = {
4-
messageId: number
5-
userName: string
6-
text: string
7-
}
8-
type Messages = ReadonlyArray<Message>
9-
10-
class FakeMessageAPI {
11-
private onMessageListener = (message: Message): void => {
12-
}
13-
14-
async playDummyMessages(): Promise<void> {
15-
await new Promise(resolve => setTimeout(resolve, 2000))
16-
this.onMessageListener({messageId: 4, userName: 'bob', text: 'なんだろ?'})
17-
await new Promise(resolve => setTimeout(resolve, 2000))
18-
this.onMessageListener({messageId: 5, userName: 'alice', text: 'ふ〜ん'})
19-
await new Promise(resolve => setTimeout(resolve, 2000))
20-
this.onMessageListener({messageId: 6, userName: 'alice', text: 'なに話す?'})
21-
}
22-
23-
onMessage(onMessageListener: (message: Message) => void) {
24-
this.onMessageListener = onMessageListener
25-
}
26-
}
27-
const messageAPI = new FakeMessageAPI()
28-
messageAPI.playDummyMessages()
29-
30-
export const Chat = () => {
31-
const [messages, setMessages] = useState<Messages>([
32-
{messageId: 1, userName: 'alice', text: 'こん〜'},
33-
{messageId: 2, userName: 'bob', text: 'ちわ〜'},
34-
{messageId: 3, userName: 'alice', text: '何はなしてた?'},
35-
]);
5+
export const Chat: React.FC = () => {
6+
const [message, setMessage] = useState("");
7+
const [messages, setMessages] = useState<Message[]>([]);
8+
const messageAPI = useRef<MessageAPI | null>();
9+
const authContext = useLoginContext();
3610

3711
useEffect(() => {
38-
messageAPI.onMessage(message => {
39-
console.log(message)
40-
setMessages(messages => messages.concat(message))
41-
})
42-
})
12+
messageAPI.current = new MessageAPI();
13+
messageAPI.current.onMessage("foo", (message) => {
14+
setMessages((messages) => messages.concat(message));
15+
});
16+
return () => {
17+
messageAPI.current!.close();
18+
};
19+
}, []);
20+
21+
const submit = async (e: React.FormEvent<HTMLFormElement>) => {
22+
e.preventDefault();
23+
setMessage("");
24+
await messageAPI.current!.postMessage({
25+
username: authContext.username!,
26+
text: message,
27+
channelId: "foo",
28+
});
29+
};
4330

4431
return (
4532
<>
4633
<div className="chatContainer">
4734
<div className="chatMessages">
48-
{messages.map(message => <div key={message.messageId}>{message.userName}: {message.text}</div>)}
35+
{messages.map((message) => (
36+
<div key={message.messageId}>
37+
{message.username}: {message.text}
38+
</div>
39+
))}
4940
</div>
5041
<div className="chatForm">
51-
<div className="chatFormContainer">
52-
<input className="chatFormInput" type="text" name="name"/>
53-
<input className="chatFormSubmitButton" type="button" value="Submit"/>
54-
</div>
42+
<form className="chatFormContainer" onSubmit={submit}>
43+
<input
44+
className="chatFormInput"
45+
type="text"
46+
value={message}
47+
onChange={(e) => setMessage(e.target.value)}
48+
/>
49+
<input
50+
className="chatFormSubmitButton"
51+
type="submit"
52+
value="Submit"
53+
/>
54+
</form>
5555
</div>
5656
</div>
5757
</>
5858
);
59-
}
59+
};

src/Side.tsx

+16-21
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
1-
import React from 'react';
2-
import './App.css';
1+
import React from "react";
2+
import "./App.css";
33

44
type Channel = {
5-
name: string
6-
}
5+
name: string;
6+
};
77

88
interface ChannelListProps {
9-
channels: Channel[],
9+
channels: Channel[];
1010
}
1111

1212
const ChannelList: React.FC<ChannelListProps> = (props) => {
13-
const channels = props.channels;
14-
const listItems = channels.map((c) => <li>{c.name}</li>)
15-
return (
16-
<ul>{listItems}</ul>
17-
);
18-
}
13+
const channels = props.channels;
14+
const channelList = channels.map((c) => <li key={c.name}>#{c.name}</li>);
15+
return <ul>{channelList}</ul>;
16+
};
1917

2018
export const Side = () => {
21-
const channels: Channel[] = [
22-
{name: "Room A"},
23-
{name: "Room B"},
24-
];
19+
const channels: Channel[] = [{ name: "general" }, { name: "random" }];
2520

26-
return (
27-
<nav>
28-
<ChannelList channels={channels}/>
29-
</nav>
30-
);
31-
}
21+
return (
22+
<nav>
23+
<ChannelList channels={channels} />
24+
</nav>
25+
);
26+
};

src/api/messageAPI.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export class MessageAPI {
2+
private nextMessageId = 1;
3+
private messages: Message[] = []
4+
private messageListeners = new Map<string, MessageListener>()
5+
6+
constructor() {
7+
console.log('MessageAPI created')
8+
}
9+
10+
onMessage(channel: string, listener: MessageListener): void {
11+
console.log('MessageAPI: MessageListener set')
12+
this.messageListeners.set(channel, listener)
13+
}
14+
15+
async postMessage({username, text, channelId}: NewMessage): Promise<Message> {
16+
await new Promise(resolve => setTimeout(resolve, 200))
17+
const message: Message = {
18+
messageId: this.generateNextMessageId(),
19+
username, text, channelId,
20+
}
21+
this.addMessage(message)
22+
return message
23+
}
24+
25+
async close(): Promise<void> {
26+
console.log('MessageAPI: close')
27+
}
28+
29+
private addMessage(message: Message): void {
30+
const messageListener = this.messageListeners.get(message.channelId)
31+
if (messageListener) {
32+
messageListener(message)
33+
}
34+
this.messages.push(message)
35+
}
36+
37+
private generateNextMessageId(): number {
38+
const nextMessageId = this.nextMessageId;
39+
this.nextMessageId++;
40+
return nextMessageId
41+
}
42+
}
43+
44+
export type Message = {
45+
readonly messageId: number
46+
readonly username: string
47+
readonly text: string
48+
readonly channelId: string
49+
}
50+
51+
export type NewMessage = {
52+
readonly username: string
53+
readonly text: string
54+
readonly channelId: string
55+
}
56+
57+
export type MessageListener = (message: Message) => void

src/contexts/login.tsx

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useEffect, useState } from "react";
2+
3+
/**
4+
* ログイン/ログアウトの状態の型定義
5+
*/
6+
type LoginState = {
7+
/**
8+
* ログイン状態のときtrue、ログアウト状態のときfalseになる。
9+
*/
10+
readonly loggedIn: boolean;
11+
/**
12+
* ユーザ名。ログイン状態のときstring、ログアウト状態のときundefinedになる。
13+
*/
14+
readonly username?: string;
15+
/**
16+
* 状態をログイン状態に変更する
17+
*/
18+
readonly setLogin: (username: string) => void;
19+
/**
20+
* 状態をログアウト状態に変更する
21+
*/
22+
readonly setLogout: () => void;
23+
};
24+
25+
/**
26+
* 初期状態
27+
*/
28+
const initialState: LoginState = {
29+
loggedIn: false,
30+
setLogin: () => {},
31+
setLogout: () => {},
32+
};
33+
34+
// コンテキストを作る
35+
const LoginContext = React.createContext(initialState);
36+
37+
// コンテキストのプロバイダー
38+
export const LoginProvider = ({
39+
children,
40+
}: {
41+
readonly children: React.ReactNode;
42+
}) => {
43+
const [loggedIn, setLoggedIn] = useState(initialState.loggedIn);
44+
const [username, setUsername] = useState(initialState.username);
45+
46+
useEffect(() => {
47+
loginAPI.fetchUsername().then((username) => {
48+
if (username) {
49+
setLoggedIn(true);
50+
setUsername(username);
51+
}
52+
});
53+
}, []);
54+
55+
const loginState: LoginState = {
56+
loggedIn,
57+
username,
58+
setLogin: async (username: string) => {
59+
await loginAPI.login(username);
60+
setLoggedIn(true);
61+
setUsername(username);
62+
},
63+
setLogout: async () => {
64+
await loginAPI.logout();
65+
setLoggedIn(false);
66+
setUsername(undefined);
67+
},
68+
};
69+
70+
return (
71+
<LoginContext.Provider value={loginState}>{children}</LoginContext.Provider>
72+
);
73+
};
74+
75+
export const useLoginContext = () => React.useContext(LoginContext);
76+
77+
/**
78+
* ログインAPI
79+
*
80+
* このサンプルアプリでは便宜的にセッションストレージを用います。
81+
* 実際の実装では、サーバの認証APIを呼び出す実装になると思います。
82+
*
83+
* セッションストレージはブラウザのタブが閉じられるまで値を保持するストレージです。
84+
* 似たものにローカルストレージがありますが、ローカルストレージはブラウザのタブを閉じてもデータが消えない点と、タブ間でデータが共有される点が異なります。
85+
* このサンプルアプリでは複数のタブを開いて、異なるユーザ名で同時にログインしたいので、ローカルストレージではなくセッションストレージを使います。
86+
*/
87+
class LoginAPI {
88+
async fetchUsername(): Promise<string | null> {
89+
return sessionStorage.getItem("username");
90+
}
91+
92+
async login(username: string): Promise<void> {
93+
sessionStorage.setItem("username", username);
94+
}
95+
96+
async logout(): Promise<void> {
97+
sessionStorage.removeItem("username");
98+
}
99+
}
100+
101+
const loginAPI = new LoginAPI();

0 commit comments

Comments
 (0)