create-template-project 1.1.2 → 1.2.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 +11 -0
- package/dist/config/dependencies.json +15 -15
- package/dist/index.js +677 -483
- package/dist/templates/base/files/.release-it.json +1 -1
- package/dist/templates/base/files/_oxc.config.ts +212 -87
- package/dist/templates/cli/files/src/index.ts +1 -2
- package/dist/templates/cli/files/src/lib.ts +1 -3
- package/dist/templates/web-app/files/playwright.config.ts +7 -4
- package/dist/templates/web-app/files/src/App.tsx +11 -12
- package/dist/templates/web-app/files/src/index.tsx +1 -1
- package/dist/templates/web-app/files/tests/e2e/basic.e2e-test.ts +2 -2
- package/dist/templates/web-fullstack/files/client/src/App.tsx +8 -5
- package/dist/templates/web-fullstack/files/client/src/components/ProtectedRoute.tsx +5 -3
- package/dist/templates/web-fullstack/files/client/src/contexts/AuthContext.tsx +22 -18
- package/dist/templates/web-fullstack/files/client/src/main.tsx +8 -5
- package/dist/templates/web-fullstack/files/client/src/pages/Dashboard.tsx +12 -7
- package/dist/templates/web-fullstack/files/client/src/pages/Login.tsx +11 -7
- package/dist/templates/web-fullstack/files/client/src/trpc.ts +2 -1
- package/dist/templates/web-fullstack/files/playwright.config.ts +8 -5
- package/dist/templates/web-fullstack/files/server/src/context.ts +7 -5
- package/dist/templates/web-fullstack/files/server/src/index.ts +2 -3
- package/dist/templates/web-fullstack/files/server/src/routers/_app.ts +1 -0
- package/dist/templates/web-fullstack/files/server/src/routers/auth.ts +4 -4
- package/dist/templates/web-fullstack/files/server/src/trpc.ts +3 -4
- package/dist/templates/web-vanilla/files/playwright.config.ts +7 -4
- package/dist/templates/web-vanilla/files/src/index.test.ts +1 -1
- package/dist/templates/web-vanilla/files/src/index.ts +1 -1
- package/dist/templates/web-vanilla/files/src/lib.ts +1 -3
- package/package.json +13 -7
|
@@ -1,98 +1,223 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import {defineConfig} from 'oxlint';
|
|
2
|
+
import {configs as regexpConfigs} from 'eslint-plugin-regexp';
|
|
3
3
|
|
|
4
4
|
/** Filter out core ESLint rules bundled into eslint-plugin-regexp recommended config */
|
|
5
|
-
const regexpPluginRules = Object.fromEntries(Object.entries(
|
|
5
|
+
const regexpPluginRules = Object.fromEntries(Object.entries(regexpConfigs.recommended.rules).filter(([key]) => key.startsWith('regexp/')));
|
|
6
6
|
|
|
7
7
|
const commonIgnore = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
'**/.*',
|
|
9
|
+
'node_modules/**',
|
|
10
|
+
'dist/**',
|
|
11
|
+
'build/**',
|
|
12
|
+
'coverage/**',
|
|
13
|
+
'temp/**',
|
|
14
|
+
'public/**',
|
|
15
|
+
'**/*.md',
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
export const linter = defineConfig({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
19
|
+
options: {
|
|
20
|
+
typeAware: true,
|
|
21
|
+
typeCheck: true,
|
|
22
|
+
},
|
|
23
|
+
plugins: [
|
|
24
|
+
'unicorn',
|
|
25
|
+
'typescript',
|
|
26
|
+
'oxc',
|
|
27
|
+
'import',
|
|
28
|
+
'react',
|
|
29
|
+
'jsdoc',
|
|
30
|
+
'promise',
|
|
31
|
+
'vitest',
|
|
32
|
+
],
|
|
33
|
+
jsPlugins: ['eslint-plugin-regexp'],
|
|
34
|
+
categories: {
|
|
35
|
+
correctness: 'error',
|
|
36
|
+
nursery: 'error',
|
|
37
|
+
pedantic: 'error',
|
|
38
|
+
perf: 'error',
|
|
39
|
+
restriction: 'error',
|
|
40
|
+
style: 'error',
|
|
41
|
+
suspicious: 'error',
|
|
42
|
+
},
|
|
43
|
+
rules: {
|
|
44
|
+
...regexpPluginRules,
|
|
45
|
+
'eslint/complexity': 'off', // TODO: consider enabling
|
|
46
|
+
'eslint/curly': ['error', 'all'],
|
|
47
|
+
'eslint/id-length': 'off',
|
|
48
|
+
'eslint/init-declarations': 'off', // TODO: consider enabling
|
|
49
|
+
'eslint/max-depth': 'off', // TODO: consider enabling
|
|
50
|
+
'eslint/max-lines': 'off', // TODO: consider enabling
|
|
51
|
+
'eslint/max-lines-per-function': 'off', // TODO: consider enabling
|
|
52
|
+
'eslint/max-params': 'off', // TODO: consider enabling
|
|
53
|
+
'eslint/max-statements': 'off', // TODO: consider enabling
|
|
54
|
+
'eslint/capitalized-comments': 'off', // TODO: consider enabling
|
|
55
|
+
'eslint/no-await-in-loop': 'warn',
|
|
56
|
+
'eslint/no-console': 'off',
|
|
57
|
+
'eslint/no-continue': 'off',
|
|
58
|
+
'eslint/no-inline-comments': 'off',
|
|
59
|
+
'eslint/no-magic-numbers': 'off',
|
|
60
|
+
'eslint/no-negated-condition': 'off', // TODO: consider enabling
|
|
61
|
+
'eslint/no-nested-ternary': 'off',
|
|
62
|
+
'eslint/no-warning-comments': 'off',
|
|
63
|
+
'eslint/no-undefined': 'off', // TODO: consider enabling
|
|
64
|
+
'eslint/no-plusplus': 'off',
|
|
65
|
+
'eslint/sort-imports': 'off',
|
|
66
|
+
'eslint/sort-keys': 'off',
|
|
67
|
+
'eslint/no-ternary': 'off',
|
|
68
|
+
'typescript/no-unused-vars': [
|
|
69
|
+
'error',
|
|
70
|
+
{
|
|
71
|
+
caughtErrors: 'none',
|
|
72
|
+
argsIgnorePattern: '^_',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
'typescript/consistent-type-definitions': ['error', 'type'],
|
|
76
|
+
'typescript/no-import-type-side-effects': 'off',
|
|
77
|
+
'typescript/prefer-readonly-parameter-types': 'off',
|
|
78
|
+
'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
|
|
79
|
+
'import/exports-last': 'off',
|
|
80
|
+
'import/group-exports': 'off',
|
|
81
|
+
'import/max-dependencies': 'off',
|
|
82
|
+
'import/no-named-export': 'off',
|
|
83
|
+
'import/no-namespace': 'off', // TODO: consider enabling
|
|
84
|
+
'import/no-nodejs-modules': 'off',
|
|
85
|
+
'import/prefer-default-export': 'off',
|
|
86
|
+
'import/no-default-export': 'off',
|
|
87
|
+
// FIXME: remove all jest rules
|
|
88
|
+
'jest/consistent-test-it': 'off',
|
|
89
|
+
'jest/expect-expect': 'off',
|
|
90
|
+
'jest/max-expects': 'off',
|
|
91
|
+
'jest/max-nested-describe': 'off',
|
|
92
|
+
'jest/no-alias-methods': 'off',
|
|
93
|
+
'jest/no-commented-out-tests': 'off',
|
|
94
|
+
'jest/no-conditional-expect': 'off',
|
|
95
|
+
'jest/no-conditional-in-test': 'off',
|
|
96
|
+
'jest/no-confusing-set-timeout': 'off',
|
|
97
|
+
'jest/no-deprecated-functions': 'off',
|
|
98
|
+
'jest/no-disabled-tests': 'off',
|
|
99
|
+
'jest/no-done-callback': 'off',
|
|
100
|
+
'jest/no-duplicate-hooks': 'off',
|
|
101
|
+
'jest/no-export': 'off',
|
|
102
|
+
'jest/no-focused-tests': 'off',
|
|
103
|
+
'jest/no-hooks': 'off',
|
|
104
|
+
'jest/no-identical-title': 'off',
|
|
105
|
+
'jest/no-interpolation-in-snapshots': 'off',
|
|
106
|
+
'jest/no-jasmine-globals': 'off',
|
|
107
|
+
'jest/no-large-snapshots': 'off',
|
|
108
|
+
'jest/no-mocks-import': 'off',
|
|
109
|
+
'jest/no-restricted-jest-methods': 'off',
|
|
110
|
+
'jest/no-restricted-matchers': 'off',
|
|
111
|
+
'jest/no-standalone-expect': 'off',
|
|
112
|
+
'jest/no-test-prefixes': 'off',
|
|
113
|
+
'jest/no-test-return-statement': 'off',
|
|
114
|
+
'jest/no-unneeded-async-expect-function': 'off',
|
|
115
|
+
'jest/no-untyped-mock-factory': 'off',
|
|
116
|
+
//'jest/padding-around-after-all-blocks': 'off',
|
|
117
|
+
'jest/padding-around-test-blocks': 'off',
|
|
118
|
+
'jest/prefer-called-with': 'off',
|
|
119
|
+
'jest/prefer-comparison-matcher': 'off',
|
|
120
|
+
'jest/prefer-each': 'off',
|
|
121
|
+
'jest/prefer-equality-matcher': 'off',
|
|
122
|
+
'jest/prefer-expect-resolves': 'off',
|
|
123
|
+
'jest/prefer-hooks-in-order': 'off',
|
|
124
|
+
'jest/prefer-hooks-on-top': 'off',
|
|
125
|
+
'jest/prefer-jest-mocked': 'off',
|
|
126
|
+
'jest/prefer-lowercase-title': 'off',
|
|
127
|
+
'jest/prefer-mock-promise-shorthand': 'off',
|
|
128
|
+
'jest/prefer-mock-return-shorthand': 'off',
|
|
129
|
+
//'jest/prefer-snapshot-hint': 'off',
|
|
130
|
+
'jest/prefer-spy-on': 'off',
|
|
131
|
+
'jest/prefer-strict-equal': 'off',
|
|
132
|
+
'jest/prefer-to-be': 'off',
|
|
133
|
+
'jest/prefer-to-contain': 'off',
|
|
134
|
+
'jest/prefer-to-have-been-called': 'off',
|
|
135
|
+
'jest/prefer-to-have-been-called-times': 'off',
|
|
136
|
+
'jest/prefer-to-have-length': 'off',
|
|
137
|
+
'jest/prefer-todo': 'off',
|
|
138
|
+
'jest/require-hook': 'off',
|
|
139
|
+
'jest/require-to-throw-message': 'off',
|
|
140
|
+
'jest/require-top-level-describe': 'off',
|
|
141
|
+
'jest/valid-describe-callback': 'off',
|
|
142
|
+
'jest/valid-expect': 'off',
|
|
143
|
+
'jest/valid-title': 'off',
|
|
144
|
+
'oxc/no-async-await': 'off',
|
|
145
|
+
'oxc/no-map-spread': 'off', // TODO: consider enabling
|
|
146
|
+
'oxc/no-rest-spread-properties': 'off',
|
|
147
|
+
'unicorn/escape-case': 'off',
|
|
148
|
+
'unicorn/filename-case': 'off', // TODO: consider enabling
|
|
149
|
+
'unicorn/no-array-reduce': 'off', // TODO: consider enabling
|
|
150
|
+
'unicorn/no-array-sort': 'off', // TODO: consider enabling
|
|
151
|
+
'unicorn/no-hex-escape': 'off',
|
|
152
|
+
'unicorn/no-immediate-mutation': 'off',
|
|
153
|
+
'unicorn/no-nested-ternary': 'off',
|
|
154
|
+
'unicorn/no-null': 'off', // TODO: consider enabling
|
|
155
|
+
'unicorn/no-process-exit': 'off', // TODO: consider enabling
|
|
156
|
+
'unicorn/no-typeof-undefined': 'off', // TODO: consider enabling
|
|
157
|
+
'unicorn/prefer-module': 'off', // TODO: consider enabling
|
|
158
|
+
'react/jsx-filename-extension': 'off',
|
|
159
|
+
'react/react-in-jsx-scope': 'off',
|
|
160
|
+
'vitest/no-importing-vitest-globals': 'off',
|
|
161
|
+
'vitest/prefer-describe-function-title': 'off',
|
|
162
|
+
'vitest/prefer-to-be-truthy': 'off', // FIXME: Conflict Detected: prefer-strict-boolean-matchers enforces toBe(true), but prefer-to-be-truthy enforces toBeTruthy().
|
|
163
|
+
'vitest/require-test-timeout': 'off',
|
|
164
|
+
},
|
|
165
|
+
overrides: [
|
|
166
|
+
{
|
|
167
|
+
files: ['tests/e2e/**/*.e2e-test.ts', '**/*.e2e-test.ts'],
|
|
168
|
+
rules: {
|
|
169
|
+
'vitest/prefer-importing-vitest-globals': 'off',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
settings: {
|
|
174
|
+
'jsx-a11y': {
|
|
175
|
+
polymorphicPropName: undefined,
|
|
176
|
+
components: {},
|
|
177
|
+
attributes: {},
|
|
178
|
+
},
|
|
179
|
+
next: {
|
|
180
|
+
rootDir: [],
|
|
181
|
+
},
|
|
182
|
+
react: {
|
|
183
|
+
formComponents: [],
|
|
184
|
+
linkComponents: [],
|
|
185
|
+
version: undefined,
|
|
186
|
+
},
|
|
187
|
+
jsdoc: {
|
|
188
|
+
ignorePrivate: false,
|
|
189
|
+
ignoreInternal: false,
|
|
190
|
+
ignoreReplacesDocs: true,
|
|
191
|
+
overrideReplacesDocs: true,
|
|
192
|
+
augmentsExtendsReplacesDocs: false,
|
|
193
|
+
implementsReplacesDocs: false,
|
|
194
|
+
exemptDestructuredRootsFromChecks: false,
|
|
195
|
+
tagNamePreference: {},
|
|
196
|
+
},
|
|
197
|
+
vitest: {
|
|
198
|
+
typecheck: false,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
env: {
|
|
202
|
+
builtin: true,
|
|
203
|
+
},
|
|
204
|
+
globals: {},
|
|
205
|
+
ignorePatterns: commonIgnore,
|
|
81
206
|
});
|
|
82
207
|
|
|
83
208
|
export const formatter = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
209
|
+
printWidth: 160,
|
|
210
|
+
embeddedLanguageFormatting: 'off',
|
|
211
|
+
useTabs: true,
|
|
212
|
+
singleQuote: true,
|
|
213
|
+
bracketSpacing: false,
|
|
214
|
+
ignorePatterns: commonIgnore,
|
|
215
|
+
overrides: [
|
|
216
|
+
{
|
|
217
|
+
files: ['src/**/*.{scss,css}'],
|
|
218
|
+
options: {
|
|
219
|
+
singleQuote: false,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
],
|
|
98
223
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {Command} from 'commander';
|
|
3
3
|
import {SingleBar, Presets} from 'cli-progress';
|
|
4
|
+
import {greet} from './lib.js';
|
|
4
5
|
|
|
5
6
|
const program = new Command();
|
|
6
7
|
program.name('my-cli').description('A sample CLI');
|
|
@@ -11,6 +12,4 @@ bar.start(100, 0);
|
|
|
11
12
|
bar.update(50);
|
|
12
13
|
bar.stop();
|
|
13
14
|
|
|
14
|
-
import {greet} from './lib.js';
|
|
15
|
-
|
|
16
15
|
console.log(greet('Developer'));
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export const greet = (name: string): string => {
|
|
2
|
-
return `Hello, ${name}! Welcome to your new CLI.`;
|
|
3
|
-
};
|
|
1
|
+
export const greet = (name: string): string => `Hello, ${name}! Welcome to your new CLI.`;
|
|
4
2
|
|
|
5
3
|
export const calculateProgress = (current: number, total: number): number => {
|
|
6
4
|
if (total === 0) {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {defineConfig, devices} from '@playwright/test';
|
|
2
|
+
import {env} from 'node:process';
|
|
3
|
+
|
|
4
|
+
const isCi = env.CI !== undefined;
|
|
2
5
|
|
|
3
6
|
export default defineConfig({
|
|
4
7
|
testDir: './tests/e2e',
|
|
5
8
|
testMatch: '**/*.e2e-test.ts',
|
|
6
9
|
fullyParallel: true,
|
|
7
|
-
forbidOnly:
|
|
8
|
-
retries:
|
|
9
|
-
workers:
|
|
10
|
+
forbidOnly: isCi,
|
|
11
|
+
retries: isCi ? 2 : 0,
|
|
12
|
+
workers: isCi ? 1 : undefined,
|
|
10
13
|
reporter: 'html',
|
|
11
14
|
use: {
|
|
12
15
|
baseURL: 'http://localhost:3000',
|
|
@@ -21,6 +24,6 @@ export default defineConfig({
|
|
|
21
24
|
webServer: {
|
|
22
25
|
command: 'npm run build && npm run start',
|
|
23
26
|
url: 'http://localhost:3000',
|
|
24
|
-
reuseExistingServer: !
|
|
27
|
+
reuseExistingServer: !isCi,
|
|
25
28
|
},
|
|
26
29
|
});
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import {Typography, Container, Box} from '@mui/material';
|
|
2
|
+
import {type ReactElement} from 'react';
|
|
2
3
|
|
|
3
|
-
export const App = () =>
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
);
|
|
14
|
-
};
|
|
4
|
+
export const App = (): ReactElement => (
|
|
5
|
+
<Container maxWidth="sm">
|
|
6
|
+
<Box sx={{my: 4, textAlign: 'center'}}>
|
|
7
|
+
<Typography variant="h4" component="h1" gutterBottom>
|
|
8
|
+
Hello from React!
|
|
9
|
+
</Typography>
|
|
10
|
+
<Typography variant="body1">This project was scaffolded with the web-app template.</Typography>
|
|
11
|
+
</Box>
|
|
12
|
+
</Container>
|
|
13
|
+
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createRoot} from 'react-dom/client';
|
|
2
2
|
import {App} from './App.js';
|
|
3
3
|
|
|
4
|
-
const container = document.
|
|
4
|
+
const container = document.querySelector<HTMLElement>('#app');
|
|
5
5
|
if (container) {
|
|
6
6
|
const root = createRoot(container);
|
|
7
7
|
root.render(<App />);
|
|
@@ -8,6 +8,6 @@ test('has title', async ({page}) => {
|
|
|
8
8
|
test('api is reachable', async ({page}) => {
|
|
9
9
|
const response = await page.request.get('/api/hello');
|
|
10
10
|
expect(response.ok()).toBeTruthy();
|
|
11
|
-
const data = await response.json();
|
|
12
|
-
expect(data
|
|
11
|
+
const data: unknown = await response.json();
|
|
12
|
+
expect(data).toMatchObject({message: 'Hello from Express!'});
|
|
13
13
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable react/jsx-max-depth */
|
|
2
|
+
import {useMemo, type ReactNode} from 'react';
|
|
2
3
|
import {BrowserRouter, Routes, Route, Navigate} from 'react-router-dom';
|
|
3
4
|
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
4
5
|
import {httpBatchLink} from '@trpc/client';
|
|
@@ -11,20 +12,22 @@ import {CssBaseline, ThemeProvider, createTheme} from '@mui/material';
|
|
|
11
12
|
|
|
12
13
|
const theme = createTheme();
|
|
13
14
|
|
|
14
|
-
export const App = () => {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
export const App = (): ReactNode => {
|
|
16
|
+
const queryClient = useMemo(() => new QueryClient(), []);
|
|
17
|
+
const trpcClient = useMemo(
|
|
18
|
+
() =>
|
|
17
19
|
trpc.createClient({
|
|
18
20
|
links: [
|
|
19
21
|
httpBatchLink({
|
|
20
22
|
url: 'http://localhost:3001/trpc',
|
|
21
23
|
headers: () => {
|
|
22
24
|
const token = localStorage.getItem('token');
|
|
23
|
-
return token ? {Authorization: `Bearer ${token}`} : {};
|
|
25
|
+
return token !== null && token !== '' ? {Authorization: `Bearer ${token}`} : {};
|
|
24
26
|
},
|
|
25
27
|
}),
|
|
26
28
|
],
|
|
27
29
|
}),
|
|
30
|
+
[],
|
|
28
31
|
);
|
|
29
32
|
|
|
30
33
|
return (
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
/* eslint-disable import/no-relative-parent-imports */
|
|
1
2
|
import {Navigate, Outlet} from 'react-router-dom';
|
|
3
|
+
import {type ReactNode} from 'react';
|
|
2
4
|
import {useAuth} from '../contexts/AuthContext.js';
|
|
3
5
|
import {CircularProgress, Box} from '@mui/material';
|
|
4
6
|
|
|
5
|
-
export const ProtectedRoute = () => {
|
|
7
|
+
export const ProtectedRoute = (): ReactNode => {
|
|
6
8
|
const {token, isLoading} = useAuth();
|
|
7
9
|
|
|
8
10
|
if (isLoading) {
|
|
9
11
|
return (
|
|
10
|
-
<Box display
|
|
12
|
+
<Box sx={{display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh'}}>
|
|
11
13
|
<CircularProgress />
|
|
12
14
|
</Box>
|
|
13
15
|
);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
if (
|
|
18
|
+
if (token === null || token === '') {
|
|
17
19
|
return <Navigate to="/login" replace />;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -1,56 +1,60 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable import/no-relative-parent-imports */
|
|
2
|
+
/* eslint-disable react/only-export-components */
|
|
3
|
+
/* eslint-disable react/jsx-no-constructed-context-values */
|
|
4
|
+
/* eslint-disable typescript/no-unnecessary-condition */
|
|
5
|
+
import {createContext, useContext, useState, useEffect, type ReactNode} from 'react';
|
|
2
6
|
import {trpc} from '../trpc.js';
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
type User = {
|
|
5
9
|
id: string;
|
|
6
10
|
name: string;
|
|
7
11
|
email: string;
|
|
8
|
-
}
|
|
12
|
+
};
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
type AuthContextType = {
|
|
11
15
|
user: User | null;
|
|
12
16
|
token: string | null;
|
|
13
17
|
isLoading: boolean;
|
|
14
18
|
login: (token: string, user: User) => void;
|
|
15
19
|
logout: () => void;
|
|
16
|
-
}
|
|
20
|
+
};
|
|
17
21
|
|
|
18
22
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
19
23
|
|
|
20
|
-
export const AuthProvider = ({children}: {children: ReactNode}) => {
|
|
21
|
-
const [token, setToken] = useState
|
|
24
|
+
export const AuthProvider = ({children}: {children: ReactNode}): ReactNode => {
|
|
25
|
+
const [token, setToken] = useState(localStorage.getItem('token'));
|
|
22
26
|
const [user, setUser] = useState<User | null>(null);
|
|
23
27
|
|
|
24
28
|
const {data: me, isLoading} = trpc.auth.me.useQuery(undefined, {
|
|
25
|
-
enabled:
|
|
29
|
+
enabled: Boolean(token),
|
|
26
30
|
retry: false,
|
|
27
31
|
});
|
|
28
32
|
|
|
33
|
+
const logout = (): void => {
|
|
34
|
+
localStorage.removeItem('token');
|
|
35
|
+
setToken(null);
|
|
36
|
+
setUser(null);
|
|
37
|
+
};
|
|
38
|
+
|
|
29
39
|
useEffect(() => {
|
|
30
|
-
if (me) {
|
|
40
|
+
if (me !== undefined && me !== null) {
|
|
31
41
|
setUser(me);
|
|
32
|
-
} else if (!isLoading && token) {
|
|
42
|
+
} else if (!isLoading && token !== null && token !== '') {
|
|
33
43
|
// Token might be invalid
|
|
34
44
|
logout();
|
|
35
45
|
}
|
|
36
46
|
}, [me, isLoading, token]);
|
|
37
47
|
|
|
38
|
-
const login = (newToken: string, newUser: User) => {
|
|
48
|
+
const login = (newToken: string, newUser: User): void => {
|
|
39
49
|
localStorage.setItem('token', newToken);
|
|
40
50
|
setToken(newToken);
|
|
41
51
|
setUser(newUser);
|
|
42
52
|
};
|
|
43
53
|
|
|
44
|
-
const logout = () => {
|
|
45
|
-
localStorage.removeItem('token');
|
|
46
|
-
setToken(null);
|
|
47
|
-
setUser(null);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
54
|
return <AuthContext.Provider value={{user, token, isLoading, login, logout}}>{children}</AuthContext.Provider>;
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
export const useAuth = () => {
|
|
57
|
+
export const useAuth = (): AuthContextType => {
|
|
54
58
|
const context = useContext(AuthContext);
|
|
55
59
|
if (!context) {
|
|
56
60
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
@@ -2,8 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
3
|
import {App} from './App.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const rootElement = document.querySelector('#root');
|
|
6
|
+
if (rootElement !== null) {
|
|
7
|
+
ReactDOM.createRoot(rootElement).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</React.StrictMode>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -1,29 +1,34 @@
|
|
|
1
|
+
/* eslint-disable react/jsx-max-depth */
|
|
2
|
+
/* eslint-disable import/no-relative-parent-imports */
|
|
3
|
+
/* eslint-disable eslint/no-void */
|
|
4
|
+
/* eslint-disable oxc/no-optional-chaining */
|
|
1
5
|
import {Container, Typography, Button, Paper, Box} from '@mui/material';
|
|
6
|
+
import {type ReactNode} from 'react';
|
|
2
7
|
import {useAuth} from '../contexts/AuthContext.js';
|
|
3
8
|
import {useNavigate} from 'react-router-dom';
|
|
4
9
|
|
|
5
|
-
export const Dashboard = () => {
|
|
10
|
+
export const Dashboard = (): ReactNode => {
|
|
6
11
|
const {user, logout} = useAuth();
|
|
7
12
|
const navigate = useNavigate();
|
|
8
13
|
|
|
9
|
-
const handleLogout = () => {
|
|
14
|
+
const handleLogout = (): void => {
|
|
10
15
|
logout();
|
|
11
16
|
void navigate('/login');
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
return (
|
|
15
20
|
<Container maxWidth="md">
|
|
16
|
-
<Box
|
|
21
|
+
<Box sx={{mt: 4}}>
|
|
17
22
|
<Paper elevation={3}>
|
|
18
|
-
<Box
|
|
23
|
+
<Box sx={{p: 4}}>
|
|
19
24
|
<Typography variant="h4" gutterBottom>
|
|
20
25
|
Dashboard
|
|
21
26
|
</Typography>
|
|
22
|
-
<Typography variant="h6">Welcome, {user?.name}!</Typography>
|
|
27
|
+
<Typography variant="h6">Welcome, {user?.name ?? ''}!</Typography>
|
|
23
28
|
<Typography variant="body1" color="textSecondary" gutterBottom>
|
|
24
|
-
Email: {user?.email}
|
|
29
|
+
Email: {user?.email ?? ''}
|
|
25
30
|
</Typography>
|
|
26
|
-
<Box
|
|
31
|
+
<Box sx={{mt: 4}}>
|
|
27
32
|
<Typography variant="body2" gutterBottom>
|
|
28
33
|
This is a protected page. You can only see this because you are logged in.
|
|
29
34
|
</Typography>
|