@stanlemon/app-template 0.1.2 → 0.2.2
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 +18 -0
- package/index.html +1 -0
- package/package.json +8 -4
- package/src/App.less +3 -3
- package/src/App.test.tsx +34 -7
- package/src/App.tsx +104 -4
- package/src/Input.tsx +27 -16
- package/src/Login.tsx +81 -0
- package/src/Register.tsx +78 -0
package/app.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppServer,
|
|
3
|
+
asyncJsonHandler as handler,
|
|
4
|
+
SimpleUsersDao,
|
|
5
|
+
} from "@stanlemon/server-with-auth";
|
|
6
|
+
|
|
7
|
+
const users = new SimpleUsersDao();
|
|
8
|
+
|
|
9
|
+
const app = createAppServer({
|
|
10
|
+
webpack: "http://localhost:8080",
|
|
11
|
+
secure: ["/api/"],
|
|
12
|
+
...users,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.get(
|
|
16
|
+
"/api/users",
|
|
17
|
+
handler(() => ({ users: users.users }))
|
|
18
|
+
);
|
package/index.html
CHANGED
package/package.json
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/app-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start": "
|
|
10
|
-
"build": "
|
|
9
|
+
"start": "node app.js",
|
|
10
|
+
"build": "npm run webpack:build",
|
|
11
|
+
"webpack:serve": "webpack serve",
|
|
12
|
+
"webpack:build": "NODE_ENV=production webpack",
|
|
11
13
|
"test": "jest",
|
|
14
|
+
"test:watch": "jest -w",
|
|
12
15
|
"test:coverage": "jest --coverage",
|
|
13
16
|
"lint": "eslint --ext js,jsx,ts,tsx ./src/",
|
|
14
17
|
"lint:format": "eslint --fix --ext js,jsx,ts,tsx ./src/"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@stanlemon/webdev": "*"
|
|
20
|
+
"@stanlemon/webdev": "*",
|
|
21
|
+
"@stanlemon/server-with-auth": "*"
|
|
18
22
|
}
|
|
19
23
|
}
|
package/src/App.less
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
@
|
|
1
|
+
@errorColor: "red";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
noscript {
|
|
4
|
+
background-color: @errorColor;
|
|
5
5
|
}
|
package/src/App.test.tsx
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
fireEvent,
|
|
5
|
+
waitFor,
|
|
6
|
+
act,
|
|
7
|
+
} from "@testing-library/react";
|
|
2
8
|
import userEvent from "@testing-library/user-event";
|
|
3
9
|
import App from "./App";
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
global.fetch = jest.fn(() =>
|
|
12
|
+
Promise.resolve({
|
|
13
|
+
ok: true,
|
|
14
|
+
json: () =>
|
|
15
|
+
Promise.resolve({
|
|
16
|
+
token: "token",
|
|
17
|
+
user: {
|
|
18
|
+
name: "Test Tester",
|
|
19
|
+
email: "test@test.com",
|
|
20
|
+
username: "test",
|
|
21
|
+
password: "password",
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
})
|
|
25
|
+
) as jest.Mock;
|
|
7
26
|
|
|
8
|
-
|
|
9
|
-
|
|
27
|
+
test("<App/>", async () => {
|
|
28
|
+
act(() => {
|
|
29
|
+
render(<App />);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// A fetch request will be made, and then the page will be initialized, wait for that
|
|
33
|
+
await waitFor(() => {
|
|
34
|
+
// The header is present
|
|
35
|
+
expect(screen.getByRole("heading")).toHaveTextContent("Hello World!");
|
|
36
|
+
});
|
|
10
37
|
|
|
11
38
|
// Type some data into the input
|
|
12
|
-
userEvent.type(screen.
|
|
39
|
+
userEvent.type(screen.getByLabelText("Item"), "The first item");
|
|
13
40
|
|
|
14
41
|
// Click the add button
|
|
15
|
-
fireEvent.click(screen.
|
|
42
|
+
fireEvent.click(screen.getByText("Add", { selector: "button" }));
|
|
16
43
|
|
|
17
44
|
// Now we should have a list item with the text we entered
|
|
18
45
|
expect(screen.getByRole("listitem")).toHaveTextContent("The first item");
|
package/src/App.tsx
CHANGED
|
@@ -1,19 +1,119 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, useEffect, createContext } from "react";
|
|
2
2
|
import "./App.less";
|
|
3
3
|
import Header from "./Header";
|
|
4
4
|
import Input from "./Input";
|
|
5
|
+
import Login from "./Login";
|
|
6
|
+
import Register from "./Register";
|
|
7
|
+
|
|
8
|
+
export const SessionContext = createContext<{
|
|
9
|
+
session: Session | null;
|
|
10
|
+
setSession: React.Dispatch<React.SetStateAction<Session | null>>;
|
|
11
|
+
} | null>(null);
|
|
12
|
+
|
|
13
|
+
export type ErrorMessage = {
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FormErrors = {
|
|
18
|
+
errors: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Session = {
|
|
22
|
+
token: string | null;
|
|
23
|
+
user: User | null;
|
|
24
|
+
};
|
|
25
|
+
export type User = {
|
|
26
|
+
name: string | null;
|
|
27
|
+
email: string | null;
|
|
28
|
+
username: string;
|
|
29
|
+
password: string;
|
|
30
|
+
};
|
|
5
31
|
|
|
6
32
|
export default function App() {
|
|
33
|
+
const [initialized, setInitialized] = useState<boolean>(false);
|
|
34
|
+
const [session, setSession] = useState<Session | null>(null);
|
|
35
|
+
const [value, setValue] = useState<string>("");
|
|
7
36
|
const [items, setItems] = useState<string[]>([]);
|
|
37
|
+
|
|
38
|
+
const contextValue = { session, setSession };
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetch("/auth/session", {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${session?.token || ""}`,
|
|
44
|
+
Accept: "application/json",
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
.then((response) => {
|
|
49
|
+
setInitialized(true);
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(response.statusText);
|
|
53
|
+
}
|
|
54
|
+
return response;
|
|
55
|
+
})
|
|
56
|
+
.then((response) => response.json())
|
|
57
|
+
.then((session: Session) => {
|
|
58
|
+
setSession(session);
|
|
59
|
+
})
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
console.error(err);
|
|
62
|
+
});
|
|
63
|
+
}, [session?.token, initialized]);
|
|
64
|
+
|
|
65
|
+
const addItem = () => {
|
|
66
|
+
setItems([...items, value]);
|
|
67
|
+
setValue("");
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!initialized) {
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<em>Loading...</em>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
8
78
|
return (
|
|
9
|
-
<
|
|
79
|
+
<SessionContext.Provider value={contextValue}>
|
|
10
80
|
<Header />
|
|
11
|
-
<
|
|
81
|
+
<div>
|
|
82
|
+
{!session && (
|
|
83
|
+
<>
|
|
84
|
+
<p>
|
|
85
|
+
<em>You are not currently logged in.</em>
|
|
86
|
+
</p>
|
|
87
|
+
<Login />
|
|
88
|
+
<Spacer />
|
|
89
|
+
<Register />
|
|
90
|
+
<Spacer />
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
{session?.user && (
|
|
94
|
+
<p>
|
|
95
|
+
<em>You logged in as {session.user.username}.</em>
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
<Input
|
|
100
|
+
label="Item"
|
|
101
|
+
name="item"
|
|
102
|
+
value={value}
|
|
103
|
+
onChange={(value) => setValue(value)}
|
|
104
|
+
onEnter={addItem}
|
|
105
|
+
/>
|
|
106
|
+
<button onClick={addItem}>Add</button>
|
|
107
|
+
|
|
12
108
|
<ul>
|
|
13
109
|
{items.map((item, i) => (
|
|
14
110
|
<li key={i}>{item}</li>
|
|
15
111
|
))}
|
|
16
112
|
</ul>
|
|
17
|
-
</
|
|
113
|
+
</SessionContext.Provider>
|
|
18
114
|
);
|
|
19
115
|
}
|
|
116
|
+
|
|
117
|
+
function Spacer() {
|
|
118
|
+
return <div style={{ minHeight: "2em" }} />;
|
|
119
|
+
}
|
package/src/Input.tsx
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
|
|
3
1
|
export default function Input({
|
|
4
|
-
|
|
2
|
+
type = "text",
|
|
3
|
+
name,
|
|
4
|
+
value = "",
|
|
5
|
+
label,
|
|
6
|
+
placeholder,
|
|
7
|
+
onChange = () => {
|
|
8
|
+
/* noop */
|
|
9
|
+
},
|
|
10
|
+
onEnter = () => {
|
|
11
|
+
/* noop */
|
|
12
|
+
},
|
|
13
|
+
error,
|
|
5
14
|
}: {
|
|
6
|
-
|
|
15
|
+
type?: string;
|
|
16
|
+
name: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
onEnter?: () => void;
|
|
22
|
+
error?: string;
|
|
7
23
|
}) {
|
|
8
|
-
const [value, setValue] = useState("");
|
|
9
|
-
|
|
10
24
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
11
|
-
|
|
25
|
+
onChange(e.currentTarget.value);
|
|
12
26
|
};
|
|
13
|
-
|
|
14
27
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
15
28
|
if (e.key === "Enter") {
|
|
16
29
|
e.preventDefault();
|
|
17
|
-
|
|
30
|
+
onEnter();
|
|
18
31
|
}
|
|
19
32
|
};
|
|
20
33
|
|
|
21
|
-
const handleClick = () => {
|
|
22
|
-
onClick(value);
|
|
23
|
-
setValue("");
|
|
24
|
-
};
|
|
25
|
-
|
|
26
34
|
return (
|
|
27
35
|
<>
|
|
36
|
+
{label && <label htmlFor={name}>{label}</label>}
|
|
28
37
|
<input
|
|
29
|
-
type=
|
|
38
|
+
type={type}
|
|
39
|
+
id={name}
|
|
30
40
|
onChange={handleChange}
|
|
31
41
|
onKeyPress={handleKeyPress}
|
|
42
|
+
placeholder={placeholder}
|
|
32
43
|
value={value}
|
|
33
44
|
/>
|
|
34
|
-
<
|
|
45
|
+
{error && <div>{error}</div>}
|
|
35
46
|
</>
|
|
36
47
|
);
|
|
37
48
|
}
|
package/src/Login.tsx
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState, useContext } from "react";
|
|
2
|
+
import { Session, User, SessionContext, ErrorMessage } from "./App";
|
|
3
|
+
import Input from "./Input";
|
|
4
|
+
|
|
5
|
+
export default function Login() {
|
|
6
|
+
const [error, setError] = useState<string | null>(null);
|
|
7
|
+
const [values, setValues] = useState<User>({
|
|
8
|
+
name: "",
|
|
9
|
+
email: "",
|
|
10
|
+
username: "",
|
|
11
|
+
password: "",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const { setSession } = useContext(SessionContext) || {
|
|
15
|
+
setSession: () => {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const onSubmit = () => {
|
|
19
|
+
setError(null);
|
|
20
|
+
fetch("/auth/login", {
|
|
21
|
+
headers: {
|
|
22
|
+
Accept: "application/json",
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
},
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: JSON.stringify(values),
|
|
27
|
+
})
|
|
28
|
+
.then((response) =>
|
|
29
|
+
response.json().then((data: Record<string, unknown>) => ({
|
|
30
|
+
ok: response.ok,
|
|
31
|
+
status: response.status,
|
|
32
|
+
data,
|
|
33
|
+
}))
|
|
34
|
+
)
|
|
35
|
+
.then(
|
|
36
|
+
({
|
|
37
|
+
ok,
|
|
38
|
+
status,
|
|
39
|
+
data,
|
|
40
|
+
}: {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
status: number;
|
|
43
|
+
data: Record<string, unknown>;
|
|
44
|
+
}) => {
|
|
45
|
+
if (ok) {
|
|
46
|
+
setSession(data as Session);
|
|
47
|
+
} else {
|
|
48
|
+
setError((data as ErrorMessage).message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
console.error("error", err);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
{error && (
|
|
60
|
+
<div>
|
|
61
|
+
<strong>{error}</strong>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
<Input
|
|
65
|
+
name="username"
|
|
66
|
+
label="Username"
|
|
67
|
+
value={values.username}
|
|
68
|
+
onChange={(value) => setValues({ ...values, username: value })}
|
|
69
|
+
/>
|
|
70
|
+
<Input
|
|
71
|
+
name="password"
|
|
72
|
+
type="password"
|
|
73
|
+
label="Password"
|
|
74
|
+
value={values.password}
|
|
75
|
+
onChange={(value) => setValues({ ...values, password: value })}
|
|
76
|
+
onEnter={onSubmit}
|
|
77
|
+
/>
|
|
78
|
+
<button onClick={onSubmit}>Login</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
package/src/Register.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useState, useContext } from "react";
|
|
2
|
+
import { Session, User, SessionContext, FormErrors } from "./App";
|
|
3
|
+
import Input from "./Input";
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line max-lines-per-function
|
|
6
|
+
export default function Register() {
|
|
7
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
8
|
+
const [values, setValues] = useState<User>({
|
|
9
|
+
name: "",
|
|
10
|
+
email: "",
|
|
11
|
+
username: "",
|
|
12
|
+
password: "",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const { setSession } = useContext(SessionContext) || {
|
|
16
|
+
setSession: () => {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const onSubmit = () => {
|
|
20
|
+
setErrors({});
|
|
21
|
+
fetch("/auth/register", {
|
|
22
|
+
headers: {
|
|
23
|
+
Accept: "application/json",
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: JSON.stringify(values),
|
|
28
|
+
})
|
|
29
|
+
.then((response) =>
|
|
30
|
+
response.json().then((data: Record<string, unknown>) => ({
|
|
31
|
+
ok: response.ok,
|
|
32
|
+
status: response.status,
|
|
33
|
+
data,
|
|
34
|
+
}))
|
|
35
|
+
)
|
|
36
|
+
.then(
|
|
37
|
+
({
|
|
38
|
+
ok,
|
|
39
|
+
status,
|
|
40
|
+
data,
|
|
41
|
+
}: {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
status: number;
|
|
44
|
+
data: Record<string, unknown>;
|
|
45
|
+
}) => {
|
|
46
|
+
if (ok) {
|
|
47
|
+
setSession(data as Session);
|
|
48
|
+
} else {
|
|
49
|
+
setErrors((data as FormErrors).errors);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
console.error("error", err);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<Input
|
|
61
|
+
name="username"
|
|
62
|
+
label="Username"
|
|
63
|
+
value={values.username}
|
|
64
|
+
onChange={(value) => setValues({ ...values, username: value })}
|
|
65
|
+
error={errors.username}
|
|
66
|
+
/>
|
|
67
|
+
<Input
|
|
68
|
+
name="password"
|
|
69
|
+
type="password"
|
|
70
|
+
label="Password"
|
|
71
|
+
value={values.password}
|
|
72
|
+
onChange={(value) => setValues({ ...values, password: value })}
|
|
73
|
+
error={errors.password}
|
|
74
|
+
/>
|
|
75
|
+
<button onClick={onSubmit}>Register</button>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|