create-react-adam 0.2.1 → 0.3.0
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/README.md +72 -20
- package/bin/index.js +264 -94
- package/package.json +8 -3
- package/template/.github/workflows/check.yml +1 -1
- package/template/.github/workflows/e2e.yml +3 -3
- package/template/.github/workflows/lighthouse.yml +33 -0
- package/template/.nvmrc +1 -1
- package/template/README.md +133 -31
- package/template/e2e/gitignore +13 -0
- package/template/e2e/package.json +2 -3
- package/template/e2e/playwright.config.ts +0 -1
- package/template/eslint-rules/prefer-webp-images.js +66 -0
- package/template/eslint.config.js +3 -0
- package/template/eslint.config.no-webp.js +54 -0
- package/template/gitignore +26 -0
- package/template/index.html +19 -0
- package/template/lighthouserc.json +17 -0
- package/template/npmrc +2 -0
- package/template/package.json +18 -18
- package/template/public/fonts/InterVariable.woff2 +0 -0
- package/template/public/robots.txt +5 -0
- package/template/src/App.tsx +25 -11
- package/template/src/app.css +27 -9
- package/template/src/components/Button.tsx +27 -0
- package/template/src/pages/About/index.no-utils.tsx +5 -0
- package/template/src/pages/About/index.tsx +22 -12
- package/template/src/pages/Home/index.no-utils.tsx +37 -1
- package/template/src/pages/Home/index.tsx +22 -12
- package/template/src/pages/NotFound/index.tsx +25 -18
- package/template/src/types/ui.ts +1 -0
- package/template/src/utils/classNames.ts +23 -2
- package/template/src/utils/helpers.ts +22 -0
- package/template/src/utils/useUrlState.ts +17 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { ButtonVariant } from "../types/ui";
|
|
3
|
+
|
|
4
|
+
interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
9
|
+
primary: "bg-brand-primary hover:bg-brand-primaryHover",
|
|
10
|
+
success: "bg-brand-success hover:bg-brand-success/90",
|
|
11
|
+
danger: "bg-brand-danger hover:bg-brand-danger/90",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const Button = ({ variant = "primary", className, ...props }: ButtonProps) => (
|
|
15
|
+
<button
|
|
16
|
+
className={[
|
|
17
|
+
"text-brand-white rounded-lg px-4 py-2 transition-colors",
|
|
18
|
+
variantClasses[variant],
|
|
19
|
+
className,
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(" ")}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export default Button;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Link } from "wouter";
|
|
2
3
|
|
|
3
4
|
const About = () => {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
document.title = "About | __PROJECT_NAME__";
|
|
7
|
+
}, []);
|
|
8
|
+
|
|
4
9
|
return (
|
|
5
10
|
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
6
11
|
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Link } from "wouter";
|
|
3
|
+
import Button from "../../components/Button";
|
|
2
4
|
import { useReactPersist } from "../../utils/Storage";
|
|
3
5
|
import { useUrlState } from "../../utils/useUrlState";
|
|
4
6
|
|
|
@@ -6,6 +8,10 @@ const About = () => {
|
|
|
6
8
|
const [sharedCount, setSharedCount] = useReactPersist("sharedCounter", 0);
|
|
7
9
|
const [urlCount, setUrlCount] = useUrlState("counter", 0);
|
|
8
10
|
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
document.title = "About | __PROJECT_NAME__";
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
9
15
|
return (
|
|
10
16
|
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
11
17
|
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
@@ -26,21 +32,23 @@ const About = () => {
|
|
|
26
32
|
This counter is shared between Home and About pages
|
|
27
33
|
</p>
|
|
28
34
|
<div className="flex items-center justify-center gap-4">
|
|
29
|
-
<
|
|
35
|
+
<Button
|
|
36
|
+
variant="danger"
|
|
37
|
+
aria-label="Decrease shared counter"
|
|
30
38
|
onClick={() => setSharedCount(sharedCount - 1)}
|
|
31
|
-
className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
|
|
32
39
|
>
|
|
33
40
|
-
|
|
34
|
-
</
|
|
41
|
+
</Button>
|
|
35
42
|
<span className="text-brand-black text-3xl font-bold">
|
|
36
43
|
{sharedCount}
|
|
37
44
|
</span>
|
|
38
|
-
<
|
|
45
|
+
<Button
|
|
46
|
+
variant="success"
|
|
47
|
+
aria-label="Increase shared counter"
|
|
39
48
|
onClick={() => setSharedCount(sharedCount + 1)}
|
|
40
|
-
className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
|
|
41
49
|
>
|
|
42
50
|
+
|
|
43
|
-
</
|
|
51
|
+
</Button>
|
|
44
52
|
</div>
|
|
45
53
|
</div>
|
|
46
54
|
|
|
@@ -52,21 +60,23 @@ const About = () => {
|
|
|
52
60
|
This counter syncs with URL parameters
|
|
53
61
|
</p>
|
|
54
62
|
<div className="flex items-center justify-center gap-4">
|
|
55
|
-
<
|
|
63
|
+
<Button
|
|
64
|
+
variant="danger"
|
|
65
|
+
aria-label="Decrease URL counter"
|
|
56
66
|
onClick={() => setUrlCount(urlCount - 1)}
|
|
57
|
-
className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
|
|
58
67
|
>
|
|
59
68
|
-
|
|
60
|
-
</
|
|
69
|
+
</Button>
|
|
61
70
|
<span className="text-brand-black text-3xl font-bold">
|
|
62
71
|
{urlCount}
|
|
63
72
|
</span>
|
|
64
|
-
<
|
|
73
|
+
<Button
|
|
74
|
+
variant="success"
|
|
75
|
+
aria-label="Increase URL counter"
|
|
65
76
|
onClick={() => setUrlCount(urlCount + 1)}
|
|
66
|
-
className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
|
|
67
77
|
>
|
|
68
78
|
+
|
|
69
|
-
</
|
|
79
|
+
</Button>
|
|
70
80
|
</div>
|
|
71
81
|
</div>
|
|
72
82
|
</div>
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
1
2
|
import { Link } from "wouter";
|
|
3
|
+
import Button from "../../components/Button";
|
|
2
4
|
|
|
3
5
|
const Home = () => {
|
|
6
|
+
const [count, setCount] = useState(0);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
document.title = "__PROJECT_NAME__";
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
4
12
|
return (
|
|
5
13
|
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
6
14
|
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
@@ -11,7 +19,7 @@ const Home = () => {
|
|
|
11
19
|
Built with React, TypeScript, Vite, Wouter, and Tailwind CSS
|
|
12
20
|
</p>
|
|
13
21
|
|
|
14
|
-
<div className="mb-8">
|
|
22
|
+
<div className="mb-8 space-y-6">
|
|
15
23
|
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
16
24
|
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
17
25
|
Get Started
|
|
@@ -27,6 +35,34 @@ const Home = () => {
|
|
|
27
35
|
This is a clean starting point for your React application.
|
|
28
36
|
</p>
|
|
29
37
|
</div>
|
|
38
|
+
|
|
39
|
+
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
40
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
41
|
+
Counter
|
|
42
|
+
</h2>
|
|
43
|
+
<p className="text-brand-gray mb-4">
|
|
44
|
+
A simple counter using the shared Button component
|
|
45
|
+
</p>
|
|
46
|
+
<div className="flex items-center justify-center gap-4">
|
|
47
|
+
<Button
|
|
48
|
+
variant="danger"
|
|
49
|
+
aria-label="Decrease counter"
|
|
50
|
+
onClick={() => setCount(count - 1)}
|
|
51
|
+
>
|
|
52
|
+
-
|
|
53
|
+
</Button>
|
|
54
|
+
<span className="text-brand-black text-3xl font-bold">
|
|
55
|
+
{count}
|
|
56
|
+
</span>
|
|
57
|
+
<Button
|
|
58
|
+
variant="success"
|
|
59
|
+
aria-label="Increase counter"
|
|
60
|
+
onClick={() => setCount(count + 1)}
|
|
61
|
+
>
|
|
62
|
+
+
|
|
63
|
+
</Button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
30
66
|
</div>
|
|
31
67
|
|
|
32
68
|
<div className="flex justify-center gap-4">
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Link } from "wouter";
|
|
3
|
+
import Button from "../../components/Button";
|
|
2
4
|
import { useReactPersist } from "../../utils/Storage";
|
|
3
5
|
import { useUrlState } from "../../utils/useUrlState";
|
|
4
6
|
|
|
@@ -6,6 +8,10 @@ const Home = () => {
|
|
|
6
8
|
const [sharedCount, setSharedCount] = useReactPersist("sharedCounter", 0);
|
|
7
9
|
const [urlCount, setUrlCount] = useUrlState("counter", 0);
|
|
8
10
|
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
document.title = "__PROJECT_NAME__";
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
9
15
|
return (
|
|
10
16
|
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
11
17
|
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
@@ -25,21 +31,23 @@ const Home = () => {
|
|
|
25
31
|
This counter is shared between Home and About pages
|
|
26
32
|
</p>
|
|
27
33
|
<div className="flex items-center justify-center gap-4">
|
|
28
|
-
<
|
|
34
|
+
<Button
|
|
35
|
+
variant="danger"
|
|
36
|
+
aria-label="Decrease shared counter"
|
|
29
37
|
onClick={() => setSharedCount(sharedCount - 1)}
|
|
30
|
-
className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
|
|
31
38
|
>
|
|
32
39
|
-
|
|
33
|
-
</
|
|
40
|
+
</Button>
|
|
34
41
|
<span className="text-brand-black text-3xl font-bold">
|
|
35
42
|
{sharedCount}
|
|
36
43
|
</span>
|
|
37
|
-
<
|
|
44
|
+
<Button
|
|
45
|
+
variant="success"
|
|
46
|
+
aria-label="Increase shared counter"
|
|
38
47
|
onClick={() => setSharedCount(sharedCount + 1)}
|
|
39
|
-
className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
|
|
40
48
|
>
|
|
41
49
|
+
|
|
42
|
-
</
|
|
50
|
+
</Button>
|
|
43
51
|
</div>
|
|
44
52
|
</div>
|
|
45
53
|
|
|
@@ -51,21 +59,23 @@ const Home = () => {
|
|
|
51
59
|
This counter syncs with URL parameters
|
|
52
60
|
</p>
|
|
53
61
|
<div className="flex items-center justify-center gap-4">
|
|
54
|
-
<
|
|
62
|
+
<Button
|
|
63
|
+
variant="danger"
|
|
64
|
+
aria-label="Decrease URL counter"
|
|
55
65
|
onClick={() => setUrlCount(urlCount - 1)}
|
|
56
|
-
className="bg-brand-danger text-brand-white hover:bg-brand-danger/90 rounded-lg px-4 py-2 transition-colors"
|
|
57
66
|
>
|
|
58
67
|
-
|
|
59
|
-
</
|
|
68
|
+
</Button>
|
|
60
69
|
<span className="text-brand-black text-3xl font-bold">
|
|
61
70
|
{urlCount}
|
|
62
71
|
</span>
|
|
63
|
-
<
|
|
72
|
+
<Button
|
|
73
|
+
variant="success"
|
|
74
|
+
aria-label="Increase URL counter"
|
|
64
75
|
onClick={() => setUrlCount(urlCount + 1)}
|
|
65
|
-
className="bg-brand-success text-brand-white hover:bg-brand-success/90 rounded-lg px-4 py-2 transition-colors"
|
|
66
76
|
>
|
|
67
77
|
+
|
|
68
|
-
</
|
|
78
|
+
</Button>
|
|
69
79
|
</div>
|
|
70
80
|
</div>
|
|
71
81
|
</div>
|
|
@@ -1,23 +1,30 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Link } from "wouter";
|
|
2
3
|
|
|
3
|
-
const NotFound = () =>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
className="
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
const NotFound = () => {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
document.title = "Page Not Found | __PROJECT_NAME__";
|
|
7
|
+
}, []);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
11
|
+
<div className="text-center">
|
|
12
|
+
<h1 className="text-brand-black text-9xl font-bold">404</h1>
|
|
13
|
+
<p className="text-brand-black mt-4 text-2xl font-semibold">
|
|
14
|
+
Page Not Found
|
|
15
|
+
</p>
|
|
16
|
+
<p className="text-brand-gray mt-2">
|
|
17
|
+
The page you're looking for doesn't exist.
|
|
18
|
+
</p>
|
|
19
|
+
<Link
|
|
20
|
+
href="/"
|
|
21
|
+
className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover mt-6 inline-block rounded-lg px-6 py-3 transition-colors"
|
|
22
|
+
>
|
|
23
|
+
Go Home
|
|
24
|
+
</Link>
|
|
25
|
+
</div>
|
|
19
26
|
</div>
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
);
|
|
28
|
+
};
|
|
22
29
|
|
|
23
30
|
export default NotFound;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ButtonVariant = "primary" | "success" | "danger";
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
type ClassValue = string | false | null | undefined | Record<string, boolean>;
|
|
2
|
+
|
|
3
|
+
export default function classNames(...classes: ClassValue[]): string {
|
|
4
|
+
const result: string[] = [];
|
|
5
|
+
|
|
6
|
+
for (const entry of classes) {
|
|
7
|
+
if (!entry) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof entry === "string") {
|
|
12
|
+
result.push(entry);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const [name, isEnabled] of Object.entries(entry)) {
|
|
17
|
+
if (isEnabled) {
|
|
18
|
+
result.push(name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result.join(" ");
|
|
3
24
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Browsers store setTimeout delays as a 32-bit signed integer, so any delay
|
|
2
|
+
// above 2^31 - 1 ms (~24.8 days) overflows and fires immediately.
|
|
3
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Drop-in replacement for setTimeout that clamps delays larger than the
|
|
7
|
+
* maximum 32-bit signed integer (2,147,483,647 ms / ~24.8 days) instead of
|
|
8
|
+
* letting them overflow and fire immediately.
|
|
9
|
+
*/
|
|
10
|
+
export function safeTimeout<TArgs extends unknown[]>(
|
|
11
|
+
callback: (...callbackArgs: TArgs) => void,
|
|
12
|
+
delay: number,
|
|
13
|
+
...args: TArgs
|
|
14
|
+
): ReturnType<typeof globalThis.setTimeout> {
|
|
15
|
+
if (delay > MAX_TIMEOUT_MS) {
|
|
16
|
+
console.error(
|
|
17
|
+
`safeTimeout: delay ${delay} exceeds the maximum of ${MAX_TIMEOUT_MS} ms (~24.8 days). Using the maximum value.`,
|
|
18
|
+
);
|
|
19
|
+
delay = MAX_TIMEOUT_MS;
|
|
20
|
+
}
|
|
21
|
+
return globalThis.setTimeout(callback, delay, ...args);
|
|
22
|
+
}
|
|
@@ -4,16 +4,28 @@ import { useLocation, useSearch } from "wouter";
|
|
|
4
4
|
export function useUrlState(
|
|
5
5
|
key: string,
|
|
6
6
|
defaultValue: number,
|
|
7
|
-
): [number, (value: number) => void]
|
|
7
|
+
): [number, (value: number) => void];
|
|
8
|
+
export function useUrlState(
|
|
9
|
+
key: string,
|
|
10
|
+
defaultValue: string,
|
|
11
|
+
): [string, (value: string) => void];
|
|
12
|
+
export function useUrlState(
|
|
13
|
+
key: string,
|
|
14
|
+
defaultValue: string | number,
|
|
15
|
+
): [string, (value: string) => void] | [number, (value: number) => void] {
|
|
8
16
|
const [, setLocation] = useLocation();
|
|
9
17
|
const searchParams = useSearch();
|
|
10
18
|
|
|
11
19
|
const params = new URLSearchParams(searchParams);
|
|
12
20
|
const urlValue = params.get(key);
|
|
13
|
-
const value = urlValue
|
|
21
|
+
const value = urlValue
|
|
22
|
+
? typeof defaultValue === "number"
|
|
23
|
+
? Number.parseInt(urlValue, 10)
|
|
24
|
+
: urlValue
|
|
25
|
+
: defaultValue;
|
|
14
26
|
|
|
15
27
|
const setValue = useCallback(
|
|
16
|
-
(newValue: number) => {
|
|
28
|
+
(newValue: string | number) => {
|
|
17
29
|
const currentParams = new URLSearchParams(globalThis.location.search);
|
|
18
30
|
currentParams.set(key, newValue.toString());
|
|
19
31
|
setLocation(
|
|
@@ -23,5 +35,6 @@ export function useUrlState(
|
|
|
23
35
|
[key, setLocation],
|
|
24
36
|
);
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
// The overloads guarantee value/setValue agree with the default's type.
|
|
39
|
+
return [value, setValue] as [string, (value: string) => void];
|
|
27
40
|
}
|