@stanlemon/app-template 0.2.8 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +17 -9
- package/app.test.js +50 -0
- package/jest.config.js +1 -1
- package/package.json +7 -6
- package/src/App.test.tsx +28 -39
- package/src/App.tsx +161 -56
- package/src/Login.tsx +7 -6
- package/src/Register.tsx +9 -5
- package/src/Session.tsx +13 -4
package/app.js
CHANGED
|
@@ -3,19 +3,16 @@ import {
|
|
|
3
3
|
asyncJsonHandler as handler,
|
|
4
4
|
SimpleUsersDao,
|
|
5
5
|
} from "@stanlemon/server-with-auth";
|
|
6
|
-
import { Low, JSONFile } from "lowdb";
|
|
7
|
-
|
|
8
|
-
const adapter = new JSONFile("./db.json");
|
|
9
6
|
|
|
7
|
+
const dao = new SimpleUsersDao();
|
|
10
8
|
const app = createAppServer({
|
|
11
9
|
webpack: "http://localhost:8080",
|
|
12
10
|
secure: ["/api/"],
|
|
13
|
-
...
|
|
11
|
+
...dao,
|
|
14
12
|
});
|
|
15
13
|
|
|
16
|
-
const db =
|
|
17
|
-
|
|
18
|
-
db.data.items ||= [];
|
|
14
|
+
export const db = dao.getDb();
|
|
15
|
+
db.read().then(() => (db.data.items = db.data.items || []));
|
|
19
16
|
|
|
20
17
|
app.get(
|
|
21
18
|
"/api/items",
|
|
@@ -24,9 +21,20 @@ app.get(
|
|
|
24
21
|
|
|
25
22
|
app.post(
|
|
26
23
|
"/api/items",
|
|
27
|
-
handler(async (item) => {
|
|
28
|
-
db.data.items.push(item);
|
|
24
|
+
handler(async ({ item }) => {
|
|
25
|
+
db.data.items.push({ item, id: dao.generateId() });
|
|
29
26
|
await db.write();
|
|
30
27
|
return db.data.items;
|
|
31
28
|
})
|
|
32
29
|
);
|
|
30
|
+
|
|
31
|
+
app.delete(
|
|
32
|
+
"/api/items/:id",
|
|
33
|
+
handler(async ({ id }) => {
|
|
34
|
+
db.data.items = db.data.items.filter((item) => id !== item.id);
|
|
35
|
+
await db.write();
|
|
36
|
+
return db.data.items;
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export default app;
|
package/app.test.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import request from "supertest";
|
|
5
|
+
import app, { db } from "./app.js";
|
|
6
|
+
|
|
7
|
+
describe("/app", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
// Prevent data from bleeding over after each test
|
|
10
|
+
db.data.items = [];
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("lists items", async () => {
|
|
14
|
+
const response = await request(app)
|
|
15
|
+
.get("/api/items")
|
|
16
|
+
.set("Accept", "application/json");
|
|
17
|
+
|
|
18
|
+
expect(response.headers["content-type"]).toMatch(/json/);
|
|
19
|
+
expect(response.status).toEqual(200);
|
|
20
|
+
expect(response.body).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("add item", async () => {
|
|
24
|
+
const response = await request(app)
|
|
25
|
+
.post("/api/items")
|
|
26
|
+
.set("Accept", "application/json")
|
|
27
|
+
.send({ item: "hello world" });
|
|
28
|
+
|
|
29
|
+
expect(response.headers["content-type"]).toMatch(/json/);
|
|
30
|
+
expect(response.status).toEqual(200);
|
|
31
|
+
expect(response.body).toMatchObject([{ item: "hello world" }]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("delete item", async () => {
|
|
35
|
+
const response1 = await request(app)
|
|
36
|
+
.post("/api/items")
|
|
37
|
+
.set("Accept", "application/json")
|
|
38
|
+
.send({ item: "hello world" });
|
|
39
|
+
|
|
40
|
+
const items = response1.body;
|
|
41
|
+
|
|
42
|
+
const response2 = await request(app)
|
|
43
|
+
.delete(`/api/items/${items[0].id}`)
|
|
44
|
+
.set("Accept", "application/json");
|
|
45
|
+
|
|
46
|
+
expect(response2.headers["content-type"]).toMatch(/json/);
|
|
47
|
+
expect(response2.status).toEqual(200);
|
|
48
|
+
expect(response2.body).toMatchObject([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/jest.config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "@stanlemon/webdev/jest.config.js";
|
|
1
|
+
export { default } from "@stanlemon/webdev/jest.config.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/app-template",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "A template for creating apps using the webdev package.",
|
|
5
5
|
"author": "Stan Lemon <stanlemon@users.noreply.github.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
"lint:format": "eslint --fix --ext js,jsx,ts,tsx ./src/"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@stanlemon/server-with-auth": "
|
|
21
|
+
"@stanlemon/server-with-auth": "*",
|
|
22
22
|
"@stanlemon/webdev": "*",
|
|
23
|
-
"react": "^18.
|
|
24
|
-
"react-dom": "^18.
|
|
23
|
+
"react": "^18.1.0",
|
|
24
|
+
"react-dom": "^18.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@testing-library/react": "^13.1.1",
|
|
28
28
|
"@testing-library/user-event": "^14.1.1",
|
|
29
|
-
"@types/react": "^18.0.
|
|
30
|
-
"@types/react-dom": "^18.0.
|
|
29
|
+
"@types/react": "^18.0.8",
|
|
30
|
+
"@types/react-dom": "^18.0.3",
|
|
31
|
+
"supertest": "^6.2.3"
|
|
31
32
|
}
|
|
32
33
|
}
|
package/src/App.test.tsx
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
render,
|
|
3
|
-
screen,
|
|
4
|
-
fireEvent,
|
|
5
|
-
waitFor,
|
|
6
|
-
act,
|
|
7
|
-
} from "@testing-library/react";
|
|
1
|
+
import { render, screen, fireEvent, act } from "@testing-library/react";
|
|
8
2
|
import userEvent from "@testing-library/user-event";
|
|
9
|
-
import App from "./App";
|
|
3
|
+
import App, { ItemData } from "./App";
|
|
10
4
|
import { SessionContext } from "./Session";
|
|
11
5
|
|
|
12
|
-
const output = [
|
|
6
|
+
const output = [
|
|
7
|
+
{ id: "1", item: "item one" },
|
|
8
|
+
{ id: "2", item: "item two" },
|
|
9
|
+
];
|
|
13
10
|
global.fetch = jest.fn((url, opts: { method: string; body: string }) => {
|
|
14
11
|
if (opts.method === "post") {
|
|
15
|
-
output.push(JSON.parse(opts.body) as
|
|
12
|
+
output.push(JSON.parse(opts.body) as ItemData);
|
|
16
13
|
}
|
|
17
14
|
return Promise.resolve({
|
|
18
15
|
ok: true,
|
|
@@ -21,42 +18,36 @@ global.fetch = jest.fn((url, opts: { method: string; body: string }) => {
|
|
|
21
18
|
}) as jest.Mock;
|
|
22
19
|
|
|
23
20
|
test("<App/>", async () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
email: "user@example.com",
|
|
35
|
-
},
|
|
21
|
+
render(
|
|
22
|
+
<SessionContext.Provider
|
|
23
|
+
value={{
|
|
24
|
+
session: {
|
|
25
|
+
token: "abcd",
|
|
26
|
+
user: {
|
|
27
|
+
username: "user",
|
|
28
|
+
password: "password",
|
|
29
|
+
name: "user",
|
|
30
|
+
email: "user@example.com",
|
|
36
31
|
},
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
},
|
|
33
|
+
setSession: () => {},
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<App />
|
|
37
|
+
</SessionContext.Provider>
|
|
38
|
+
);
|
|
44
39
|
|
|
45
40
|
// The auth text is present
|
|
46
|
-
expect(screen.getByText("You logged in as user")).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText("You are logged in as user.")).toBeInTheDocument();
|
|
47
42
|
|
|
48
43
|
// The header is present
|
|
49
44
|
expect(
|
|
50
45
|
screen.getByRole("heading", { name: "Hello World!" })
|
|
51
46
|
).toBeInTheDocument();
|
|
52
47
|
|
|
53
|
-
expect(
|
|
54
|
-
await screen.findByText("item one", { selector: "li" })
|
|
55
|
-
).toBeInTheDocument();
|
|
48
|
+
expect(await screen.findByText("item one")).toBeInTheDocument();
|
|
56
49
|
|
|
57
|
-
expect(
|
|
58
|
-
await screen.findByText("item two", { selector: "li" })
|
|
59
|
-
).toBeInTheDocument();
|
|
50
|
+
expect(await screen.findByText("item two")).toBeInTheDocument();
|
|
60
51
|
|
|
61
52
|
// Type some data into the input
|
|
62
53
|
await userEvent.type(screen.getByLabelText("Item"), "item three");
|
|
@@ -67,7 +58,5 @@ test("<App/>", async () => {
|
|
|
67
58
|
});
|
|
68
59
|
|
|
69
60
|
// Now we should have a list item with the text we entered
|
|
70
|
-
expect(
|
|
71
|
-
await screen.findByText("item three", { selector: "li" })
|
|
72
|
-
).toBeInTheDocument();
|
|
61
|
+
expect(await screen.findByText("item three")).toBeInTheDocument();
|
|
73
62
|
});
|
package/src/App.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useState, useContext, useEffect } from "react";
|
|
1
|
+
import React, { useState, useContext, useEffect } from "react";
|
|
2
|
+
|
|
2
3
|
import "./App.less";
|
|
3
4
|
import { SessionContext } from "./Session";
|
|
4
5
|
import Header from "./Header";
|
|
@@ -6,7 +7,7 @@ import Input from "./Input";
|
|
|
6
7
|
import Login from "./Login";
|
|
7
8
|
import Register from "./Register";
|
|
8
9
|
|
|
9
|
-
export type
|
|
10
|
+
export type ErrorResponse = {
|
|
10
11
|
message: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
@@ -14,40 +15,81 @@ export type FormErrors = {
|
|
|
14
15
|
errors: Record<string, string>;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
export type ItemData = {
|
|
19
|
+
id: string;
|
|
20
|
+
item: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line max-lines-per-function
|
|
17
24
|
export default function App() {
|
|
18
|
-
const [
|
|
19
|
-
const [items, setItems] = useState<
|
|
25
|
+
const [loaded, setLoaded] = useState<boolean>(false);
|
|
26
|
+
const [items, setItems] = useState<ItemData[]>([]);
|
|
27
|
+
const [error, setError] = useState<string | boolean>(false);
|
|
20
28
|
|
|
21
|
-
const { session } = useContext(SessionContext);
|
|
29
|
+
const { session, setSession } = useContext(SessionContext);
|
|
30
|
+
|
|
31
|
+
const catchError = (err: Error) => {
|
|
32
|
+
if (err.message === "Unauthorized") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setError(err.message);
|
|
36
|
+
};
|
|
22
37
|
|
|
23
|
-
const itemsJson = JSON.stringify(items);
|
|
24
38
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
if (loaded) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fetchApi<ItemData[], null>("/api/items", session?.token || "")
|
|
44
|
+
.then((items) => {
|
|
45
|
+
setLoaded(true);
|
|
27
46
|
setItems(items);
|
|
28
47
|
})
|
|
29
|
-
.catch(
|
|
30
|
-
}
|
|
48
|
+
.catch(catchError);
|
|
49
|
+
});
|
|
31
50
|
|
|
32
|
-
const
|
|
33
|
-
|
|
51
|
+
const saveItem = (item: string) => {
|
|
52
|
+
fetchApi<ItemData[], { item: string }>(
|
|
53
|
+
"/api/items",
|
|
54
|
+
session?.token || "",
|
|
55
|
+
"post",
|
|
56
|
+
{
|
|
57
|
+
item,
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
.then((items) => {
|
|
61
|
+
setItems(items);
|
|
62
|
+
})
|
|
63
|
+
.catch(catchError);
|
|
64
|
+
};
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
const deleteItem = (id: string) => {
|
|
67
|
+
fetchApi<ItemData[], string>(
|
|
68
|
+
`/api/items/${id}`,
|
|
69
|
+
session?.token || "",
|
|
70
|
+
"delete"
|
|
71
|
+
)
|
|
72
|
+
.then((items) => {
|
|
37
73
|
setItems(items);
|
|
38
74
|
})
|
|
39
|
-
.catch(
|
|
75
|
+
.catch(catchError);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const logout = () => {
|
|
79
|
+
setSession({ token: null, user: null });
|
|
40
80
|
};
|
|
41
81
|
|
|
42
82
|
return (
|
|
43
83
|
<>
|
|
44
84
|
<Header />
|
|
85
|
+
<ErrorMessage error={error} />
|
|
45
86
|
{!session.user && (
|
|
46
87
|
<Row>
|
|
47
88
|
<Column>
|
|
48
89
|
<h2>Login</h2>
|
|
49
90
|
<Login />
|
|
50
91
|
</Column>
|
|
92
|
+
<Column />
|
|
51
93
|
<Column>
|
|
52
94
|
<h2>Register</h2>
|
|
53
95
|
<Register />
|
|
@@ -57,63 +99,128 @@ export default function App() {
|
|
|
57
99
|
{session.user && (
|
|
58
100
|
<>
|
|
59
101
|
<p>
|
|
60
|
-
<em>You logged in as {session.user?.username}
|
|
102
|
+
<em>You are logged in as {session.user?.username}.</em>{" "}
|
|
103
|
+
<span style={{ cursor: "pointer" }} onClick={logout}>
|
|
104
|
+
(logout)
|
|
105
|
+
</span>
|
|
61
106
|
</p>
|
|
62
|
-
<
|
|
63
|
-
label="Item"
|
|
64
|
-
name="item"
|
|
65
|
-
value={value}
|
|
66
|
-
onChange={(value) => setValue(value)}
|
|
67
|
-
onEnter={addItem}
|
|
68
|
-
/>
|
|
69
|
-
<button onClick={addItem}>Add</button>
|
|
70
|
-
<ul>
|
|
71
|
-
{items.map((item, i) => (
|
|
72
|
-
<li key={i}>{item}</li>
|
|
73
|
-
))}
|
|
74
|
-
</ul>
|
|
107
|
+
<ItemList items={items} saveItem={saveItem} deleteItem={deleteItem} />
|
|
75
108
|
</>
|
|
76
109
|
)}
|
|
110
|
+
<Spacer />
|
|
77
111
|
</>
|
|
78
112
|
);
|
|
79
113
|
}
|
|
80
114
|
|
|
81
|
-
function
|
|
115
|
+
function ItemList({
|
|
116
|
+
items,
|
|
117
|
+
saveItem,
|
|
118
|
+
deleteItem,
|
|
119
|
+
}: {
|
|
120
|
+
items: ItemData[];
|
|
121
|
+
saveItem(item: string): void;
|
|
122
|
+
deleteItem(item: string): void;
|
|
123
|
+
}) {
|
|
124
|
+
const [value, setValue] = useState<string>("");
|
|
125
|
+
|
|
126
|
+
const addItem = () => {
|
|
127
|
+
saveItem(value);
|
|
128
|
+
setValue("");
|
|
129
|
+
};
|
|
130
|
+
|
|
82
131
|
return (
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
<>
|
|
133
|
+
<h2>New Item</h2>
|
|
134
|
+
<Input
|
|
135
|
+
label="Item"
|
|
136
|
+
name="item"
|
|
137
|
+
value={value}
|
|
138
|
+
onChange={(value) => setValue(value)}
|
|
139
|
+
onEnter={addItem}
|
|
140
|
+
/>
|
|
141
|
+
<button onClick={addItem}>Add</button>
|
|
142
|
+
<Spacer />
|
|
143
|
+
<h2>My Items</h2>
|
|
144
|
+
<ul style={{ padding: 0 }}>
|
|
145
|
+
{items.map(({ item, id }, i) => (
|
|
146
|
+
<Row key={i} as="li">
|
|
147
|
+
<button
|
|
148
|
+
style={{ marginLeft: "auto", order: 2 }}
|
|
149
|
+
onClick={() => deleteItem(id)}
|
|
150
|
+
>
|
|
151
|
+
Delete
|
|
152
|
+
</button>
|
|
153
|
+
<div>{item}</div>
|
|
154
|
+
</Row>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function Row({
|
|
162
|
+
as = "div",
|
|
163
|
+
children,
|
|
164
|
+
}: {
|
|
165
|
+
as?: keyof JSX.IntrinsicElements;
|
|
166
|
+
children?: React.ReactNode;
|
|
167
|
+
}) {
|
|
168
|
+
return React.createElement(
|
|
169
|
+
as,
|
|
170
|
+
{
|
|
171
|
+
style: {
|
|
85
172
|
display: "flex",
|
|
86
173
|
flexDirection: "row",
|
|
87
174
|
flexWrap: "wrap",
|
|
88
175
|
width: "100%",
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</div>
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
children
|
|
93
179
|
);
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
function
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
182
|
+
export function Spacer({ height = "2em" }: { height?: string | number }) {
|
|
183
|
+
return <div style={{ height, minHeight: height }} />;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function Column({
|
|
187
|
+
as = "div",
|
|
188
|
+
children,
|
|
189
|
+
}: {
|
|
190
|
+
as?: keyof JSX.IntrinsicElements;
|
|
191
|
+
children?: React.ReactNode;
|
|
192
|
+
}) {
|
|
193
|
+
return React.createElement(
|
|
194
|
+
as,
|
|
195
|
+
{
|
|
196
|
+
style: {
|
|
100
197
|
display: "flex",
|
|
101
198
|
flexDirection: "column",
|
|
102
199
|
flexBasis: "100%",
|
|
103
|
-
flex: 1,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
</div>
|
|
200
|
+
flex: "1 1 0",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
children
|
|
108
204
|
);
|
|
109
205
|
}
|
|
110
206
|
|
|
111
|
-
function
|
|
207
|
+
export function ErrorMessage({ error }: { error: string | boolean }) {
|
|
208
|
+
if (error) {
|
|
209
|
+
return (
|
|
210
|
+
<p>
|
|
211
|
+
<strong>An error has occurred:</strong> {error}
|
|
212
|
+
</p>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return <></>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fetchApi<T, P>(
|
|
112
219
|
url: string,
|
|
113
220
|
token: string,
|
|
114
221
|
method = "get",
|
|
115
|
-
data?:
|
|
116
|
-
): Promise<
|
|
222
|
+
data?: P
|
|
223
|
+
): Promise<T> {
|
|
117
224
|
return fetch(url, {
|
|
118
225
|
method: method,
|
|
119
226
|
headers: {
|
|
@@ -122,12 +229,10 @@ function fetchApi(
|
|
|
122
229
|
"Content-Type": "application/json",
|
|
123
230
|
},
|
|
124
231
|
body: JSON.stringify(data),
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
132
|
-
.then((response) => response.json());
|
|
232
|
+
}).then((response) => {
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
throw new Error(response.statusText);
|
|
235
|
+
}
|
|
236
|
+
return response.json() as Promise<T>;
|
|
237
|
+
});
|
|
133
238
|
}
|
package/src/Login.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useContext } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ErrorResponse, Spacer } from "./App";
|
|
3
3
|
import { SessionContext, SessionData, UserData } from "./Session";
|
|
4
4
|
import Input from "./Input";
|
|
5
5
|
|
|
@@ -44,17 +44,17 @@ export default function Login() {
|
|
|
44
44
|
if (ok) {
|
|
45
45
|
setSession(data as SessionData);
|
|
46
46
|
} else {
|
|
47
|
-
setError((data as
|
|
47
|
+
setError((data as ErrorResponse).message);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
)
|
|
51
|
-
.catch((err) => {
|
|
52
|
-
|
|
51
|
+
.catch((err: Error) => {
|
|
52
|
+
setError(err.message);
|
|
53
53
|
});
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
|
-
|
|
57
|
+
<>
|
|
58
58
|
{error && (
|
|
59
59
|
<div>
|
|
60
60
|
<strong>{error}</strong>
|
|
@@ -75,7 +75,8 @@ export default function Login() {
|
|
|
75
75
|
onChange={(value) => setValues({ ...values, password: value })}
|
|
76
76
|
onEnter={onSubmit}
|
|
77
77
|
/>
|
|
78
|
+
<Spacer />
|
|
78
79
|
<button onClick={onSubmit}>Login</button>
|
|
79
|
-
|
|
80
|
+
</>
|
|
80
81
|
);
|
|
81
82
|
}
|
package/src/Register.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useState, useContext } from "react";
|
|
2
|
-
import { FormErrors } from "./App";
|
|
2
|
+
import { FormErrors, ErrorMessage, Spacer } from "./App";
|
|
3
3
|
import { SessionData, UserData, SessionContext } from "./Session";
|
|
4
4
|
import Input from "./Input";
|
|
5
5
|
|
|
6
6
|
// eslint-disable-next-line max-lines-per-function
|
|
7
7
|
export default function Register() {
|
|
8
|
+
const [error, setError] = useState<string | boolean>(false);
|
|
8
9
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
9
10
|
const [values, setValues] = useState<UserData>({
|
|
10
11
|
name: "",
|
|
@@ -49,13 +50,15 @@ export default function Register() {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
)
|
|
52
|
-
.catch((err) => {
|
|
53
|
-
|
|
53
|
+
.catch((err: Error) => {
|
|
54
|
+
// Put any generis error onto the username field
|
|
55
|
+
setError(err.message);
|
|
54
56
|
});
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
return (
|
|
58
|
-
|
|
60
|
+
<>
|
|
61
|
+
<ErrorMessage error={error} />
|
|
59
62
|
<Input
|
|
60
63
|
name="username"
|
|
61
64
|
label="Username"
|
|
@@ -72,7 +75,8 @@ export default function Register() {
|
|
|
72
75
|
onChange={(value) => setValues({ ...values, password: value })}
|
|
73
76
|
error={errors.password}
|
|
74
77
|
/>
|
|
78
|
+
<Spacer />
|
|
75
79
|
<button onClick={onSubmit}>Register</button>
|
|
76
|
-
|
|
80
|
+
</>
|
|
77
81
|
);
|
|
78
82
|
}
|
package/src/Session.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, createContext } from "react";
|
|
2
|
+
import { ErrorMessage } from "./App";
|
|
2
3
|
|
|
3
4
|
export const SessionContext = createContext<{
|
|
4
5
|
session: SessionData;
|
|
@@ -23,8 +24,9 @@ export type UserData = {
|
|
|
23
24
|
password: string;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
export default function Session({ children }: { children: React.
|
|
27
|
+
export default function Session({ children }: { children: React.ReactNode }) {
|
|
27
28
|
const [initialized, setInitialized] = useState<boolean>(false);
|
|
29
|
+
const [error, setError] = useState<string | boolean>(false);
|
|
28
30
|
const [session, setSession] = useState<SessionData>({
|
|
29
31
|
token: null,
|
|
30
32
|
user: null,
|
|
@@ -42,7 +44,7 @@ export default function Session({ children }: { children: React.ReactChild }) {
|
|
|
42
44
|
setInitialized(true);
|
|
43
45
|
|
|
44
46
|
if (!response.ok) {
|
|
45
|
-
throw new
|
|
47
|
+
throw new ErrorMessage(response.statusText);
|
|
46
48
|
}
|
|
47
49
|
return response;
|
|
48
50
|
})
|
|
@@ -50,11 +52,18 @@ export default function Session({ children }: { children: React.ReactChild }) {
|
|
|
50
52
|
.then((session: SessionData) => {
|
|
51
53
|
setSession(session);
|
|
52
54
|
})
|
|
53
|
-
.catch((err) => {
|
|
54
|
-
|
|
55
|
+
.catch((err: Error) => {
|
|
56
|
+
if (err.message === "Unauthorized") {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setError(err.message);
|
|
55
60
|
});
|
|
56
61
|
}, [session?.token, initialized]);
|
|
57
62
|
|
|
63
|
+
if (error) {
|
|
64
|
+
return <ErrorMessage error={error} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
if (!initialized) {
|
|
59
68
|
return (
|
|
60
69
|
<div>
|