|
1 |
| -# Step 04 - Everyday Journal |
| 1 | +# Step 04 - User Profile |
2 | 2 |
|
3 |
| -Now, let's build some features. We are going to build an app that let users store everyday journal. |
| 3 | +Let's build a simple user profile page with some user attributes. |
4 | 4 |
|
5 |
| -* [1. User Information](#1-user-information) |
6 |
| -* [2. Daily Album](#2-daily-album) |
7 |
| -* [3. Write Text](#3-write-text) |
8 |
| -* [4. Refresh Album](#4-refresh-album) |
9 |
| -* [5. Run App](#5-run-app) |
| 5 | +* [1. Create Profile Page](#1-create-profile-page) |
| 6 | +* [2. Load User Attributes](#2-load-user-attributes) |
| 7 | +* [3. Save User Attributes](#3-save-user-attributes) |
| 8 | +* [4. Manage States via Redux](#4-manage-states-via-redux) |
10 | 9 |
|
11 |
| -## 1. User Information |
| 10 | +## 1. Create Parofile Page |
12 | 11 |
|
13 |
| -First we need to make sure every user has his/her own space. Let's get user information first. |
| 12 | +Create `src/pages/Profile.jsx`, then add to `<Navigator>` |
14 | 13 |
|
15 |
| -Import `Auth` |
16 |
| -``` |
17 |
| -import { Auth } from 'aws-amplify'; |
18 |
| -``` |
| 14 | +Modify `src/components/Navigator.jsx` |
19 | 15 |
|
20 |
| -Get current user info |
21 | 16 | ```
|
22 |
| - componentDidMount() { |
23 |
| - Auth.currentUserInfo() |
24 |
| - .then(user => this.setState({ user: user })) // we need user.id |
25 |
| - .catch(err => console.log(err)); |
26 |
| - } |
27 |
| -``` |
28 |
| - |
29 |
| -## 2. Daily Album |
| 17 | +const ProfileItems = props => ( |
| 18 | + <React.Fragment> |
| 19 | + <Nav.ItemLink href="#/"> |
| 20 | + Home |
| 21 | + </Nav.ItemLink> |
| 22 | + <Nav.ItemLink href="#/profile" active> |
| 23 | + Profile |
| 24 | + </Nav.ItemLink> |
| 25 | + <Nav.ItemLink href="#/login"> |
| 26 | + Login |
| 27 | + <BSpan srOnly>(current}</BSpan> |
| 28 | + </Nav.ItemLink> |
| 29 | + </React.Fragment> |
| 30 | +) |
30 | 31 |
|
31 |
| -To keep it simple, we organize our journal base on datetime. One day one album. Journal contains image and text. |
32 | 32 |
|
33 |
| -This can be easily achieved with `S3Album` from `aws-amplify-react` |
34 |
| - |
35 |
| -Imports |
36 |
| -``` |
37 |
| -import { Container, Segment, Header } from 'semantic-ui-react'; |
38 |
| -import { S3Album } from 'aws-amplify-react'; |
| 33 | +... |
| 34 | + render() { |
| 35 | + ... |
| 36 | + <Route exact path="/profile" component={ProfileItems} /> |
| 37 | + ... |
| 38 | + } |
39 | 39 | ```
|
40 | 40 |
|
41 |
| -Today as string |
42 |
| -``` |
43 |
| -const today = () => { |
44 |
| - const dt = new Date(); |
45 |
| - return dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate(); |
46 |
| -} |
47 |
| -``` |
| 41 | +Add to `<Main>` |
48 | 42 |
|
49 |
| -Render `S3Album` with userId and date as path in `memberView` |
50 | 43 | ```
|
51 |
| - memberView() { |
52 |
| - const { user } = this.state; |
53 |
| - if (!user) { return null; } |
54 |
| -
|
55 |
| - const path = user.id + '/' + today() + '/'; |
56 |
| - return ( |
57 |
| - <Container> |
58 |
| - <Header as="h2" attached="top">{today()}</Header> |
59 |
| - <Segment attached> |
60 |
| - <S3Album path={path} picker /> |
61 |
| - </Segment> |
62 |
| - </Container> |
63 |
| - ) |
64 |
| - } |
| 44 | + <Route |
| 45 | + exact |
| 46 | + path="/profile" |
| 47 | + render={(props) => <Profile user={user} />} |
| 48 | + /> |
65 | 49 | ```
|
66 | 50 |
|
67 |
| -## 3. Write Text |
| 51 | +<img src="profile.png" width="480px" /> |
68 | 52 |
|
69 |
| -`S3Album` let us select photo, as well as text from device. However as a journal of course need to be able to write something. |
| 53 | +## 2. Load User Attributes |
70 | 54 |
|
71 |
| -Add an input form |
72 |
| -``` |
73 |
| - <Form> |
74 |
| - <Form.Input |
75 |
| - name="writingTitle" |
76 |
| - placeholder="Title" |
77 |
| - onChange={this.handleChange} |
78 |
| - /> |
79 |
| - <Form.TextArea |
80 |
| - name="writingContent" |
81 |
| - placeholder="Write something ..." |
82 |
| - onChange={this.handleChange} |
83 |
| - /> |
84 |
| - <Form.Button onClick={this.save}>Save</Form.Button> |
85 |
| - </Form> |
86 |
| -``` |
| 55 | +We call `Auth.userAttributes` to load attributes. Since we get `user` object from `<Main>` which could be from constructing `<Profile>` component or updating, so we treat both `componentDidMount` and `componentDidUpdate` |
87 | 56 |
|
88 |
| -Handle input |
89 |
| -``` |
90 |
| - handleChange = (e, { name, value }) => this.setState({ [name]: value }); |
91 |
| -
|
92 |
| - save() { |
93 |
| - const { path, writingTitle, writingContent } = this.state; |
94 |
| - const textKey = writingTitle? path + writingTitle.replace(/\s+/g, '_') : null; |
95 |
| - const textContent = JSON.stringify({ |
96 |
| - title: writingTitle, |
97 |
| - constent: writingContent |
98 |
| - }); |
99 |
| - this.setState({ textKey: textKey, textContent: textContent }); |
100 |
| - } |
101 | 57 | ```
|
| 58 | + componentDidMount() { |
| 59 | + if (this.props.user) { this.loadProfile() } |
| 60 | + } |
102 | 61 |
|
103 |
| -Use a hidden S3Text to save content |
104 |
| -``` |
105 |
| - const { user, path, textKey, textContent, ts } = this.state; |
106 |
| - if (!user) { return null; } |
107 |
| -
|
108 |
| - const key = textKey? textKey + '.json' : null; |
109 |
| -
|
110 |
| - render() { |
111 |
| - ... |
112 |
| -
|
113 |
| - <S3Text |
114 |
| - hidden |
115 |
| - contentType="application/json" |
116 |
| - textKey={key} |
117 |
| - body={textContent} |
118 |
| - onLoad={() => this.setState({ ts: new Date().getTime() })} |
119 |
| - /> |
120 |
| -
|
121 |
| - ... |
| 62 | + componentDidUpdate(prevProps) { |
| 63 | + if (!prevProps.user && this.props.user) { |
| 64 | + this.loadProfile(); |
122 | 65 | }
|
123 |
| -``` |
124 |
| - |
125 |
| -We save text content with title in json format. By default `S3Album` / `S3Text` will display raw json, not very reader friendly. We can add a `translateItem` property to `S3Album` |
126 |
| - |
127 |
| -``` |
128 |
| - <S3Album |
129 |
| - path={path} |
130 |
| - ts={ts} |
131 |
| - picker |
132 |
| - translateItem={this.translateItem} |
133 |
| - /> |
134 |
| -``` |
135 |
| - |
136 |
| -`translateItem` method |
137 |
| -``` |
138 |
| - translateItem(data) { |
139 |
| - if ((data.type === 'text') && data.textKey.endsWith('.json')) { |
140 |
| - if (!data.content) { return data.content; } |
141 |
| -
|
142 |
| - const content = JSON.parse(data.content); |
143 |
| - return ( |
144 |
| - <div> |
145 |
| - <h3>{content.title}</h3> |
146 |
| - <div>{content.content}</div> |
147 |
| - </div> |
148 |
| - ) |
149 |
| - } |
150 |
| - return data.content; |
| 66 | + } |
| 67 | +
|
| 68 | + loadAttributes() { |
| 69 | + const { user } = this.props; |
| 70 | + Auth.userAttributes(user) |
| 71 | + .then(data => this.loadSuccess(data)) |
| 72 | + .catch(err => this.handleError(err)); |
| 73 | + } |
| 74 | +
|
| 75 | + loadSuccess(data) { |
| 76 | + logger.info('loaded user attributes', data); |
| 77 | + const profile = this.translateAttributes(data); |
| 78 | + this.setState({ profile: profile }); |
| 79 | + } |
| 80 | +
|
| 81 | + handleError(error) { |
| 82 | + logger.info('load / save user attributes error', error); |
| 83 | + this.setState({ error: error.message || error }); |
| 84 | + } |
| 85 | +
|
| 86 | + translateAttributes(data) { |
| 87 | + const profile = {}; |
| 88 | + data |
| 89 | + .filter(attr => ['given_name', 'family_name'].includes(attr.Name)) |
| 90 | + .forEach(attr => profile[attr.Name] = attr.Value); |
| 91 | + return profile; |
| 92 | + } |
| 93 | +``` |
| 94 | + |
| 95 | +`render` the component |
| 96 | +``` |
| 97 | + render() { |
| 98 | + const { profile, error } = this.state; |
| 99 | +
|
| 100 | + return ( |
| 101 | + <Container display="flex" flex="column" alignItems="center"> |
| 102 | + <InputGroup mb="3" style={{ maxWidth: '24rem' }}> |
| 103 | + <InputGroup.PrependText>First name</InputGroup.PrependText> |
| 104 | + <Form.Input |
| 105 | + type="text" |
| 106 | + defaultValue={profile.given_name} |
| 107 | + onChange={event => this.handleInputChange('given_name', event.target.value)} |
| 108 | + /> |
| 109 | + </InputGroup> |
| 110 | + <InputGroup mb="3" style={{ maxWidth: '24rem' }}> |
| 111 | + <InputGroup.PrependText>Last name</InputGroup.PrependText> |
| 112 | + <Form.Input |
| 113 | + type="text" |
| 114 | + defaultValue={profile.family_name} |
| 115 | + onChange={event => this.handleInputChange('family_name', event.target.value)} |
| 116 | + /> |
| 117 | + </InputGroup> |
| 118 | + <Button primary px="5" onClick={this.saveProfile}>Save</Button> |
| 119 | + { error && <Alert warning>{error}</Alert> } |
| 120 | + </Container> |
| 121 | + ) |
| 122 | + } |
| 123 | +``` |
| 124 | + |
| 125 | +To keep simple we just cover 'given_name' and 'family_name', which are from 'standard attributes' from Cognito: |
| 126 | +[Configuring User Pool Attributes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html) |
| 127 | + |
| 128 | +## 3. Save User Attributes |
| 129 | + |
| 130 | +For saving, we call `Auth.updateUserAttributes` |
| 131 | + |
| 132 | +``` |
| 133 | + saveProfile() { |
| 134 | + const { user } = this.props; |
| 135 | + if (!user) { |
| 136 | + this.handleError('No user to save to'); |
| 137 | + return; |
151 | 138 | }
|
152 |
| -``` |
153 |
| - |
154 |
| -## 4. Refresh Album |
155 |
| - |
156 |
| -Notice on `S3Text.onLoad` we set a state `ts`. This is to tell `S3Album` to reload so new writing can be displayed in album. |
157 |
| - |
158 |
| -``` |
159 |
| - <S3Album path={path} ts={ts} picker /> |
160 |
| -``` |
161 |
| - |
162 |
| -## 5. Run App |
163 | 139 |
|
| 140 | + Auth.updateUserAttributes(user, this.state.profile) |
| 141 | + .then(data => this.saveSuccess(data)) |
| 142 | + .catch(err => this.handleError(err)); |
| 143 | + } |
164 | 144 | ```
|
165 |
| -npm start |
166 |
| -``` |
167 |
| - |
168 |
| -<img src="daily_journal.png" width="360px" /> |
169 | 145 |
|
170 |
| -[Step 05 - List of Journals](../step-05) |
| 146 | +[Step 05 - State Management via Redux](../step-05) |
0 commit comments