create-react-adam 0.1.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 +182 -0
- package/bin/index.js +245 -0
- package/package.json +43 -0
- package/template/.e2e-deps.json +11 -0
- package/template/.github/renovate.json +24 -0
- package/template/.github/workflows/check.yml +36 -0
- package/template/.github/workflows/e2e.yml +48 -0
- package/template/.nvmrc +2 -0
- package/template/.prettierrc +3 -0
- package/template/README.md +83 -0
- package/template/e2e/README.md +94 -0
- package/template/e2e/example.spec.ts +14 -0
- package/template/eslint.config.js +54 -0
- package/template/index.html +12 -0
- package/template/package-lock.json +6571 -0
- package/template/package.json +47 -0
- package/template/playwright.config.ts +25 -0
- package/template/src/App.tsx +17 -0
- package/template/src/app.css +29 -0
- package/template/src/main.tsx +13 -0
- package/template/src/pages/About/index.no-utils.tsx +39 -0
- package/template/src/pages/About/index.tsx +97 -0
- package/template/src/pages/Home/index.no-utils.tsx +53 -0
- package/template/src/pages/Home/index.tsx +94 -0
- package/template/src/pages/NotFound/index.tsx +23 -0
- package/template/src/utils/Internet.ts +22 -0
- package/template/src/utils/Storage.ts +86 -0
- package/template/src/utils/classNames.ts +3 -0
- package/template/src/utils/useUrlState.ts +27 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.json +21 -0
- package/template/vite.config.ts +9 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint:check": "eslint .",
|
|
11
|
+
"lint": "eslint . --fix",
|
|
12
|
+
"format:check": "prettier --check .",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"setFormatToPrecommitHook": "echo \"#!/bin/bash\\n\\nSTAGED_JS_FILES=\\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\\\.(tsx?|js)\\$' || true)\\n\\nif [ -n \\\"\\$STAGED_JS_FILES\\\" ]; then\\n echo \\\"\\$STAGED_JS_FILES\\\" | xargs npx prettier --write --ignore-unknown || exit 1\\n echo \\\"\\$STAGED_JS_FILES\\\" | xargs npx eslint --fix || exit 1\\n echo \\\"\\$STAGED_JS_FILES\\\" | xargs git add\\nfi\" > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit",
|
|
15
|
+
"_itDoesNotWork": "echo '\\nš Troubleshooting the react project...\\n' && echo '\\nš¦ Step 1: Installing Node dependencies\\n' && npm install --silent && echo 'ā
Node dependencies installed successfully!' && echo '\\nš Step 2: Checking if Vite dev server is running\\n' && (pgrep -f \"vite\" > /dev/null && echo 'ā
Vite dev server is running') || echo 'ā Vite dev server is not running. Start it with: npm run dev' && echo '\\nā
Basic troubleshooting complete. If issues persist, please ask for help.'",
|
|
16
|
+
"itDoesNotWork": "npm run _itDoesNotWork --silent"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"react": "19.2.0",
|
|
20
|
+
"react-dom": "19.2.0",
|
|
21
|
+
"react-icons": "5.5.0",
|
|
22
|
+
"wouter": "3.7.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/js": "9.39.0",
|
|
26
|
+
"@tailwindcss/vite": "4.1.16",
|
|
27
|
+
"@types/react": "19.2.2",
|
|
28
|
+
"@types/react-dom": "19.2.2",
|
|
29
|
+
"@vitejs/plugin-react": "5.1.0",
|
|
30
|
+
"eslint": "9.39.0",
|
|
31
|
+
"eslint-config-prettier": "10.1.8",
|
|
32
|
+
"eslint-plugin-de-morgan": "2.0.0",
|
|
33
|
+
"eslint-plugin-import": "2.32.0",
|
|
34
|
+
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
35
|
+
"eslint-plugin-promise": "7.2.1",
|
|
36
|
+
"eslint-plugin-react-hooks": "7.0.1",
|
|
37
|
+
"eslint-plugin-react-refresh": "0.4.24",
|
|
38
|
+
"eslint-plugin-unicorn": "62.0.0",
|
|
39
|
+
"prettier": "3.6.2",
|
|
40
|
+
"prettier-plugin-organize-imports": "4.3.0",
|
|
41
|
+
"prettier-plugin-tailwindcss": "0.7.1",
|
|
42
|
+
"tailwindcss": "4.1.16",
|
|
43
|
+
"typescript": "5.9.3",
|
|
44
|
+
"typescript-eslint": "8.46.2",
|
|
45
|
+
"vite": "7.1.12"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: "./e2e",
|
|
5
|
+
fullyParallel: true,
|
|
6
|
+
forbidOnly: !!process.env.CI,
|
|
7
|
+
retries: process.env.CI ? 2 : 0,
|
|
8
|
+
workers: process.env.CI ? 1 : undefined,
|
|
9
|
+
reporter: [["html"], ["allure-playwright"]],
|
|
10
|
+
use: {
|
|
11
|
+
baseURL: "http://localhost:5173",
|
|
12
|
+
trace: "on-first-retry",
|
|
13
|
+
},
|
|
14
|
+
projects: [
|
|
15
|
+
{
|
|
16
|
+
name: "chromium",
|
|
17
|
+
use: { ...devices["Desktop Chrome"] },
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
webServer: {
|
|
21
|
+
command: "npm run dev",
|
|
22
|
+
url: "http://localhost:5173",
|
|
23
|
+
reuseExistingServer: !process.env.CI,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Redirect, Route, Switch } from "wouter";
|
|
2
|
+
import About from "./pages/About";
|
|
3
|
+
import Home from "./pages/Home";
|
|
4
|
+
import NotFound from "./pages/NotFound";
|
|
5
|
+
|
|
6
|
+
const App = () => (
|
|
7
|
+
<Switch>
|
|
8
|
+
<Route path="/" component={Home} />
|
|
9
|
+
<Route path="/about" component={About} />
|
|
10
|
+
<Route path="/home">
|
|
11
|
+
<Redirect to="/" />
|
|
12
|
+
</Route>
|
|
13
|
+
<Route component={NotFound} />
|
|
14
|
+
</Switch>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export default App;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--font-sans: "Inter", sans-serif;
|
|
5
|
+
|
|
6
|
+
--color-brand-primary: #3b82f6;
|
|
7
|
+
--color-brand-primaryHover: #2563eb;
|
|
8
|
+
--color-brand-primaryActive: #1d4ed8;
|
|
9
|
+
|
|
10
|
+
--color-brand-secondary: #8b5cf6;
|
|
11
|
+
--color-brand-secondaryHover: #7c3aed;
|
|
12
|
+
--color-brand-secondaryActive: #6d28d9;
|
|
13
|
+
|
|
14
|
+
--color-brand-success: #22c55e;
|
|
15
|
+
--color-brand-warning: #f59e0b;
|
|
16
|
+
--color-brand-danger: #ef4444;
|
|
17
|
+
|
|
18
|
+
--color-brand-black: #0f172a;
|
|
19
|
+
--color-brand-white: #ffffff;
|
|
20
|
+
|
|
21
|
+
--color-brand-gray: #64748b;
|
|
22
|
+
--color-brand-grayLight: #e2e8f0;
|
|
23
|
+
--color-brand-grayDark: #334155;
|
|
24
|
+
|
|
25
|
+
--color-brand-background: #f8fafc;
|
|
26
|
+
--color-brand-border: #cbd5e1;
|
|
27
|
+
|
|
28
|
+
--color-brand-disabled: #94a3b8;
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import App from "./App.tsx";
|
|
4
|
+
import "./app.css";
|
|
5
|
+
|
|
6
|
+
const rootElement = document.getElementById("root");
|
|
7
|
+
if (!rootElement) throw new Error("Failed to find the root element");
|
|
8
|
+
|
|
9
|
+
createRoot(rootElement).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<App />
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Link } from "wouter";
|
|
2
|
+
|
|
3
|
+
const About = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
6
|
+
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
7
|
+
<h1 className="text-brand-black mb-6 text-5xl font-bold">
|
|
8
|
+
About This Project
|
|
9
|
+
</h1>
|
|
10
|
+
<p className="text-brand-gray mb-8 text-xl">
|
|
11
|
+
This is a starter template created with create-react-adam. It includes
|
|
12
|
+
everything you need to build modern React applications.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div className="mb-8 text-left">
|
|
16
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
17
|
+
Included Tools:
|
|
18
|
+
</h2>
|
|
19
|
+
<ul className="text-brand-gray space-y-2">
|
|
20
|
+
<li>āļø React 19 with TypeScript</li>
|
|
21
|
+
<li>ā” Vite for fast development</li>
|
|
22
|
+
<li>šØ Tailwind CSS for styling</li>
|
|
23
|
+
<li>š£ļø Wouter for lightweight routing</li>
|
|
24
|
+
<li>⨠ESLint & Prettier configured</li>
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<Link
|
|
29
|
+
href="/"
|
|
30
|
+
className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover rounded-lg px-6 py-3 transition-colors"
|
|
31
|
+
>
|
|
32
|
+
Back to Home
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default About;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Link } from "wouter";
|
|
2
|
+
import { useReactPersist } from "../../utils/Storage";
|
|
3
|
+
import { useUrlState } from "../../utils/useUrlState";
|
|
4
|
+
|
|
5
|
+
const About = () => {
|
|
6
|
+
const [sharedCount, setSharedCount] = useReactPersist("sharedCounter", 0);
|
|
7
|
+
const [urlCount, setUrlCount] = useUrlState("counter", 0);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
11
|
+
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
12
|
+
<h1 className="text-brand-black mb-6 text-5xl font-bold">
|
|
13
|
+
About This Project
|
|
14
|
+
</h1>
|
|
15
|
+
<p className="text-brand-gray mb-8 text-xl">
|
|
16
|
+
This is a starter template created with create-react-adam. It includes
|
|
17
|
+
everything you need to build modern React applications.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<div className="mb-8 space-y-6">
|
|
21
|
+
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
22
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
23
|
+
Shared Counter (localStorage)
|
|
24
|
+
</h2>
|
|
25
|
+
<p className="text-brand-gray mb-4">
|
|
26
|
+
This counter is shared between Home and About pages
|
|
27
|
+
</p>
|
|
28
|
+
<div className="flex items-center justify-center gap-4">
|
|
29
|
+
<button
|
|
30
|
+
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
|
+
>
|
|
33
|
+
-
|
|
34
|
+
</button>
|
|
35
|
+
<span className="text-brand-black text-3xl font-bold">
|
|
36
|
+
{sharedCount}
|
|
37
|
+
</span>
|
|
38
|
+
<button
|
|
39
|
+
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
|
+
>
|
|
42
|
+
+
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
48
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
49
|
+
URL Counter
|
|
50
|
+
</h2>
|
|
51
|
+
<p className="text-brand-gray mb-4">
|
|
52
|
+
This counter syncs with URL parameters
|
|
53
|
+
</p>
|
|
54
|
+
<div className="flex items-center justify-center gap-4">
|
|
55
|
+
<button
|
|
56
|
+
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
|
+
>
|
|
59
|
+
-
|
|
60
|
+
</button>
|
|
61
|
+
<span className="text-brand-black text-3xl font-bold">
|
|
62
|
+
{urlCount}
|
|
63
|
+
</span>
|
|
64
|
+
<button
|
|
65
|
+
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
|
+
>
|
|
68
|
+
+
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="mb-8 text-left">
|
|
75
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
76
|
+
Included Tools:
|
|
77
|
+
</h2>
|
|
78
|
+
<ul className="text-brand-gray space-y-2">
|
|
79
|
+
<li>āļø React 19 with TypeScript</li>
|
|
80
|
+
<li>ā” Vite for fast development</li>
|
|
81
|
+
<li>šØ Tailwind CSS for styling</li>
|
|
82
|
+
<li>š£ļø Wouter for lightweight routing</li>
|
|
83
|
+
<li>⨠ESLint & Prettier configured</li>
|
|
84
|
+
</ul>
|
|
85
|
+
</div>
|
|
86
|
+
<Link
|
|
87
|
+
href="/"
|
|
88
|
+
className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover rounded-lg px-6 py-3 transition-colors"
|
|
89
|
+
>
|
|
90
|
+
Back to Home
|
|
91
|
+
</Link>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default About;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Link } from "wouter";
|
|
2
|
+
|
|
3
|
+
const Home = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
6
|
+
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
7
|
+
<h1 className="text-brand-black mb-6 text-5xl font-bold">
|
|
8
|
+
Welcome to Your React App
|
|
9
|
+
</h1>
|
|
10
|
+
<p className="text-brand-gray mb-8 text-xl">
|
|
11
|
+
Built with React, TypeScript, Vite, Wouter, and Tailwind CSS
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<div className="mb-8">
|
|
15
|
+
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
16
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
17
|
+
Get Started
|
|
18
|
+
</h2>
|
|
19
|
+
<p className="text-brand-gray mb-4">
|
|
20
|
+
Edit{" "}
|
|
21
|
+
<code className="bg-brand-grayLight rounded px-2 py-1">
|
|
22
|
+
src/pages/Home/index.tsx
|
|
23
|
+
</code>{" "}
|
|
24
|
+
to customize this page.
|
|
25
|
+
</p>
|
|
26
|
+
<p className="text-brand-gray">
|
|
27
|
+
This is a clean starting point for your React application.
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="flex justify-center gap-4">
|
|
33
|
+
<Link
|
|
34
|
+
href="/about"
|
|
35
|
+
className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover rounded-lg px-6 py-3 transition-colors"
|
|
36
|
+
>
|
|
37
|
+
About Page
|
|
38
|
+
</Link>
|
|
39
|
+
<a
|
|
40
|
+
href="https://react.dev"
|
|
41
|
+
target="_blank"
|
|
42
|
+
rel="noopener noreferrer"
|
|
43
|
+
className="border-brand-border bg-brand-white text-brand-gray hover:bg-brand-grayLight rounded-lg border px-6 py-3 transition-colors"
|
|
44
|
+
>
|
|
45
|
+
Learn React
|
|
46
|
+
</a>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default Home;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Link } from "wouter";
|
|
2
|
+
import { useReactPersist } from "../../utils/Storage";
|
|
3
|
+
import { useUrlState } from "../../utils/useUrlState";
|
|
4
|
+
|
|
5
|
+
const Home = () => {
|
|
6
|
+
const [sharedCount, setSharedCount] = useReactPersist("sharedCounter", 0);
|
|
7
|
+
const [urlCount, setUrlCount] = useUrlState("counter", 0);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
11
|
+
<div className="mx-auto max-w-2xl px-4 text-center">
|
|
12
|
+
<h1 className="text-brand-black mb-6 text-5xl font-bold">
|
|
13
|
+
Welcome to Your React App
|
|
14
|
+
</h1>
|
|
15
|
+
<p className="text-brand-gray mb-8 text-xl">
|
|
16
|
+
Built with React, TypeScript, Vite, Wouter, and Tailwind CSS
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<div className="mb-8 space-y-6">
|
|
20
|
+
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
21
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
22
|
+
Shared Counter (localStorage)
|
|
23
|
+
</h2>
|
|
24
|
+
<p className="text-brand-gray mb-4">
|
|
25
|
+
This counter is shared between Home and About pages
|
|
26
|
+
</p>
|
|
27
|
+
<div className="flex items-center justify-center gap-4">
|
|
28
|
+
<button
|
|
29
|
+
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
|
+
>
|
|
32
|
+
-
|
|
33
|
+
</button>
|
|
34
|
+
<span className="text-brand-black text-3xl font-bold">
|
|
35
|
+
{sharedCount}
|
|
36
|
+
</span>
|
|
37
|
+
<button
|
|
38
|
+
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
|
+
>
|
|
41
|
+
+
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className="bg-brand-white rounded-lg p-6 shadow-md">
|
|
47
|
+
<h2 className="text-brand-black mb-4 text-2xl font-semibold">
|
|
48
|
+
URL Counter
|
|
49
|
+
</h2>
|
|
50
|
+
<p className="text-brand-gray mb-4">
|
|
51
|
+
This counter syncs with URL parameters
|
|
52
|
+
</p>
|
|
53
|
+
<div className="flex items-center justify-center gap-4">
|
|
54
|
+
<button
|
|
55
|
+
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
|
+
>
|
|
58
|
+
-
|
|
59
|
+
</button>
|
|
60
|
+
<span className="text-brand-black text-3xl font-bold">
|
|
61
|
+
{urlCount}
|
|
62
|
+
</span>
|
|
63
|
+
<button
|
|
64
|
+
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
|
+
>
|
|
67
|
+
+
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="flex justify-center gap-4">
|
|
74
|
+
<Link
|
|
75
|
+
href="/about"
|
|
76
|
+
className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover rounded-lg px-6 py-3 transition-colors"
|
|
77
|
+
>
|
|
78
|
+
About Page
|
|
79
|
+
</Link>
|
|
80
|
+
<a
|
|
81
|
+
href="https://react.dev"
|
|
82
|
+
target="_blank"
|
|
83
|
+
rel="noopener noreferrer"
|
|
84
|
+
className="border-brand-border bg-brand-white text-brand-gray hover:bg-brand-grayLight rounded-lg border px-6 py-3 transition-colors"
|
|
85
|
+
>
|
|
86
|
+
Learn React
|
|
87
|
+
</a>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default Home;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Link } from "wouter";
|
|
2
|
+
|
|
3
|
+
const NotFound = () => (
|
|
4
|
+
<div className="bg-brand-background flex min-h-screen flex-col items-center justify-center">
|
|
5
|
+
<div className="text-center">
|
|
6
|
+
<h1 className="text-brand-black text-9xl font-bold">404</h1>
|
|
7
|
+
<p className="text-brand-black mt-4 text-2xl font-semibold">
|
|
8
|
+
Page Not Found
|
|
9
|
+
</p>
|
|
10
|
+
<p className="text-brand-gray mt-2">
|
|
11
|
+
The page you're looking for doesn't exist.
|
|
12
|
+
</p>
|
|
13
|
+
<Link
|
|
14
|
+
href="/"
|
|
15
|
+
className="bg-brand-primary text-brand-white hover:bg-brand-primaryHover mt-6 inline-block rounded-lg px-6 py-3 transition-colors"
|
|
16
|
+
>
|
|
17
|
+
Go Home
|
|
18
|
+
</Link>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export default NotFound;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DependencyList, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function useInternetConnected(
|
|
4
|
+
callback: () => void,
|
|
5
|
+
dependencies: DependencyList = [],
|
|
6
|
+
) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (navigator.onLine) {
|
|
9
|
+
callback();
|
|
10
|
+
}
|
|
11
|
+
const handleOnline = () => {
|
|
12
|
+
callback();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
globalThis.addEventListener("online", handleOnline);
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
globalThis.removeEventListener("online", handleOnline);
|
|
19
|
+
};
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
}, dependencies);
|
|
22
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface StorageOptions {
|
|
4
|
+
secondsTillExpiry?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface StorageValue<T> {
|
|
8
|
+
value: T;
|
|
9
|
+
expiry?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class Storage {
|
|
13
|
+
load<T>(key: string, defaultValue: T): T {
|
|
14
|
+
try {
|
|
15
|
+
const item = localStorage.getItem(key);
|
|
16
|
+
if (!item) {
|
|
17
|
+
return defaultValue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parsed = JSON.parse(item) as StorageValue<T>;
|
|
21
|
+
|
|
22
|
+
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
23
|
+
localStorage.removeItem(key);
|
|
24
|
+
return defaultValue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return parsed.value;
|
|
28
|
+
} catch {
|
|
29
|
+
return defaultValue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
loadAsString(key: string): string | null {
|
|
34
|
+
return localStorage.getItem(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
save<T>(key: string, value: T, options?: StorageOptions): void {
|
|
38
|
+
try {
|
|
39
|
+
const storageValue: StorageValue<T> = {
|
|
40
|
+
value,
|
|
41
|
+
...(options?.secondsTillExpiry && {
|
|
42
|
+
expiry: Date.now() + options.secondsTillExpiry * 1000,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
localStorage.setItem(key, JSON.stringify(storageValue));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("Failed to save to localStorage:", error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
remove(key: string): void {
|
|
52
|
+
localStorage.removeItem(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clear(): void {
|
|
56
|
+
localStorage.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const storage = new Storage();
|
|
61
|
+
|
|
62
|
+
export function useReactPersist<T>(
|
|
63
|
+
key: string,
|
|
64
|
+
defaultValue: T,
|
|
65
|
+
options?: StorageOptions,
|
|
66
|
+
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
67
|
+
const [state, setState] = useState<T>(() => storage.load(key, defaultValue));
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
storage.save(key, state, options);
|
|
71
|
+
}, [key, state, options]);
|
|
72
|
+
|
|
73
|
+
const setPersistedState = useCallback((value: T | ((prev: T) => T)) => {
|
|
74
|
+
setState((prevState) => {
|
|
75
|
+
const newState =
|
|
76
|
+
typeof value === "function"
|
|
77
|
+
? (value as (prev: T) => T)(prevState)
|
|
78
|
+
: value;
|
|
79
|
+
return newState;
|
|
80
|
+
});
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
return [state, setPersistedState];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default storage;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useLocation, useSearch } from "wouter";
|
|
3
|
+
|
|
4
|
+
export function useUrlState(
|
|
5
|
+
key: string,
|
|
6
|
+
defaultValue: number,
|
|
7
|
+
): [number, (value: number) => void] {
|
|
8
|
+
const [, setLocation] = useLocation();
|
|
9
|
+
const searchParams = useSearch();
|
|
10
|
+
|
|
11
|
+
const params = new URLSearchParams(searchParams);
|
|
12
|
+
const urlValue = params.get(key);
|
|
13
|
+
const value = urlValue ? Number.parseInt(urlValue, 10) : defaultValue;
|
|
14
|
+
|
|
15
|
+
const setValue = useCallback(
|
|
16
|
+
(newValue: number) => {
|
|
17
|
+
const currentParams = new URLSearchParams(globalThis.location.search);
|
|
18
|
+
currentParams.set(key, newValue.toString());
|
|
19
|
+
setLocation(
|
|
20
|
+
`${globalThis.location.pathname}?${currentParams.toString()}`,
|
|
21
|
+
);
|
|
22
|
+
},
|
|
23
|
+
[key, setLocation],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return [value, setValue];
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedSideEffectImports": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-unresolved
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
// eslint-disable-next-line import/no-unresolved
|
|
4
|
+
import react from "@vitejs/plugin-react";
|
|
5
|
+
import { defineConfig } from "vite";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react(), tailwindcss()],
|
|
9
|
+
});
|