@stanlemon/app-template 0.2.9 → 0.2.10

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 CHANGED
@@ -30,3 +30,12 @@ app.post(
30
30
  return db.data.items;
31
31
  })
32
32
  );
33
+
34
+ app.delete(
35
+ "/api/items/:item",
36
+ handler(async ({ item }) => {
37
+ db.data.items = db.data.items.filter((i) => i !== item);
38
+ await db.write();
39
+ return db.data.items;
40
+ })
41
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stanlemon/app-template",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",
package/src/App.test.tsx CHANGED
@@ -1,10 +1,4 @@
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
3
  import App from "./App";
10
4
  import { SessionContext } from "./Session";
@@ -21,42 +15,36 @@ global.fetch = jest.fn((url, opts: { method: string; body: string }) => {
21
15
  }) as jest.Mock;
22
16
 
23
17
  test("<App/>", async () => {
24
- act(() => {
25
- render(
26
- <SessionContext.Provider
27
- value={{
28
- session: {
29
- token: "abcd",
30
- user: {
31
- username: "user",
32
- password: "password",
33
- name: "user",
34
- email: "user@example.com",
35
- },
18
+ render(
19
+ <SessionContext.Provider
20
+ value={{
21
+ session: {
22
+ token: "abcd",
23
+ user: {
24
+ username: "user",
25
+ password: "password",
26
+ name: "user",
27
+ email: "user@example.com",
36
28
  },
37
- setSession: () => {},
38
- }}
39
- >
40
- <App />
41
- </SessionContext.Provider>
42
- );
43
- });
29
+ },
30
+ setSession: () => {},
31
+ }}
32
+ >
33
+ <App />
34
+ </SessionContext.Provider>
35
+ );
44
36
 
45
37
  // The auth text is present
46
- expect(screen.getByText("You logged in as user")).toBeInTheDocument();
38
+ expect(screen.getByText("You are logged in as user.")).toBeInTheDocument();
47
39
 
48
40
  // The header is present
49
41
  expect(
50
42
  screen.getByRole("heading", { name: "Hello World!" })
51
43
  ).toBeInTheDocument();
52
44
 
53
- expect(
54
- await screen.findByText("item one", { selector: "li" })
55
- ).toBeInTheDocument();
45
+ expect(await screen.findByText("item one")).toBeInTheDocument();
56
46
 
57
- expect(
58
- await screen.findByText("item two", { selector: "li" })
59
- ).toBeInTheDocument();
47
+ expect(await screen.findByText("item two")).toBeInTheDocument();
60
48
 
61
49
  // Type some data into the input
62
50
  await userEvent.type(screen.getByLabelText("Item"), "item three");
@@ -67,7 +55,5 @@ test("<App/>", async () => {
67
55
  });
68
56
 
69
57
  // Now we should have a list item with the text we entered
70
- expect(
71
- await screen.findByText("item three", { selector: "li" })
72
- ).toBeInTheDocument();
58
+ expect(await screen.findByText("item three")).toBeInTheDocument();
73
59
  });
package/src/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useContext, useEffect } from "react";
1
+ import React, { useState, useContext, useEffect } from "react";
2
2
  import "./App.less";
3
3
  import { SessionContext } from "./Session";
4
4
  import Header from "./Header";
@@ -6,7 +6,7 @@ import Input from "./Input";
6
6
  import Login from "./Login";
7
7
  import Register from "./Register";
8
8
 
9
- export type ErrorMessage = {
9
+ export type ErrorResponse = {
10
10
  message: string;
11
11
  };
12
12
 
@@ -15,39 +15,67 @@ export type FormErrors = {
15
15
  };
16
16
 
17
17
  export default function App() {
18
- const [value, setValue] = useState<string>("");
18
+ const [loaded, setLoaded] = useState<boolean>(false);
19
19
  const [items, setItems] = useState<string[]>([]);
20
+ const [error, setError] = useState<string | boolean>(false);
21
+
22
+ const { session, setSession } = useContext(SessionContext);
20
23
 
21
- const { session } = useContext(SessionContext);
24
+ const catchError = (err: Error) => {
25
+ if (err.message === "Unauthorized") {
26
+ return;
27
+ }
28
+ setError(err.message);
29
+ };
22
30
 
23
- const itemsJson = JSON.stringify(items);
24
31
  useEffect(() => {
25
- fetchApi("/api/items", session?.token || "")
26
- .then((items: string[]) => {
32
+ if (loaded) {
33
+ return;
34
+ }
35
+
36
+ fetchApi<string[], null>("/api/items", session?.token || "")
37
+ .then((items) => {
38
+ setLoaded(true);
27
39
  setItems(items);
28
40
  })
29
- .catch((err) => console.error(err));
30
- }, [itemsJson, session?.token]);
41
+ .catch(catchError);
42
+ });
31
43
 
32
- const addItem = () => {
33
- setValue("");
44
+ const saveItem = (item: string) => {
45
+ fetchApi<string[], string>("/api/items", session?.token || "", "post", item)
46
+ .then((items) => {
47
+ setItems(items);
48
+ })
49
+ .catch(catchError);
50
+ };
34
51
 
35
- fetchApi("/api/items", session?.token || "", "post", value)
36
- .then((items: string[]) => {
52
+ const deleteItem = (item: string) => {
53
+ fetchApi<string[], string>(
54
+ "/api/items/" + item,
55
+ session?.token || "",
56
+ "delete"
57
+ )
58
+ .then((items) => {
37
59
  setItems(items);
38
60
  })
39
- .catch((err) => console.error(err));
61
+ .catch(catchError);
62
+ };
63
+
64
+ const logout = () => {
65
+ setSession({ token: null, user: null });
40
66
  };
41
67
 
42
68
  return (
43
69
  <>
44
70
  <Header />
71
+ <ErrorMessage error={error} />
45
72
  {!session.user && (
46
73
  <Row>
47
74
  <Column>
48
75
  <h2>Login</h2>
49
76
  <Login />
50
77
  </Column>
78
+ <Column />
51
79
  <Column>
52
80
  <h2>Register</h2>
53
81
  <Register />
@@ -57,63 +85,128 @@ export default function App() {
57
85
  {session.user && (
58
86
  <>
59
87
  <p>
60
- <em>You logged in as {session.user?.username}</em>
88
+ <em>You are logged in as {session.user?.username}.</em>{" "}
89
+ <span style={{ cursor: "pointer" }} onClick={logout}>
90
+ (logout)
91
+ </span>
61
92
  </p>
62
- <Input
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>
93
+ <ItemList items={items} saveItem={saveItem} deleteItem={deleteItem} />
75
94
  </>
76
95
  )}
96
+ <Spacer />
77
97
  </>
78
98
  );
79
99
  }
80
100
 
81
- function Row({ children }: { children: React.ReactNode }) {
101
+ function ItemList({
102
+ items,
103
+ saveItem,
104
+ deleteItem,
105
+ }: {
106
+ items: string[];
107
+ saveItem(item: string): void;
108
+ deleteItem(item: string): void;
109
+ }) {
110
+ const [value, setValue] = useState<string>("");
111
+
112
+ const addItem = () => {
113
+ saveItem(value);
114
+ setValue("");
115
+ };
116
+
82
117
  return (
83
- <div
84
- style={{
118
+ <>
119
+ <h2>New Item</h2>
120
+ <Input
121
+ label="Item"
122
+ name="item"
123
+ value={value}
124
+ onChange={(value) => setValue(value)}
125
+ onEnter={addItem}
126
+ />
127
+ <button onClick={addItem}>Add</button>
128
+ <Spacer />
129
+ <h2>My Items</h2>
130
+ <ul style={{ padding: 0 }}>
131
+ {items.map((item, i) => (
132
+ <Row key={i} as="li">
133
+ <button
134
+ style={{ marginLeft: "auto", order: 2 }}
135
+ onClick={() => deleteItem(item)}
136
+ >
137
+ Delete
138
+ </button>
139
+ <div>{item}</div>
140
+ </Row>
141
+ ))}
142
+ </ul>
143
+ </>
144
+ );
145
+ }
146
+
147
+ export function Row({
148
+ as = "div",
149
+ children,
150
+ }: {
151
+ as?: keyof JSX.IntrinsicElements;
152
+ children?: React.ReactNode;
153
+ }) {
154
+ return React.createElement(
155
+ as,
156
+ {
157
+ style: {
85
158
  display: "flex",
86
159
  flexDirection: "row",
87
160
  flexWrap: "wrap",
88
161
  width: "100%",
89
- }}
90
- >
91
- {children}
92
- </div>
162
+ },
163
+ },
164
+ children
93
165
  );
94
166
  }
95
167
 
96
- function Column({ children }: { children: React.ReactNode }) {
97
- return (
98
- <div
99
- style={{
168
+ export function Spacer({ height = "2em" }: { height?: string | number }) {
169
+ return <div style={{ height, minHeight: height }} />;
170
+ }
171
+
172
+ export function Column({
173
+ as = "div",
174
+ children,
175
+ }: {
176
+ as?: keyof JSX.IntrinsicElements;
177
+ children?: React.ReactNode;
178
+ }) {
179
+ return React.createElement(
180
+ as,
181
+ {
182
+ style: {
100
183
  display: "flex",
101
184
  flexDirection: "column",
102
185
  flexBasis: "100%",
103
- flex: 1,
104
- }}
105
- >
106
- {children}
107
- </div>
186
+ flex: "1 1 0",
187
+ },
188
+ },
189
+ children
108
190
  );
109
191
  }
110
192
 
111
- function fetchApi(
193
+ export function ErrorMessage({ error }: { error: string | boolean }) {
194
+ if (error) {
195
+ return (
196
+ <p>
197
+ <strong>An error has occurred:</strong> {error}
198
+ </p>
199
+ );
200
+ }
201
+ return <></>;
202
+ }
203
+
204
+ function fetchApi<T, P>(
112
205
  url: string,
113
206
  token: string,
114
207
  method = "get",
115
- data?: any
116
- ): Promise<any> {
208
+ data?: P
209
+ ): Promise<T> {
117
210
  return fetch(url, {
118
211
  method: method,
119
212
  headers: {
@@ -122,12 +215,10 @@ function fetchApi(
122
215
  "Content-Type": "application/json",
123
216
  },
124
217
  body: JSON.stringify(data),
125
- })
126
- .then((response) => {
127
- if (!response.ok) {
128
- throw new Error(response.statusText);
129
- }
130
- return response;
131
- })
132
- .then((response) => response.json());
218
+ }).then((response) => {
219
+ if (!response.ok) {
220
+ throw new Error(response.statusText);
221
+ }
222
+ return response.json() as Promise<T>;
223
+ });
133
224
  }
package/src/Login.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useContext } from "react";
2
- import { ErrorMessage } from "./App";
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 ErrorMessage).message);
47
+ setError((data as ErrorResponse).message);
48
48
  }
49
49
  }
50
50
  )
51
- .catch((err) => {
52
- console.error("error", err);
51
+ .catch((err: Error) => {
52
+ setError(err.message);
53
53
  });
54
54
  };
55
55
 
56
56
  return (
57
- <div>
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
- </div>
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
- console.error("error", err);
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
- <div>
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
- </div>
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.ReactChild }) {
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 Error(response.statusText);
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
- console.error(err);
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>