@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 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
@@ -4,6 +4,7 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.min.css">
7
8
  <title>App</title>
8
9
  </head>
9
10
 
package/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@stanlemon/app-template",
3
- "version": "0.1.2",
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": "webpack serve",
10
- "build": "NODE_ENV=production webpack",
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
- @primaryFont: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
1
+ @errorColor: "red";
2
2
 
3
- body {
4
- font-family: @primaryFont;
3
+ noscript {
4
+ background-color: @errorColor;
5
5
  }
package/src/App.test.tsx CHANGED
@@ -1,18 +1,45 @@
1
- import { render, screen, fireEvent } from "@testing-library/react";
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
- test("<App/>", () => {
6
- render(<App />);
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
- // The header is present
9
- expect(screen.getByRole("heading")).toHaveTextContent("Hello World!");
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.getByRole("textbox"), "The first item");
39
+ userEvent.type(screen.getByLabelText("Item"), "The first item");
13
40
 
14
41
  // Click the add button
15
- fireEvent.click(screen.getByRole("button"));
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
- <div>
79
+ <SessionContext.Provider value={contextValue}>
10
80
  <Header />
11
- <Input onClick={(item) => setItems([...items, item])} />
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
- </div>
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
- onClick,
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
- onClick: (value: string) => void;
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
- setValue(e.currentTarget.value);
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
- handleClick();
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="text"
38
+ type={type}
39
+ id={name}
30
40
  onChange={handleChange}
31
41
  onKeyPress={handleKeyPress}
42
+ placeholder={placeholder}
32
43
  value={value}
33
44
  />
34
- <button onClick={handleClick}>Add</button>
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
+ }
@@ -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
+ }