create-next-style-guide 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/addons/ci/.cursor/rules/ci.mdc +11 -0
  2. package/addons/ci/docs/style-guide/ops/ci.md +7 -0
  3. package/addons/docker/.cursor/rules/docker.mdc +12 -0
  4. package/addons/docker/.dockerignore +12 -0
  5. package/addons/docker/docker/production.Dockerfile +53 -0
  6. package/addons/docker/docs/style-guide/ops/docker.md +14 -0
  7. package/addons/docs/data/tanstack-query.md +7 -0
  8. package/addons/docs/examples/api-app-crud.md +94 -0
  9. package/addons/docs/ui/icons.md +5 -0
  10. package/addons/endpoints/.cursor/rules/endpoints.mdc +17 -0
  11. package/addons/endpoints/docs/style-guide/http/endpoints.md +21 -0
  12. package/addons/endpoints/scripts/endpoints.codegen.json +7 -0
  13. package/addons/endpoints/scripts/generate-endpoints.ts +1384 -0
  14. package/addons/endpoints/src/endpoints/Types.ts +105 -0
  15. package/addons/endpoints/src/endpoints/index.ts +2 -0
  16. package/addons/endpoints/src/utils/api-error.ts +181 -0
  17. package/addons/endpoints/src/utils/api-result.ts +13 -0
  18. package/addons/endpoints/src/utils/axios-instance.ts +23 -0
  19. package/addons/endpoints/src/utils/form-data.ts +37 -0
  20. package/addons/endpoints/src/utils/search-params.ts +21 -0
  21. package/addons/eslint/.cursor/rules/eslint.mdc +12 -0
  22. package/addons/eslint/docs/style-guide/tooling/eslint.md +13 -0
  23. package/addons/eslint/eslint.config.mjs +85 -0
  24. package/addons/husky/.husky/pre-commit +2 -0
  25. package/addons/husky/docs/style-guide/tooling/husky.md +10 -0
  26. package/addons/prettier/.cursor/rules/prettier.mdc +11 -0
  27. package/addons/prettier/.prettierrc +14 -0
  28. package/addons/prettier/docs/style-guide/tooling/prettier.md +9 -0
  29. package/dist/index.js +3848 -0
  30. package/dist/index.js.map +1 -0
  31. package/package.json +38 -4
  32. package/template/.cursor/rules/components.mdc +27 -0
  33. package/template/.cursor/rules/core.mdc +16 -0
  34. package/template/.cursor/rules/forms.mdc +16 -0
  35. package/template/.env.example +6 -0
  36. package/template/AGENTS.md +38 -0
  37. package/template/README.md +28 -0
  38. package/template/docs/style-guide/README.md +11 -0
  39. package/template/docs/style-guide/http/client.md +9 -0
  40. package/template/docs/style-guide/overview/architecture.md +21 -0
  41. package/template/docs/style-guide/overview/project-structure.md +36 -0
  42. package/template/eslint.config.mjs +13 -0
  43. package/template/gitignore +39 -0
  44. package/template/next-env.d.ts +4 -0
  45. package/template/next.config.ts +7 -0
  46. package/template/package.json +27 -0
  47. package/template/src/app/_components/welcome-card/index.tsx +24 -0
  48. package/template/src/app/globals.css +84 -0
  49. package/template/src/app/layout.tsx +22 -0
  50. package/template/src/app/page.tsx +25 -0
  51. package/template/src/components/.gitkeep +0 -0
  52. package/template/src/constants/.gitkeep +0 -0
  53. package/template/src/hooks/.gitkeep +0 -0
  54. package/template/src/lib/env.ts +17 -0
  55. package/template/src/lib/http.ts +28 -0
  56. package/template/src/lib/utils.ts +6 -0
  57. package/template/src/providers/.gitkeep +0 -0
  58. package/template/src/types/.gitkeep +0 -0
  59. package/template/src/utils/.gitkeep +0 -0
  60. package/template/tsconfig.json +23 -0
  61. package/README.md +0 -3
