generator-kodly-react-app 1.0.16 → 1.0.17
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/generators/app/index.js +11 -1
- package/generators/app/templates/.env.sandbox +4 -3
- package/generators/app/templates/.node-version +1 -0
- package/generators/app/templates/.pnpmfile.cjs +14 -0
- package/generators/app/templates/.prettierignore +13 -0
- package/generators/app/templates/AGENTS.md +14 -0
- package/generators/app/templates/TESTING.md +30 -0
- package/generators/app/templates/openapi-ts.config.ts +5 -1
- package/generators/app/templates/oxlint.config.ts +72 -0
- package/generators/app/templates/package.json +66 -52
- package/generators/app/templates/pnpm-workspace.yaml +19 -0
- package/generators/app/templates/scripts/check-duplicates.mjs +28 -0
- package/generators/app/templates/scripts/generated-files.mjs +21 -0
- package/generators/app/templates/src/components/layout/language-switcher.tsx +1 -1
- package/generators/app/templates/src/components/layout/theme-switcher.tsx +1 -1
- package/generators/app/templates/src/components/ui/input-group.tsx +0 -6
- package/generators/app/templates/src/index.css +70 -70
- package/generators/app/templates/src/lib/i18n.ts +26 -19
- package/generators/app/templates/src/modules/auth/components/auth-form.tsx +4 -1
- package/generators/app/templates/src/modules/auth/hooks/use-auth-hook.ts +3 -3
- package/generators/app/templates/src/modules/landing/components/auth-hero.tsx +2 -2
- package/generators/app/templates/src/modules/landing/components/welcome-hero.tsx +1 -1
- package/generators/app/templates/src/router.tsx +2 -2
- package/generators/app/templates/src/routes/__root.tsx +1 -1
- package/generators/app/templates/src/routes/auth/route.tsx +4 -1
- package/generators/app/templates/src/sdk/client.gen.ts +1 -5
- package/generators/app/templates/src/sdk/types.gen.ts +1 -1
- package/generators/app/templates/stylelint.config.js +55 -0
- package/generators/app/templates/tests/form-controls.test.tsx +39 -0
- package/generators/app/templates/tests/sdk.test.ts +49 -0
- package/generators/app/templates/tests/setup.ts +5 -0
- package/generators/app/templates/tsconfig.app.json +0 -1
- package/generators/app/templates/tsconfig.json +2 -1
- package/generators/app/templates/vite.config.js +5 -7
- package/generators/app/templates/vitest.config.ts +14 -0
- package/package.json +10 -9
- package/generators/app/templates/postcss.config.js +0 -5
package/generators/app/index.js
CHANGED
|
@@ -16,17 +16,27 @@ export default class ReactAppGenerator extends BaseGenerator {
|
|
|
16
16
|
this._writeLandingFiles();
|
|
17
17
|
this._writeAppModuleFiles();
|
|
18
18
|
this._writeSdkFiles();
|
|
19
|
+
this._writeDir('scripts');
|
|
20
|
+
this._writeDir('tests');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
_writeConfigFiles() {
|
|
22
24
|
const configFiles = [
|
|
23
25
|
'package.json',
|
|
26
|
+
'oxlint.config.ts',
|
|
27
|
+
'stylelint.config.js',
|
|
28
|
+
'vitest.config.ts',
|
|
29
|
+
'.node-version',
|
|
30
|
+
'pnpm-workspace.yaml',
|
|
31
|
+
'.pnpmfile.cjs',
|
|
32
|
+
'AGENTS.md',
|
|
33
|
+
'.prettierignore',
|
|
34
|
+
'TESTING.md',
|
|
24
35
|
'vite.config.js',
|
|
25
36
|
'tsconfig.json',
|
|
26
37
|
'tsconfig.app.json',
|
|
27
38
|
'tsr.config.json',
|
|
28
39
|
'tailwind.config.js',
|
|
29
|
-
'postcss.config.js',
|
|
30
40
|
'index.html',
|
|
31
41
|
'components.json',
|
|
32
42
|
'openapi-ts.config.ts',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
BASE_URL=/
|
|
2
|
-
PORT=
|
|
3
|
-
ALLOWED_HOSTS=
|
|
4
|
-
|
|
2
|
+
PORT=5173
|
|
3
|
+
ALLOWED_HOSTS=localhost
|
|
4
|
+
# Kodly injects the connected API URL and preview host at server startup.
|
|
5
|
+
VITE_API_BASE_URL=
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
26.12.1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Hey API uses the JavaScript compiler API, which TypeScript 7 no longer ships.
|
|
2
|
+
// Keep its compiler private; the application and type-aware lint use TypeScript 7.
|
|
3
|
+
module.exports = {
|
|
4
|
+
hooks: {
|
|
5
|
+
/** @param {{name?: string, version?: string, dependencies?: Record<string, string>, peerDependencies?: Record<string, string>}} pkg */
|
|
6
|
+
readPackage(pkg) {
|
|
7
|
+
if (pkg.name === "@hey-api/openapi-ts" && pkg.version === "0.99.0") {
|
|
8
|
+
if (pkg.peerDependencies) delete pkg.peerDependencies.typescript;
|
|
9
|
+
pkg.dependencies = { ...pkg.dependencies, typescript: "6.0.3" };
|
|
10
|
+
}
|
|
11
|
+
return pkg;
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Working in this project
|
|
2
|
+
|
|
3
|
+
Run `pnpm check` before handing off a change. It runs the production build and
|
|
4
|
+
type checker, Oxlint with TypeScript 7 type-aware rules, shadcn design rules,
|
|
5
|
+
Stylelint, Prettier, duplication detection and Vitest. Fix failures at their source.
|
|
6
|
+
|
|
7
|
+
Use `unknown` and validation at data boundaries instead of `any`. Handle promises.
|
|
8
|
+
Reuse the components in `src/components/ui` and the theme tokens in `src/index.css`.
|
|
9
|
+
Add named component variants for reusable styles. Avoid raw colours, inline styles
|
|
10
|
+
and arbitrary Tailwind values in screens. Add behaviour tests for changed features.
|
|
11
|
+
|
|
12
|
+
API transport is generated by `pnpm openapi-ts:dev` or `pnpm openapi-ts:sandbox`.
|
|
13
|
+
Keep handwritten code outside generated files. Do not weaken checks or turn off
|
|
14
|
+
tests to get a green build. All verification configuration belongs to this repo.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Project checks
|
|
2
|
+
|
|
3
|
+
Install the declared package manager and run `pnpm install` once to create the lockfile.
|
|
4
|
+
Commit that lockfile; subsequent local and CI installs use `pnpm install --frozen-lockfile`.
|
|
5
|
+
|
|
6
|
+
`pnpm check` runs the build/type check, type-aware Oxlint, shadcn design rules, Stylelint, Prettier, duplicate detection and Vitest tests.
|
|
7
|
+
Each step is also a package script and fails with its original exit code. All tool
|
|
8
|
+
versions, rules and tests are owned by this repository and run without Kodly.
|
|
9
|
+
|
|
10
|
+
- `pnpm lint` checks TypeScript 7 types, unsafe `any`, promises, React Hooks, accessibility and shadcn design conventions.
|
|
11
|
+
- `pnpm lint:styles` checks CSS and requires tokens for colours outside theme token definitions.
|
|
12
|
+
- `pnpm format:check` checks formatting; `pnpm format` applies it explicitly.
|
|
13
|
+
- `pnpm check:duplicates` checks handwritten source at a 0.5% threshold with 50-token blocks.
|
|
14
|
+
- `pnpm test` runs unit and component tests in `tests/` and `src/` once; it fails if no tests exist. Use `pnpm test:watch` while developing.
|
|
15
|
+
- `pnpm check` runs every gate. A failure stops the command with a nonzero exit code.
|
|
16
|
+
|
|
17
|
+
The SDK generator invokes the installed formatter. `scripts/generated-files.mjs`
|
|
18
|
+
identifies generated transport by both its filename and Hey API banner. Only those
|
|
19
|
+
files receive the transport-specific lint allowances and duplication exclusion;
|
|
20
|
+
handwritten wrappers remain checked. Compilation and formatting still cover generated code.
|
|
21
|
+
Use the existing OpenAPI configuration when the backend changes.
|
|
22
|
+
|
|
23
|
+
Browser/API acceptance suites can be added with project-local dependencies and
|
|
24
|
+
configuration when needed. Save their test cases in source so they also run in CI;
|
|
25
|
+
do not import test tools from a Kodly scratch directory.
|
|
26
|
+
|
|
27
|
+
The app and type-aware linter use TypeScript 7. The pinned Hey API generator still
|
|
28
|
+
uses the older JavaScript compiler API internally. `.pnpmfile.cjs` gives only that
|
|
29
|
+
package a private TypeScript 6.0.3 dependency; it never changes the app compiler.
|
|
30
|
+
Remove this scoped hook once Hey API supports the TypeScript 7 compiler API.
|
|
@@ -9,10 +9,14 @@ const env = loadEnv(mode, process.cwd(), "");
|
|
|
9
9
|
|
|
10
10
|
export default defineConfig({
|
|
11
11
|
input: `${env.VITE_API_BASE_URL}/v3/api-docs`,
|
|
12
|
-
output:
|
|
12
|
+
output: {
|
|
13
|
+
path: "src/sdk",
|
|
14
|
+
postProcess: ["prettier"],
|
|
15
|
+
},
|
|
13
16
|
plugins: [
|
|
14
17
|
{
|
|
15
18
|
name: "@hey-api/client-axios",
|
|
19
|
+
baseUrl: false,
|
|
16
20
|
runtimeConfigPath: "@/openapi-client-config.ts",
|
|
17
21
|
},
|
|
18
22
|
"@tanstack/react-query",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineConfig } from "oxlint";
|
|
2
|
+
import { generatedFiles } from "./scripts/generated-files.mjs";
|
|
3
|
+
|
|
4
|
+
// TypeScript 7 uses the native type checker; tsgolint shares that compiler.
|
|
5
|
+
const typeRules = {
|
|
6
|
+
"typescript/no-explicit-any": "error",
|
|
7
|
+
"typescript/no-unsafe-assignment": "error",
|
|
8
|
+
"typescript/no-unsafe-argument": "error",
|
|
9
|
+
"typescript/no-unsafe-call": "error",
|
|
10
|
+
"typescript/no-unsafe-member-access": "error",
|
|
11
|
+
"typescript/no-unsafe-return": "error",
|
|
12
|
+
"typescript/no-floating-promises": "error",
|
|
13
|
+
"typescript/no-misused-promises": "error",
|
|
14
|
+
"typescript/no-non-null-assertion": "error",
|
|
15
|
+
"typescript/switch-exhaustiveness-check": "error",
|
|
16
|
+
} as const;
|
|
17
|
+
const generated = generatedFiles();
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
plugins: ["typescript", "react", "jsx-a11y", "import"],
|
|
21
|
+
jsPlugins: ["@shadcn/lint"],
|
|
22
|
+
categories: { correctness: "error" },
|
|
23
|
+
ignorePatterns: [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
"coverage",
|
|
28
|
+
".tanstack",
|
|
29
|
+
".kodly*",
|
|
30
|
+
],
|
|
31
|
+
env: { browser: true, node: true },
|
|
32
|
+
rules: {
|
|
33
|
+
...typeRules,
|
|
34
|
+
"jsx-a11y/prefer-tag-over-role": "off",
|
|
35
|
+
"react/rules-of-hooks": "error",
|
|
36
|
+
"react/exhaustive-deps": "error",
|
|
37
|
+
"shadcn/no-restyle": ["error", { allow: ["layout"] }],
|
|
38
|
+
"shadcn/no-raw-colors": "error",
|
|
39
|
+
"shadcn/no-arbitrary-values": "error",
|
|
40
|
+
"shadcn/no-inline-styles": "error",
|
|
41
|
+
"shadcn/no-unknown-classes": "error",
|
|
42
|
+
"shadcn/require-static-classes": "error",
|
|
43
|
+
},
|
|
44
|
+
overrides: [
|
|
45
|
+
{
|
|
46
|
+
files: ["src/components/ui/**"],
|
|
47
|
+
rules: {
|
|
48
|
+
"shadcn/no-restyle": "off",
|
|
49
|
+
"shadcn/no-arbitrary-values": "off",
|
|
50
|
+
"shadcn/require-static-classes": "off",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
// Only verified generator output gets type-rule exceptions. Handwritten SDK
|
|
54
|
+
// wrappers remain checked; parsing and the full TypeScript build cover both.
|
|
55
|
+
...(generated.length
|
|
56
|
+
? [
|
|
57
|
+
{
|
|
58
|
+
files: generated,
|
|
59
|
+
rules: {
|
|
60
|
+
...Object.fromEntries(
|
|
61
|
+
Object.keys(typeRules).map((rule) => [rule, "off"] as const),
|
|
62
|
+
),
|
|
63
|
+
"typescript/no-redundant-type-constituents": "off" as const,
|
|
64
|
+
"typescript/no-base-to-string": "off" as const,
|
|
65
|
+
"typescript/restrict-template-expressions": "off" as const,
|
|
66
|
+
"typescript/no-misused-spread": "off" as const,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
],
|
|
72
|
+
});
|
|
@@ -3,65 +3,79 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"version": "0.0.1",
|
|
6
|
-
"packageManager": "pnpm@
|
|
6
|
+
"packageManager": "pnpm@12.14.0",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"vite:sandbox": "vite --mode sandbox",
|
|
13
|
-
"dev": "npm-run-all --parallel watch-routes vite:dev",
|
|
14
|
-
"dev:sandbox": "npm-run-all --parallel watch-routes vite:sandbox",
|
|
15
|
-
"build": "pnpm run generate-routes && vite build --mode dev && tsc",
|
|
16
|
-
"build:sandbox": "pnpm run generate-routes && vite build --mode sandbox && tsc",
|
|
8
|
+
"dev": "vite --mode dev",
|
|
9
|
+
"dev:sandbox": "vite --mode sandbox",
|
|
10
|
+
"build": "vite build --mode dev && tsc --noEmit",
|
|
11
|
+
"build:sandbox": "vite build --mode sandbox && tsc --noEmit",
|
|
17
12
|
"serve": "vite preview",
|
|
18
13
|
"openapi-ts:dev": "MODE=dev openapi-ts",
|
|
19
|
-
"openapi-ts:sandbox": "MODE=sandbox openapi-ts"
|
|
14
|
+
"openapi-ts:sandbox": "MODE=sandbox openapi-ts",
|
|
15
|
+
"check": "pnpm run build && pnpm run lint && pnpm run lint:styles && pnpm run format:check && pnpm run check:duplicates && pnpm test",
|
|
16
|
+
"lint": "oxlint --type-aware --deny-warnings",
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"format:check": "prettier --check .",
|
|
19
|
+
"check:duplicates": "node scripts/check-duplicates.mjs",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"lint:styles": "stylelint \"src/**/*.css\" --max-warnings 0",
|
|
22
|
+
"test:watch": "vitest"
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"@
|
|
24
|
-
"@radix-ui/react-
|
|
25
|
-
"@radix-ui/react-
|
|
26
|
-
"@radix-ui/react-
|
|
27
|
-
"@
|
|
28
|
-
"@tanstack/react-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"react": "
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"tailwind-merge": "^3.4.0",
|
|
44
|
-
"tailwindcss-animate": "^1.0.7",
|
|
45
|
-
"tw-animate-css": "^1.4.0",
|
|
46
|
-
"zod": "^4.3.6"
|
|
25
|
+
"@hookform/resolvers": "6.0.0",
|
|
26
|
+
"@radix-ui/react-label": "2.1.17",
|
|
27
|
+
"@radix-ui/react-select": "2.4.1",
|
|
28
|
+
"@radix-ui/react-separator": "1.2.0",
|
|
29
|
+
"@radix-ui/react-slot": "1.5.0",
|
|
30
|
+
"@tanstack/react-query": "5.104.5",
|
|
31
|
+
"@tanstack/react-router": "1.170.45",
|
|
32
|
+
"axios": "1.20.0",
|
|
33
|
+
"class-variance-authority": "0.7.1",
|
|
34
|
+
"clsx": "2.1.1",
|
|
35
|
+
"i18next": "26.5.4",
|
|
36
|
+
"jotai": "3.0.1",
|
|
37
|
+
"lucide-react": "1.61.0",
|
|
38
|
+
"react": "19.3.1",
|
|
39
|
+
"react-dom": "19.3.1",
|
|
40
|
+
"react-hook-form": "7.90.0",
|
|
41
|
+
"react-i18next": "17.1.0",
|
|
42
|
+
"sonner": "2.0.8",
|
|
43
|
+
"tailwind-merge": "3.7.0",
|
|
44
|
+
"tw-animate-css": "1.4.0",
|
|
45
|
+
"zod": "4.6.7"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@tailwindcss/
|
|
50
|
-
"@
|
|
51
|
-
"@
|
|
52
|
-
"@types/
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
48
|
+
"@tailwindcss/vite": "4.3.3",
|
|
49
|
+
"@types/node": "26.6.5",
|
|
50
|
+
"@types/react": "19.3.0",
|
|
51
|
+
"@types/react-dom": "19.3.0",
|
|
52
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
53
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
54
|
+
"prettier": "3.9.10",
|
|
55
|
+
"tailwindcss": "4.3.3",
|
|
56
|
+
"typescript": "7.0.2",
|
|
57
|
+
"vite": "8.3.5",
|
|
58
|
+
"jscpd": "5.4.2",
|
|
59
|
+
"tsx": "4.23.15",
|
|
60
|
+
"oxlint": "1.89.0",
|
|
61
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
62
|
+
"@shadcn/lint": "0.2.3",
|
|
63
|
+
"stylelint": "17.16.0",
|
|
64
|
+
"stylelint-config-standard": "40.0.0",
|
|
65
|
+
"stylelint-declaration-strict-value": "1.12.1",
|
|
66
|
+
"vitest": "5.0.5",
|
|
67
|
+
"@testing-library/react": "16.3.3",
|
|
68
|
+
"@testing-library/dom": "10.4.2",
|
|
69
|
+
"@testing-library/jest-dom": "7.0.1",
|
|
70
|
+
"@testing-library/user-event": "14.6.8",
|
|
71
|
+
"jsdom": "30.1.3",
|
|
72
|
+
"@hey-api/openapi-ts": "0.99.0",
|
|
73
|
+
"@tanstack/router-plugin": "1.168.47",
|
|
74
|
+
"dotenv": "18.0.8",
|
|
75
|
+
"@rolldown/plugin-babel": "0.2.4",
|
|
76
|
+
"@babel/core": "8.0.7"
|
|
61
77
|
},
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"esbuild"
|
|
65
|
-
]
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=26.12.1"
|
|
66
80
|
}
|
|
67
81
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Verification never silently reinstalls or rewrites dependencies. Run pnpm install first.
|
|
2
|
+
verifyDepsBeforeRun: error
|
|
3
|
+
engineStrict: true
|
|
4
|
+
saveExact: true
|
|
5
|
+
allowBuilds:
|
|
6
|
+
esbuild: true
|
|
7
|
+
minimumReleaseAgeExclude:
|
|
8
|
+
- i18next@26.5.4
|
|
9
|
+
- jscpd-darwin-arm64@5.4.2
|
|
10
|
+
- jscpd-darwin-x64@5.4.2
|
|
11
|
+
- jscpd-linux-arm64-gnu@5.4.2
|
|
12
|
+
- jscpd-linux-arm64-musl@5.4.2
|
|
13
|
+
- jscpd-linux-x64-gnu@5.4.2
|
|
14
|
+
- jscpd-linux-x64-musl@5.4.2
|
|
15
|
+
- jscpd-windows-arm64-msvc@5.4.2
|
|
16
|
+
- jscpd-windows-x64-msvc@5.4.2
|
|
17
|
+
- jscpd@5.4.2
|
|
18
|
+
- lucide-react@1.61.0
|
|
19
|
+
- vite@8.3.5
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { generatedFiles } from "./generated-files.mjs";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const binary = path.join(
|
|
8
|
+
path.dirname(require.resolve("jscpd/package.json")),
|
|
9
|
+
"run-jscpd.js",
|
|
10
|
+
);
|
|
11
|
+
const result = spawnSync(
|
|
12
|
+
process.execPath,
|
|
13
|
+
[
|
|
14
|
+
binary,
|
|
15
|
+
"src",
|
|
16
|
+
"--min-tokens",
|
|
17
|
+
"50",
|
|
18
|
+
"--threshold",
|
|
19
|
+
"0.5",
|
|
20
|
+
"--reporters",
|
|
21
|
+
"consoleFull",
|
|
22
|
+
"--ignore",
|
|
23
|
+
generatedFiles().join(","),
|
|
24
|
+
],
|
|
25
|
+
{ stdio: "inherit" },
|
|
26
|
+
);
|
|
27
|
+
if (result.error) console.error(result.error.message);
|
|
28
|
+
process.exitCode = result.status ?? 1;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Keep this policy in source: handwritten SDK wrappers must still be checked.
|
|
5
|
+
/** @returns {string[]} */
|
|
6
|
+
export function generatedFiles(directory = "src") {
|
|
7
|
+
if (!fs.existsSync(directory)) return [];
|
|
8
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
9
|
+
const file = path.join(directory, entry.name);
|
|
10
|
+
if (entry.isSymbolicLink()) return [];
|
|
11
|
+
if (entry.isDirectory()) return generatedFiles(file);
|
|
12
|
+
const generated =
|
|
13
|
+
/\.gen\.[cm]?[jt]sx?$/.test(file) || /\/client\/index\.ts$/.test(file);
|
|
14
|
+
return generated &&
|
|
15
|
+
/^\/\/ This file is auto-generated by @hey-api\/openapi-ts\r?\n/.test(
|
|
16
|
+
fs.readFileSync(file, "utf8"),
|
|
17
|
+
)
|
|
18
|
+
? [file.replaceAll(path.sep, "/")]
|
|
19
|
+
: [];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -32,7 +32,7 @@ export function LanguageSwitcher() {
|
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
34
|
<Select value={currentLang} onValueChange={handleLanguageChange}>
|
|
35
|
-
<SelectTrigger className="min-w-
|
|
35
|
+
<SelectTrigger className="min-w-30">
|
|
36
36
|
<SelectValue />
|
|
37
37
|
</SelectTrigger>
|
|
38
38
|
<SelectContent>
|
|
@@ -9,7 +9,7 @@ export function ThemeSwitcher() {
|
|
|
9
9
|
const themes: ThemeTypes[] = ["light", "dark", "system"];
|
|
10
10
|
const currentIndex = themes.indexOf(theme);
|
|
11
11
|
const nextIndex = (currentIndex + 1) % themes.length;
|
|
12
|
-
setTheme(themes[nextIndex]);
|
|
12
|
+
setTheme(themes[nextIndex] ?? "system");
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const getIcon = () => {
|
|
@@ -68,12 +68,6 @@ function InputGroupAddon({
|
|
|
68
68
|
data-slot="input-group-addon"
|
|
69
69
|
data-align={align}
|
|
70
70
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
71
|
-
onClick={(e) => {
|
|
72
|
-
if ((e.target as HTMLElement).closest("button")) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
|
76
|
-
}}
|
|
77
71
|
{...props}
|
|
78
72
|
/>
|
|
79
73
|
);
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
|
|
3
|
-
@plugin "tailwindcss-animate";
|
|
4
2
|
@import "tw-animate-css";
|
|
5
3
|
|
|
6
4
|
@custom-variant dark (&:is(.dark *));
|
|
7
5
|
|
|
8
6
|
body {
|
|
9
7
|
@apply m-0;
|
|
8
|
+
|
|
10
9
|
font-family:
|
|
11
10
|
system-ui,
|
|
12
11
|
-apple-system,
|
|
13
12
|
"Segoe UI",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
Roboto,
|
|
14
|
+
Oxygen,
|
|
15
|
+
Ubuntu,
|
|
16
|
+
Cantarell,
|
|
18
17
|
"Fira Sans",
|
|
19
18
|
"Droid Sans",
|
|
20
19
|
"Helvetica Neue",
|
|
@@ -24,74 +23,74 @@ body {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
:root {
|
|
27
|
-
--background: oklch(
|
|
28
|
-
--foreground: oklch(
|
|
29
|
-
--primary: oklch(
|
|
30
|
-
--primary-foreground: oklch(
|
|
31
|
-
--secondary: oklch(
|
|
32
|
-
--secondary-foreground: oklch(
|
|
33
|
-
--muted: oklch(
|
|
34
|
-
--muted-foreground: oklch(
|
|
35
|
-
--accent: oklch(
|
|
36
|
-
--accent-foreground: oklch(
|
|
37
|
-
--destructive: oklch(
|
|
38
|
-
--destructive-foreground: oklch(
|
|
39
|
-
--border: oklch(
|
|
40
|
-
--input: oklch(
|
|
41
|
-
--ring: oklch(
|
|
26
|
+
--background: oklch(100% 0 0deg);
|
|
27
|
+
--foreground: oklch(14.7% 0.004 49.25deg);
|
|
28
|
+
--primary: oklch(21.6% 0.006 56.043deg);
|
|
29
|
+
--primary-foreground: oklch(98.5% 0.001 106.423deg);
|
|
30
|
+
--secondary: oklch(97% 0.001 106.424deg);
|
|
31
|
+
--secondary-foreground: oklch(21.6% 0.006 56.043deg);
|
|
32
|
+
--muted: oklch(97% 0.001 106.424deg);
|
|
33
|
+
--muted-foreground: oklch(55.3% 0.013 58.071deg);
|
|
34
|
+
--accent: oklch(97% 0.001 106.424deg);
|
|
35
|
+
--accent-foreground: oklch(21.6% 0.006 56.043deg);
|
|
36
|
+
--destructive: oklch(57.7% 0.245 27.325deg);
|
|
37
|
+
--destructive-foreground: oklch(98.5% 0 0deg);
|
|
38
|
+
--border: oklch(92.3% 0.003 48.717deg);
|
|
39
|
+
--input: oklch(92.3% 0.003 48.717deg);
|
|
40
|
+
--ring: oklch(70.9% 0.01 56.259deg);
|
|
42
41
|
--radius: 0.625rem;
|
|
43
|
-
--card: oklch(
|
|
44
|
-
--card-foreground: oklch(
|
|
45
|
-
--popover: oklch(
|
|
46
|
-
--popover-foreground: oklch(
|
|
47
|
-
--chart-1: oklch(
|
|
48
|
-
--chart-2: oklch(
|
|
49
|
-
--chart-3: oklch(
|
|
50
|
-
--chart-4: oklch(
|
|
51
|
-
--chart-5: oklch(
|
|
52
|
-
--sidebar: oklch(
|
|
53
|
-
--sidebar-foreground: oklch(
|
|
54
|
-
--sidebar-primary: oklch(
|
|
55
|
-
--sidebar-primary-foreground: oklch(
|
|
56
|
-
--sidebar-accent: oklch(
|
|
57
|
-
--sidebar-accent-foreground: oklch(
|
|
58
|
-
--sidebar-border: oklch(
|
|
59
|
-
--sidebar-ring: oklch(
|
|
42
|
+
--card: oklch(100% 0 0deg);
|
|
43
|
+
--card-foreground: oklch(14.7% 0.004 49.25deg);
|
|
44
|
+
--popover: oklch(100% 0 0deg);
|
|
45
|
+
--popover-foreground: oklch(14.7% 0.004 49.25deg);
|
|
46
|
+
--chart-1: oklch(64.6% 0.222 41.116deg);
|
|
47
|
+
--chart-2: oklch(60% 0.118 184.704deg);
|
|
48
|
+
--chart-3: oklch(39.8% 0.07 227.392deg);
|
|
49
|
+
--chart-4: oklch(82.8% 0.189 84.429deg);
|
|
50
|
+
--chart-5: oklch(76.9% 0.188 70.08deg);
|
|
51
|
+
--sidebar: oklch(98.5% 0.001 106.423deg);
|
|
52
|
+
--sidebar-foreground: oklch(14.7% 0.004 49.25deg);
|
|
53
|
+
--sidebar-primary: oklch(21.6% 0.006 56.043deg);
|
|
54
|
+
--sidebar-primary-foreground: oklch(98.5% 0.001 106.423deg);
|
|
55
|
+
--sidebar-accent: oklch(97% 0.001 106.424deg);
|
|
56
|
+
--sidebar-accent-foreground: oklch(21.6% 0.006 56.043deg);
|
|
57
|
+
--sidebar-border: oklch(92.3% 0.003 48.717deg);
|
|
58
|
+
--sidebar-ring: oklch(70.9% 0.01 56.259deg);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
.dark {
|
|
63
|
-
--background: oklch(
|
|
64
|
-
--foreground: oklch(
|
|
65
|
-
--primary: oklch(
|
|
66
|
-
--primary-foreground: oklch(
|
|
67
|
-
--secondary: oklch(
|
|
68
|
-
--secondary-foreground: oklch(
|
|
69
|
-
--muted: oklch(
|
|
70
|
-
--muted-foreground: oklch(
|
|
71
|
-
--accent: oklch(
|
|
72
|
-
--accent-foreground: oklch(
|
|
73
|
-
--destructive: oklch(
|
|
74
|
-
--destructive-foreground: oklch(
|
|
75
|
-
--border: oklch(
|
|
76
|
-
--input: oklch(
|
|
77
|
-
--ring: oklch(
|
|
78
|
-
--card: oklch(
|
|
79
|
-
--card-foreground: oklch(
|
|
80
|
-
--popover: oklch(
|
|
81
|
-
--popover-foreground: oklch(
|
|
82
|
-
--chart-1: oklch(
|
|
83
|
-
--chart-2: oklch(
|
|
84
|
-
--chart-3: oklch(
|
|
85
|
-
--chart-4: oklch(
|
|
86
|
-
--chart-5: oklch(
|
|
87
|
-
--sidebar: oklch(
|
|
88
|
-
--sidebar-foreground: oklch(
|
|
89
|
-
--sidebar-primary: oklch(
|
|
90
|
-
--sidebar-primary-foreground: oklch(
|
|
91
|
-
--sidebar-accent: oklch(
|
|
92
|
-
--sidebar-accent-foreground: oklch(
|
|
93
|
-
--sidebar-border: oklch(
|
|
94
|
-
--sidebar-ring: oklch(
|
|
62
|
+
--background: oklch(14.7% 0.004 49.25deg);
|
|
63
|
+
--foreground: oklch(98.5% 0.001 106.423deg);
|
|
64
|
+
--primary: oklch(92.3% 0.003 48.717deg);
|
|
65
|
+
--primary-foreground: oklch(21.6% 0.006 56.043deg);
|
|
66
|
+
--secondary: oklch(26.8% 0.007 34.298deg);
|
|
67
|
+
--secondary-foreground: oklch(98.5% 0.001 106.423deg);
|
|
68
|
+
--muted: oklch(26.8% 0.007 34.298deg);
|
|
69
|
+
--muted-foreground: oklch(70.9% 0.01 56.259deg);
|
|
70
|
+
--accent: oklch(26.8% 0.007 34.298deg);
|
|
71
|
+
--accent-foreground: oklch(98.5% 0.001 106.423deg);
|
|
72
|
+
--destructive: oklch(70.4% 0.191 22.216deg);
|
|
73
|
+
--destructive-foreground: oklch(63.7% 0.237 25.331deg);
|
|
74
|
+
--border: oklch(100% 0 0deg / 10%);
|
|
75
|
+
--input: oklch(100% 0 0deg / 15%);
|
|
76
|
+
--ring: oklch(55.3% 0.013 58.071deg);
|
|
77
|
+
--card: oklch(21.6% 0.006 56.043deg);
|
|
78
|
+
--card-foreground: oklch(98.5% 0.001 106.423deg);
|
|
79
|
+
--popover: oklch(21.6% 0.006 56.043deg);
|
|
80
|
+
--popover-foreground: oklch(98.5% 0.001 106.423deg);
|
|
81
|
+
--chart-1: oklch(48.8% 0.243 264.376deg);
|
|
82
|
+
--chart-2: oklch(69.6% 0.17 162.48deg);
|
|
83
|
+
--chart-3: oklch(76.9% 0.188 70.08deg);
|
|
84
|
+
--chart-4: oklch(62.7% 0.265 303.9deg);
|
|
85
|
+
--chart-5: oklch(64.5% 0.246 16.439deg);
|
|
86
|
+
--sidebar: oklch(21.6% 0.006 56.043deg);
|
|
87
|
+
--sidebar-foreground: oklch(98.5% 0.001 106.423deg);
|
|
88
|
+
--sidebar-primary: oklch(48.8% 0.243 264.376deg);
|
|
89
|
+
--sidebar-primary-foreground: oklch(98.5% 0.001 106.423deg);
|
|
90
|
+
--sidebar-accent: oklch(26.8% 0.007 34.298deg);
|
|
91
|
+
--sidebar-accent-foreground: oklch(98.5% 0.001 106.423deg);
|
|
92
|
+
--sidebar-border: oklch(100% 0 0deg / 10%);
|
|
93
|
+
--sidebar-ring: oklch(55.3% 0.013 58.071deg);
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
@theme inline {
|
|
@@ -140,6 +139,7 @@ body {
|
|
|
140
139
|
* {
|
|
141
140
|
@apply border-border outline-ring/50;
|
|
142
141
|
}
|
|
142
|
+
|
|
143
143
|
body {
|
|
144
144
|
@apply bg-background text-foreground;
|
|
145
145
|
}
|
|
@@ -33,26 +33,31 @@ if (!i18n.isInitialized) {
|
|
|
33
33
|
const lng = getStoredLanguage();
|
|
34
34
|
const enResources = mergeTranslations("en");
|
|
35
35
|
const jaResources = mergeTranslations("ja");
|
|
36
|
-
i18n
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
i18n
|
|
37
|
+
.use(initReactI18next)
|
|
38
|
+
.init({
|
|
39
|
+
resources: {
|
|
40
|
+
en: {
|
|
41
|
+
translation: enResources,
|
|
42
|
+
...enResources,
|
|
43
|
+
},
|
|
44
|
+
ja: {
|
|
45
|
+
translation: jaResources,
|
|
46
|
+
...jaResources,
|
|
47
|
+
},
|
|
41
48
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
defaultNS: "translation",
|
|
50
|
+
ns: ["translation", "auth", "app", "landing", "common", "theme"],
|
|
51
|
+
supportedLngs: supportedLanguages,
|
|
52
|
+
lng,
|
|
53
|
+
fallbackLng: defaultLanguage,
|
|
54
|
+
interpolation: {
|
|
55
|
+
escapeValue: false,
|
|
45
56
|
},
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
lng,
|
|
51
|
-
fallbackLng: defaultLanguage,
|
|
52
|
-
interpolation: {
|
|
53
|
-
escapeValue: false,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
57
|
+
})
|
|
58
|
+
.catch((error: unknown) =>
|
|
59
|
+
console.error("Translation initialization failed", error),
|
|
60
|
+
);
|
|
56
61
|
setLanguageHooks(lng);
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -66,7 +71,9 @@ export const changeLanguage = (language: SupportedLanguages) => {
|
|
|
66
71
|
} catch {
|
|
67
72
|
// Fallback if localStorage is not available
|
|
68
73
|
}
|
|
69
|
-
i18n
|
|
74
|
+
i18n
|
|
75
|
+
.changeLanguage(language)
|
|
76
|
+
.catch((error: unknown) => console.error("Language change failed", error));
|
|
70
77
|
setLanguageHooks(language);
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { handleBackendError } from "@/lib/utils/error-handler";
|
|
1
2
|
import type { ReactNode } from "react";
|
|
2
3
|
import type {
|
|
3
4
|
FieldPath,
|
|
@@ -50,7 +51,9 @@ export function AuthForm<T extends FieldValues>({
|
|
|
50
51
|
<form
|
|
51
52
|
noValidate
|
|
52
53
|
className="flex flex-col gap-4"
|
|
53
|
-
onSubmit={
|
|
54
|
+
onSubmit={(event) => {
|
|
55
|
+
form.handleSubmit(onSubmit)(event).catch(handleBackendError);
|
|
56
|
+
}}
|
|
54
57
|
>
|
|
55
58
|
{fields.map(({ name, label, ...input }) => (
|
|
56
59
|
<FormField
|
|
@@ -144,7 +144,7 @@ export const useLogout = () => {
|
|
|
144
144
|
setAuthToken("");
|
|
145
145
|
setCurrentUser(undefined);
|
|
146
146
|
setTokenInClient();
|
|
147
|
-
navigate({ to: DEFAULT_NON_LOGGED_IN_ROUTE });
|
|
147
|
+
return navigate({ to: DEFAULT_NON_LOGGED_IN_ROUTE });
|
|
148
148
|
},
|
|
149
149
|
});
|
|
150
150
|
};
|
|
@@ -159,7 +159,7 @@ export const useSendOtp = () => {
|
|
|
159
159
|
// Extract email from the mutation variables and navigate to reset-password
|
|
160
160
|
const email = variables.query?.authId;
|
|
161
161
|
if (email) {
|
|
162
|
-
navigate({
|
|
162
|
+
return navigate({
|
|
163
163
|
to: "/auth/reset-password",
|
|
164
164
|
search: { email },
|
|
165
165
|
});
|
|
@@ -183,7 +183,7 @@ export const useResetPassword = () => {
|
|
|
183
183
|
toast.success(
|
|
184
184
|
response.message || t("auth.messages.passwordResetSuccess"),
|
|
185
185
|
);
|
|
186
|
-
navigate({ to: LoginRoute.to });
|
|
186
|
+
return navigate({ to: LoginRoute.to });
|
|
187
187
|
},
|
|
188
188
|
onError: (err) => {
|
|
189
189
|
console.error("Reset password error:", err);
|
|
@@ -17,7 +17,7 @@ export function AuthHero() {
|
|
|
17
17
|
</p>
|
|
18
18
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
|
19
19
|
<Link to={LoginRoute.to}>
|
|
20
|
-
<Button size="lg" className="w-full sm:w-auto min-w-
|
|
20
|
+
<Button size="lg" className="w-full sm:w-auto min-w-37.5">
|
|
21
21
|
{t("hero.loginButton")}
|
|
22
22
|
</Button>
|
|
23
23
|
</Link>
|
|
@@ -25,7 +25,7 @@ export function AuthHero() {
|
|
|
25
25
|
<Button
|
|
26
26
|
size="lg"
|
|
27
27
|
variant="outline"
|
|
28
|
-
className="w-full sm:w-auto min-w-
|
|
28
|
+
className="w-full sm:w-auto min-w-37.5"
|
|
29
29
|
>
|
|
30
30
|
{t("hero.signupButton")}
|
|
31
31
|
</Button>
|
|
@@ -16,7 +16,7 @@ export function WelcomeHero() {
|
|
|
16
16
|
</p>
|
|
17
17
|
<div className="flex justify-center items-center">
|
|
18
18
|
<Link to={DEFAULT_LOGGED_IN_ROUTE}>
|
|
19
|
-
<Button size="lg" className="w-full sm:w-auto min-w-
|
|
19
|
+
<Button size="lg" className="w-full sm:w-auto min-w-37.5">
|
|
20
20
|
{t("landing.hero.getStartedButton")}
|
|
21
21
|
</Button>
|
|
22
22
|
</Link>
|
|
@@ -3,7 +3,7 @@ import type { AuthResponseDtoUserDetailDto } from "@/sdk";
|
|
|
3
3
|
|
|
4
4
|
interface RouterContext {
|
|
5
5
|
token: string;
|
|
6
|
-
data: AuthResponseDtoUserDetailDto;
|
|
6
|
+
data: AuthResponseDtoUserDetailDto | undefined;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
@@ -8,6 +8,7 @@ import { AuthLayout } from "@/modules/auth/layouts/auth-layout";
|
|
|
8
8
|
import { DEFAULT_LOGGED_IN_ROUTE } from "@/lib/routes";
|
|
9
9
|
import { useAtomValue } from "jotai";
|
|
10
10
|
import { authTokenAtom } from "@/modules/auth/hooks/use-auth-hook";
|
|
11
|
+
import { handleBackendError } from "@/lib/utils/error-handler";
|
|
11
12
|
import { useEffect } from "react";
|
|
12
13
|
|
|
13
14
|
export const Route = createFileRoute("/auth")({
|
|
@@ -28,7 +29,9 @@ function RouteComponent() {
|
|
|
28
29
|
// Navigate after successful authentication
|
|
29
30
|
useEffect(() => {
|
|
30
31
|
if (token) {
|
|
31
|
-
navigate({ to: DEFAULT_LOGGED_IN_ROUTE, replace: true })
|
|
32
|
+
navigate({ to: DEFAULT_LOGGED_IN_ROUTE, replace: true }).catch(
|
|
33
|
+
handleBackendError,
|
|
34
|
+
);
|
|
32
35
|
}
|
|
33
36
|
}, [token, navigate]);
|
|
34
37
|
|
|
@@ -23,9 +23,5 @@ export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
|
|
23
23
|
) => Config<Required<ClientOptions> & T>;
|
|
24
24
|
|
|
25
25
|
export const client = createClient(
|
|
26
|
-
createClientConfig(
|
|
27
|
-
createConfig<ClientOptions2>({
|
|
28
|
-
baseURL: "https://sandbox.beta.kodly.dev/proxy/95-2",
|
|
29
|
-
}),
|
|
30
|
-
),
|
|
26
|
+
createClientConfig(createConfig<ClientOptions2>()),
|
|
31
27
|
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
extends: ["stylelint-config-standard"],
|
|
3
|
+
plugins: ["stylelint-declaration-strict-value"],
|
|
4
|
+
rules: {
|
|
5
|
+
"at-rule-prelude-no-invalid": [
|
|
6
|
+
true,
|
|
7
|
+
{
|
|
8
|
+
ignoreAtRules: [
|
|
9
|
+
"apply",
|
|
10
|
+
"theme",
|
|
11
|
+
"custom-variant",
|
|
12
|
+
"utility",
|
|
13
|
+
"variant",
|
|
14
|
+
"source",
|
|
15
|
+
"plugin",
|
|
16
|
+
"config",
|
|
17
|
+
"reference",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
"at-rule-no-unknown": [
|
|
22
|
+
true,
|
|
23
|
+
{
|
|
24
|
+
ignoreAtRules: [
|
|
25
|
+
"theme",
|
|
26
|
+
"utility",
|
|
27
|
+
"variant",
|
|
28
|
+
"custom-variant",
|
|
29
|
+
"apply",
|
|
30
|
+
"source",
|
|
31
|
+
"plugin",
|
|
32
|
+
"config",
|
|
33
|
+
"reference",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
"import-notation": "string",
|
|
38
|
+
"scale-unlimited/declaration-strict-value": [
|
|
39
|
+
["/color$/", "fill", "stroke"],
|
|
40
|
+
{
|
|
41
|
+
ignoreValues: [
|
|
42
|
+
"inherit",
|
|
43
|
+
"initial",
|
|
44
|
+
"unset",
|
|
45
|
+
"revert",
|
|
46
|
+
"transparent",
|
|
47
|
+
"currentColor",
|
|
48
|
+
"none",
|
|
49
|
+
],
|
|
50
|
+
ignoreFunctions: false,
|
|
51
|
+
disableFix: true,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
describe("form controls", () => {
|
|
8
|
+
it("accepts keyboard input and submits its value", async () => {
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
const save = vi.fn();
|
|
11
|
+
render(
|
|
12
|
+
<form
|
|
13
|
+
onSubmit={(event) => {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
save(new FormData(event.currentTarget).get("name"));
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
<label htmlFor="name">Name</label>
|
|
19
|
+
<Input id="name" name="name" required />
|
|
20
|
+
<Button type="submit">Save</Button>
|
|
21
|
+
</form>,
|
|
22
|
+
);
|
|
23
|
+
await user.type(screen.getByRole("textbox", { name: "Name" }), "Ada");
|
|
24
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
25
|
+
expect(save).toHaveBeenCalledWith("Ada");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not submit a disabled action", async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
const save = vi.fn();
|
|
31
|
+
render(
|
|
32
|
+
<Button disabled onClick={save}>
|
|
33
|
+
Save
|
|
34
|
+
</Button>,
|
|
35
|
+
);
|
|
36
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
37
|
+
expect(save).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { test } from "vitest";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
formDataBodySerializer,
|
|
5
|
+
urlSearchParamsBodySerializer,
|
|
6
|
+
jsonBodySerializer,
|
|
7
|
+
} from "../src/sdk/core/bodySerializer.gen";
|
|
8
|
+
import { serializeQueryKeyValue } from "../src/sdk/core/queryKeySerializer.gen";
|
|
9
|
+
|
|
10
|
+
test("API serialization preserves values and omits absent fields", () => {
|
|
11
|
+
const body = {
|
|
12
|
+
title: "hello",
|
|
13
|
+
count: 3,
|
|
14
|
+
tags: ["a", "b"],
|
|
15
|
+
absent: null,
|
|
16
|
+
missing: undefined,
|
|
17
|
+
date: new Date("2026-10-20T00:00:00Z"),
|
|
18
|
+
nested: { enabled: true },
|
|
19
|
+
};
|
|
20
|
+
const form = formDataBodySerializer.bodySerializer(body);
|
|
21
|
+
assert.deepEqual(form.getAll("tags"), ["a", "b"]);
|
|
22
|
+
assert.equal(form.get("date"), body.date.toISOString());
|
|
23
|
+
assert.equal(form.get("nested"), '{"enabled":true}');
|
|
24
|
+
assert.equal(form.has("absent"), false);
|
|
25
|
+
assert.equal(form.has("missing"), false);
|
|
26
|
+
const params = new URLSearchParams(
|
|
27
|
+
urlSearchParamsBodySerializer.bodySerializer(body),
|
|
28
|
+
);
|
|
29
|
+
assert.deepEqual(params.getAll("tags"), ["a", "b"]);
|
|
30
|
+
assert.equal(params.get("count"), "3");
|
|
31
|
+
assert.equal(jsonBodySerializer.bodySerializer({ id: 23n }), '{"id":"23"}');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("query cache keys preserve dates and have stable parameter ordering", () => {
|
|
35
|
+
assert.equal(serializeQueryKeyValue(23n), "23");
|
|
36
|
+
assert.equal(
|
|
37
|
+
serializeQueryKeyValue(new Date("2026-10-20T00:00:00Z")),
|
|
38
|
+
"2026-10-20T00:00:00.000Z",
|
|
39
|
+
);
|
|
40
|
+
assert.equal(serializeQueryKeyValue(Symbol("omitted")), undefined);
|
|
41
|
+
assert.equal(
|
|
42
|
+
serializeQueryKeyValue(() => 1),
|
|
43
|
+
undefined,
|
|
44
|
+
);
|
|
45
|
+
assert.deepEqual(serializeQueryKeyValue(new URLSearchParams("z=1&a=2&a=3")), {
|
|
46
|
+
a: ["2", "3"],
|
|
47
|
+
z: "1",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineConfig, loadEnv } from "vite";
|
|
2
|
-
import viteReact from "@vitejs/plugin-react";
|
|
2
|
+
import viteReact, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
3
|
+
import babel from "@rolldown/plugin-babel";
|
|
3
4
|
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
|
4
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
6
|
import { resolve } from "node:path";
|
|
@@ -19,16 +20,13 @@ export default defineConfig(({ mode }) => {
|
|
|
19
20
|
enabled: true,
|
|
20
21
|
},
|
|
21
22
|
}),
|
|
22
|
-
viteReact(
|
|
23
|
-
|
|
24
|
-
plugins: [["babel-plugin-react-compiler"]],
|
|
25
|
-
},
|
|
26
|
-
}),
|
|
23
|
+
viteReact(),
|
|
24
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
27
25
|
tailwindcss(),
|
|
28
26
|
],
|
|
29
27
|
resolve: {
|
|
30
28
|
alias: {
|
|
31
|
-
"@": resolve(
|
|
29
|
+
"@": resolve(import.meta.dirname, "./src"),
|
|
32
30
|
},
|
|
33
31
|
},
|
|
34
32
|
server: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
|
|
8
|
+
test: {
|
|
9
|
+
environment: "jsdom",
|
|
10
|
+
setupFiles: ["./tests/setup.ts"],
|
|
11
|
+
include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"],
|
|
12
|
+
restoreMocks: true,
|
|
13
|
+
},
|
|
14
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generator-kodly-react-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "A Yeoman generator for creating React.js applications with Vite, TanStack Router, authentication, and i18n",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"generator"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "
|
|
21
|
+
"test": "node --test test/*.test.mjs",
|
|
22
22
|
"prepublishOnly": "rm -rf generators/app/templates/node_modules generators/app/templates/dist generators/app/templates/.tanstack generators/app/templates/package-lock.json generators/app/templates/*.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"author": "The Panther <info@thepanther.io>",
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/thepanther-io/kodly-react-yo-generator#readme",
|
|
34
34
|
"engines": {
|
|
35
|
-
"node": ">=
|
|
35
|
+
"node": ">=26.12.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"yeoman-generator": "
|
|
39
|
-
"lodash": "
|
|
40
|
-
"mem-fs": "
|
|
41
|
-
"mem-fs-editor": "
|
|
42
|
-
"chalk": "
|
|
43
|
-
}
|
|
38
|
+
"yeoman-generator": "10.2.1",
|
|
39
|
+
"lodash": "4.18.1",
|
|
40
|
+
"mem-fs": "6.0.0",
|
|
41
|
+
"mem-fs-editor": "12.1.0",
|
|
42
|
+
"chalk": "6.0.1"
|
|
43
|
+
},
|
|
44
|
+
"packageManager": "pnpm@12.14.0"
|
|
44
45
|
}
|