@@ -0,0 +1,105 @@
1
+ /* -------------------------------------------------------------------------- */
2
+ /* Request */
3
+ /* -------------------------------------------------------------------------- */
4
+
5
+ /** Query params + JSON body wrapper used by every endpoint call. */
6
+ export type ApiRequestData<TParams = undefined, TBody = undefined> = {
7
+ params?: TParams;
8
+ body?: TBody;
9
+ };
10
+
11
+ /** Standard argument shape for endpoint functions. */
12
+ export type ApiRequest<TParams = undefined, TBody = undefined> = {
13
+ request?: ApiRequestData<TParams, TBody>;
14
+ };
15
+
16
+ export type ApiListParams = {
17
+ all?: string;
18
+ order?: string;
19
+ search?: string;
20
+ };
21
+
22
+ export type ApiPaginatedParams = {
23
+ page_size?: string | number;
24
+ p?: string | number;
25
+ order?: string;
26
+ search?: string;
27
+ };
28
+
29
+ export type ApiRequestAll<TParams = undefined, TBody = undefined> = ApiRequest<
30
+ ApiListParams & TParams,
31
+ TBody
32
+ >;
33
+
34
+ export type ApiRequestPaginated<TParams = undefined, TBody = undefined> =
35
+ ApiRequest<ApiPaginatedParams & TParams, TBody>;
36
+
37
+ /* -------------------------------------------------------------------------- */
38
+ /* Response */
39
+ /* -------------------------------------------------------------------------- */
40
+
41
+ /** API envelope for a single object `result`. */
42
+ export type ApiObjectResponse<T> = {
43
+ result: T;
44
+ status: number;
45
+ success: boolean;
46
+ messages: Record<string, unknown>;
47
+ };
48
+
49
+ /** API envelope for an unpaginated list `result`. */
50
+ export type ApiListResponse<T> = {
51
+ result: T[];
52
+ status: number;
53
+ success: boolean;
54
+ messages: Record<string, unknown>;
55
+ };
56
+
57
+ /** API envelope for page-number paginated `result`. */
58
+ export type ApiPaginatedResponse<T> = {
59
+ result: {
60
+ count: number;
61
+ next: string | null;
62
+ previous: string | null;
63
+ page_size: number;
64
+ results: T[];
65
+ };
66
+ status: number;
67
+ success: boolean;
68
+ messages: Record<string, unknown>;
69
+ };
70
+
71
+ /** Limit/offset page payload (often the unwrapped `result` of list endpoints). */
72
+ export type ApiLimitOffsetPage<T> = {
73
+ limit: number;
74
+ offset: number;
75
+ count: number;
76
+ next: string | null;
77
+ previous: string | null;
78
+ results: T[];
79
+ };
80
+
81
+ /** API envelope for limit/offset paginated `result`. */
82
+ export type ApiLimitOffsetResponse<T> = {
83
+ result: ApiLimitOffsetPage<T>;
84
+ status: number;
85
+ success: boolean;
86
+ messages: Record<string, unknown>;
87
+ };
88
+
89
+ /* -------------------------------------------------------------------------- */
90
+ /* Errors */
91
+ /* -------------------------------------------------------------------------- */
92
+
93
+ /** Single message item from standard API error payload. */
94
+ export type ApiMessageItem = {
95
+ message: string;
96
+ code?: string;
97
+ };
98
+
99
+ /** API error response envelope (HTTP 400). */
100
+ export type ApiErrorEnvelope = {
101
+ success: boolean;
102
+ status: number;
103
+ result: unknown[];
104
+ messages: Record<string, ApiMessageItem[] | string[] | string>;
105
+ };
@@ -0,0 +1,2 @@
1
+ /** Hand-written stubs until you run `pnpm endpoints:generate`. */
2
+ export * from "./Types";
@@ -0,0 +1,181 @@
1
+ import { isAxiosError } from "axios";
2
+
3
+ import type { ApiErrorEnvelope, ApiMessageItem } from "@/endpoints/Types";
4
+
5
+ export type { ApiErrorEnvelope, ApiMessageItem };
6
+
7
+ function codeFromValue(value: unknown): string | null {
8
+ if (Array.isArray(value)) {
9
+ for (const item of value) {
10
+ const code = codeFromValue(item);
11
+ if (code) return code;
12
+ }
13
+ return null;
14
+ }
15
+
16
+ if (value && typeof value === "object" && "code" in value) {
17
+ const code = (value as ApiMessageItem).code;
18
+ if (typeof code === "string" && code.trim()) return code.trim();
19
+ }
20
+
21
+ return null;
22
+ }
23
+
24
+ function envelopeMessages(error: unknown): Record<string, unknown> | null {
25
+ if (!isAxiosError(error)) return null;
26
+
27
+ const messages = (error.response?.data as { messages?: unknown } | undefined)
28
+ ?.messages;
29
+ if (!messages || typeof messages !== "object") return null;
30
+
31
+ if (!Array.isArray(messages)) {
32
+ return messages as Record<string, unknown>;
33
+ }
34
+
35
+ const mapped: Record<string, unknown> = {};
36
+ for (let i = 0; i < messages.length; i += 1) {
37
+ const item = messages[i];
38
+ if (item && typeof item === "object") {
39
+ const field = (item as { field?: unknown }).field;
40
+ if (typeof field === "string" && field.trim()) {
41
+ mapped[field.trim()] = item;
42
+ } else {
43
+ mapped[`_${i}`] = item;
44
+ }
45
+ }
46
+ }
47
+ return mapped;
48
+ }
49
+
50
+ function messageFromValue(value: unknown): string | null {
51
+ if (typeof value === "string" && value.trim()) return value;
52
+
53
+ if (Array.isArray(value)) {
54
+ for (const item of value) {
55
+ if (typeof item === "string" && item.trim()) return item;
56
+ if (item && typeof item === "object" && "message" in item) {
57
+ const text = (item as ApiMessageItem).message;
58
+ if (typeof text === "string" && text.trim()) return text;
59
+ }
60
+ }
61
+ }
62
+
63
+ if (value && typeof value === "object" && "message" in value) {
64
+ const text = (value as ApiMessageItem).message;
65
+ if (typeof text === "string" && text.trim()) return text;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ export const NETWORK_ERROR_MESSAGE =
72
+ "Could not reach the API. Check the network or that the server is running.";
73
+
74
+ export const SERVER_ERROR_MESSAGE =
75
+ "The API returned a server error. Try again later.";
76
+
77
+ export function isNetworkError(error: unknown): boolean {
78
+ if (!isAxiosError(error)) return false;
79
+ return (
80
+ !error.response ||
81
+ error.code === "ERR_NETWORK" ||
82
+ error.code === "ECONNREFUSED" ||
83
+ error.code === "ETIMEDOUT" ||
84
+ error.message === "Network Error"
85
+ );
86
+ }
87
+
88
+ export function isServerError(error: unknown): boolean {
89
+ if (!isAxiosError(error)) return false;
90
+ const status = error.response?.status;
91
+ return typeof status === "number" && status >= 500;
92
+ }
93
+
94
+ export function getApiErrorMessage(
95
+ error: unknown,
96
+ fallback = "Something went wrong. Please try again.",
97
+ ): string {
98
+ if (isNetworkError(error)) return NETWORK_ERROR_MESSAGE;
99
+ if (isServerError(error)) return SERVER_ERROR_MESSAGE;
100
+
101
+ if (!isAxiosError(error)) {
102
+ if (error instanceof Error && error.message.trim()) return error.message;
103
+ return fallback;
104
+ }
105
+
106
+ const data = error.response?.data as
107
+ | { messages?: Record<string, unknown>; detail?: unknown }
108
+ | undefined;
109
+
110
+ const messages = envelopeMessages(error);
111
+ if (messages) {
112
+ for (const value of Object.values(messages)) {
113
+ const message = messageFromValue(value);
114
+ if (message) return message;
115
+ }
116
+ }
117
+
118
+ const detail = messageFromValue(data?.detail);
119
+ if (detail) return detail;
120
+
121
+ return fallback;
122
+ }
123
+
124
+ export function getApiErrorCode(error: unknown): string | null {
125
+ if (!isAxiosError(error)) return null;
126
+
127
+ const data = error.response?.data as
128
+ | { messages?: Record<string, unknown>; detail?: unknown; code?: unknown }
129
+ | undefined;
130
+
131
+ if (typeof data?.code === "string" && data.code.trim()) {
132
+ return data.code.trim();
133
+ }
134
+
135
+ const messages = envelopeMessages(error);
136
+ if (messages) {
137
+ for (const value of Object.values(messages)) {
138
+ const code = codeFromValue(value);
139
+ if (code) return code;
140
+ }
141
+ }
142
+
143
+ return codeFromValue(data?.detail);
144
+ }
145
+
146
+ export function getApiHttpStatus(error: unknown): number | undefined {
147
+ if (!isAxiosError(error)) return undefined;
148
+ return error.response?.status;
149
+ }
150
+
151
+ export function getApiErrorFieldCode(
152
+ error: unknown,
153
+ field: string,
154
+ ): string | null {
155
+ const messages = envelopeMessages(error);
156
+ if (!messages || !(field in messages)) return null;
157
+ return codeFromValue(messages[field]);
158
+ }
159
+
160
+ export function getApiErrorFieldMessage(
161
+ error: unknown,
162
+ field: string,
163
+ ): string | null {
164
+ const messages = envelopeMessages(error);
165
+ if (!messages || !(field in messages)) return null;
166
+ return messageFromValue(messages[field]);
167
+ }
168
+
169
+ export function extractApiFieldErrors(
170
+ error: unknown,
171
+ ): Record<string, string> {
172
+ const messages = envelopeMessages(error);
173
+ if (!messages) return {};
174
+
175
+ const result: Record<string, string> = {};
176
+ for (const [key, value] of Object.entries(messages)) {
177
+ const msg = messageFromValue(value);
178
+ if (msg) result[key] = msg;
179
+ }
180
+ return result;
181
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Many APIs return `{ result, status, success, messages }`.
3
+ * Some auth responses may be the bare payload.
4
+ */
5
+ export function unwrapApiResult<T>(data: unknown): T {
6
+ if (data && typeof data === "object" && "result" in data) {
7
+ const result = (data as { result: unknown }).result;
8
+ if (result !== undefined && result !== null) {
9
+ return result as T;
10
+ }
11
+ }
12
+ return data as T;
13
+ }
@@ -0,0 +1,23 @@
1
+ import axios from "axios";
2
+
3
+ const baseURL = process.env.NEXT_PUBLIC_API_URL ?? "";
4
+
5
+ /**
6
+ * Shared Axios client for `src/endpoints/*`.
7
+ * Do not call from UI components directly — go through generated endpoint modules.
8
+ */
9
+ export const api = axios.create({
10
+ baseURL,
11
+ headers: {
12
+ Accept: "application/json",
13
+ "Content-Type": "application/json",
14
+ },
15
+ });
16
+
17
+ api.interceptors.request.use((config) => {
18
+ // Let the browser set multipart boundary; default JSON Content-Type breaks FormData.
19
+ if (typeof FormData !== "undefined" && config.data instanceof FormData) {
20
+ config.headers.delete("Content-Type");
21
+ }
22
+ return config;
23
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * JSON body as-is, or multipart FormData when any value is a File/Blob.
3
+ * Used by OpenAPI codegen for upload endpoints.
4
+ */
5
+ export function toApiBody(
6
+ body: Record<string, unknown> | undefined,
7
+ ): FormData | Record<string, unknown> | undefined {
8
+ if (!body) {
9
+ return undefined;
10
+ }
11
+
12
+ const hasFile = Object.values(body).some(
13
+ (value) =>
14
+ (typeof File !== "undefined" && value instanceof File) ||
15
+ (typeof Blob !== "undefined" && value instanceof Blob),
16
+ );
17
+
18
+ if (!hasFile) {
19
+ return body;
20
+ }
21
+
22
+ const form = new FormData();
23
+ for (const [key, value] of Object.entries(body)) {
24
+ if (value === undefined || value === null) continue;
25
+ if (
26
+ (typeof File !== "undefined" && value instanceof File) ||
27
+ (typeof Blob !== "undefined" && value instanceof Blob)
28
+ ) {
29
+ form.append(key, value);
30
+ } else if (typeof value === "object") {
31
+ form.append(key, JSON.stringify(value));
32
+ } else {
33
+ form.append(key, String(value));
34
+ }
35
+ }
36
+ return form;
37
+ }
@@ -0,0 +1,21 @@
1
+ /** Build `?a=1&b=2` from a params object (skips empty values). */
2
+ export function toSearchParams(
3
+ params:
4
+ | Record<string, string | number | boolean | null | undefined>
5
+ | undefined,
6
+ ): string {
7
+ if (!params) {
8
+ return "";
9
+ }
10
+
11
+ const search = new URLSearchParams();
12
+ for (const [key, value] of Object.entries(params)) {
13
+ if (value === undefined || value === null || value === "") {
14
+ continue;
15
+ }
16
+ search.set(key, String(value));
17
+ }
18
+
19
+ const query = search.toString();
20
+ return query ? `?${query}` : "";
21
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Enhanced ESLint conventions (import order, func-style)
3
+ globs: eslint.config.mjs,src/**/*.{ts,tsx}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # ESLint
8
+
9
+ - Import order (simple-import-sort): react/next → packages → `@/app` → `@/providers` → `@/endpoints` → `@/components` → `@/hooks` → `@/lib` → `@/utils` → other `@/` → relative → css.
10
+ - `function Name()` declarations (not `const Name = () =>` for components/helpers that ESLint `func-style` covers).
11
+ - JSX props: shorthand first, callbacks last (`react/jsx-sort-props`).
12
+ - Scripts: `pnpm lint` · `pnpm lint:fix`. Prefer `required_permissions: ["all"]` for shell lint in agents.
@@ -0,0 +1,13 @@
1
+ # ESLint
2
+
3
+ Enhanced flat config: `eslint.config.mjs`.
4
+
5
+ - Next core-web-vitals + TypeScript
6
+ - `eslint-plugin-simple-import-sort` + `eslint-plugin-import`
7
+ - `func-style: declaration`, `react/jsx-sort-props`
8
+ - `eslint-config-prettier` (turns off formatting rules that clash with Prettier)
9
+
10
+ ```bash
11
+ pnpm lint
12
+ pnpm lint:fix
13
+ ```
@@ -0,0 +1,85 @@
1
+ import nextVitals from "eslint-config-next/core-web-vitals";
2
+ import nextTs from "eslint-config-next/typescript";
3
+ import eslintConfigPrettier from "eslint-config-prettier";
4
+ import importPlugin from "eslint-plugin-import";
5
+ import simpleImportSort from "eslint-plugin-simple-import-sort";
6
+
7
+ /**
8
+ * Enhanced flat config (import sort, func-style, jsx-sort-props).
9
+ * Prettier disable-conflicts block is always included; harmless without Prettier.
10
+ */
11
+ const eslintConfig = [
12
+ ...nextVitals,
13
+ ...nextTs,
14
+ eslintConfigPrettier,
15
+ {
16
+ settings: {
17
+ react: {
18
+ version: "19",
19
+ },
20
+ },
21
+ plugins: {
22
+ import: importPlugin,
23
+ "simple-import-sort": simpleImportSort,
24
+ },
25
+ rules: {
26
+ "simple-import-sort/imports": [
27
+ "error",
28
+ {
29
+ groups: [
30
+ ["^react\\u0000?$", "^react/", "^next", "^next/"],
31
+ ["^@?\\w"],
32
+ ["^@/app"],
33
+ ["^@/providers"],
34
+ ["^@/endpoints"],
35
+ ["^@/components"],
36
+ ["^@/hooks"],
37
+ ["^@/lib"],
38
+ ["^@/utils"],
39
+ ["^@/"],
40
+ ["^~"],
41
+ ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
42
+ ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
43
+ ["^.+\\.s?css$"],
44
+ ["^\\u0000"],
45
+ ],
46
+ },
47
+ ],
48
+ "simple-import-sort/exports": "error",
49
+ "import/first": "error",
50
+ "import/newline-after-import": "error",
51
+ "import/no-duplicates": "error",
52
+
53
+ "react/react-in-jsx-scope": "off",
54
+ "react/jsx-sort-props": [
55
+ "error",
56
+ {
57
+ callbacksLast: true,
58
+ shorthandFirst: true,
59
+ multiline: "last",
60
+ },
61
+ ],
62
+
63
+ "func-style": ["error", "declaration", { allowArrowFunctions: false }],
64
+
65
+ "@typescript-eslint/no-unused-expressions": "off",
66
+ "@typescript-eslint/no-explicit-any": "off",
67
+ "@typescript-eslint/no-empty-object-type": "off",
68
+ "no-unused-vars": "off",
69
+ "@typescript-eslint/no-unused-vars": [
70
+ "warn",
71
+ {
72
+ argsIgnorePattern: "^_",
73
+ varsIgnorePattern: "^_",
74
+ caughtErrorsIgnorePattern: "^_",
75
+ ignoreRestSiblings: true,
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ {
81
+ ignores: [".next/**", "out/**", "build/**", "next-env.d.ts", "node_modules/**"],
82
+ },
83
+ ];
84
+
85
+ export default eslintConfig;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ npx lint-staged
@@ -0,0 +1,10 @@
1
+ # Husky + lint-staged
2
+
3
+ Pre-commit runs `npx lint-staged` (Prettier and/or ESLint — whatever was enabled at scaffold).
4
+
5
+ | Path | Role |
6
+ |------|------|
7
+ | `.husky/pre-commit` | Hook entry |
8
+ | `package.json` → `lint-staged` | Globs |
9
+
10
+ `prepare`: `husky` — runs after install.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Prettier formatting
3
+ globs: .prettierrc,**/*.{ts,tsx,js,jsx,json,css,md,yml,yaml}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Prettier
8
+
9
+ - Config: `.prettierrc` (printWidth 100, double quotes, trailing commas).
10
+ - Do not hand-fight formatting — run `pnpm format`.
11
+ - With Husky: lint-staged runs Prettier before ESLint on commit.
@@ -0,0 +1,14 @@
1
+ {
2
+ "arrowParens": "always",
3
+ "bracketSpacing": true,
4
+ "bracketSameLine": false,
5
+ "htmlWhitespaceSensitivity": "css",
6
+ "printWidth": 100,
7
+ "proseWrap": "preserve",
8
+ "quoteProps": "as-needed",
9
+ "semi": true,
10
+ "singleQuote": false,
11
+ "tabWidth": 2,
12
+ "trailingComma": "all",
13
+ "useTabs": false
14
+ }
@@ -0,0 +1,9 @@
1
+ # Prettier
2
+
3
+ Config: `.prettierrc`.
4
+
5
+ ```bash
6
+ pnpm format
7
+ ```
8
+
9
+ When Husky is enabled, pre-commit runs Prettier via lint-staged.