void-snippets-monorepo 0.1.1

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 (32) hide show
  1. package/README.md +2261 -0
  2. package/package.json +18 -0
  3. package/packages/client/package.json +47 -0
  4. package/packages/client/src/configure.ts +34 -0
  5. package/packages/client/src/index.ts +4 -0
  6. package/packages/client/src/services/base-api.service.ts +26 -0
  7. package/packages/client/src/services/resource-api.service.ts +117 -0
  8. package/packages/client/src/utils/handle-api-error.ts +20 -0
  9. package/packages/client/tsconfig.json +13 -0
  10. package/packages/client/tsup.config.ts +10 -0
  11. package/packages/core/package.json +41 -0
  12. package/packages/core/src/id.ts +19 -0
  13. package/packages/core/src/index.ts +4 -0
  14. package/packages/core/src/string-to-id.ts +22 -0
  15. package/packages/core/src/types/index.ts +86 -0
  16. package/packages/core/src/utils/catch-error.ts +20 -0
  17. package/packages/core/tsconfig.json +13 -0
  18. package/packages/core/tsup.config.ts +9 -0
  19. package/packages/react/package.json +80 -0
  20. package/packages/react/src/hooks/createResourceHooks.ts +872 -0
  21. package/packages/react/src/hooks/useAlertMessage.ts +45 -0
  22. package/packages/react/src/hooks/useAsyncState.ts +110 -0
  23. package/packages/react/src/hooks/useCallTimer.ts +37 -0
  24. package/packages/react/src/hooks/useModal.ts +71 -0
  25. package/packages/react/src/hooks/usePagination.ts +57 -0
  26. package/packages/react/src/index.ts +43 -0
  27. package/packages/react/src/routing/createRouteContract.ts +483 -0
  28. package/packages/react/src/socket/createSocketHooks.ts +351 -0
  29. package/packages/react/tsconfig.json +14 -0
  30. package/packages/react/tsup.config.ts +10 -0
  31. package/pnpm-workspace.yaml +2 -0
  32. package/tsconfig.base.json +12 -0
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "void-snippets-monorepo",
3
+ "private": false,
4
+ "version": "0.1.1",
5
+ "devDependencies": {
6
+ "typescript": "^5.4.0",
7
+ "tsup": "^8.0.0"
8
+ },
9
+ "scripts": {
10
+ "build": "pnpm -r run build",
11
+ "build:core": "pnpm --filter @void-snippets/core run build",
12
+ "build:client": "pnpm --filter @void-snippets/client run build",
13
+ "build:react": "pnpm --filter @void-snippets/react run build",
14
+ "dev": "pnpm -r run dev",
15
+ "publish:all": "pnpm -r publish --access public --no-git-checks",
16
+ "version:bump": "pnpm -r exec npm version"
17
+ }
18
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@void-snippets/client",
3
+ "version": "0.3.0",
4
+ "description": "Framework-agnostic HTTP resource service for TypeScript projects",
5
+ "homepage": "https://void-snippets.vercel.app",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/shahtirthhh/void-snippets.git",
9
+ "directory": "packages/client"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/shahtirthhh/void-snippets/issues"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.mjs",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.js",
21
+ "default": "./dist/index.mjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch"
30
+ },
31
+ "keywords": [
32
+ "axios",
33
+ "typescript",
34
+ "api",
35
+ "void-snippets",
36
+ "resource"
37
+ ],
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "@void-snippets/core": "workspace:*",
41
+ "axios": "^1.6.0"
42
+ },
43
+ "devDependencies": {
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.4.0"
46
+ }
47
+ }
@@ -0,0 +1,34 @@
1
+ import type { AxiosInstance } from "axios";
2
+
3
+ let _instance: AxiosInstance | null = null;
4
+
5
+ /**
6
+ * Registers your axios instance globally.
7
+ * Call this once at your app's entry point before using any service.
8
+ *
9
+ * @example
10
+ * import axios from 'axios';
11
+ * import { configure } from '@void-snippets/client';
12
+ *
13
+ * const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });
14
+ * configure(axiosInstance);
15
+ */
16
+ export function configure(instance: AxiosInstance): void {
17
+ _instance = instance;
18
+ }
19
+
20
+ /**
21
+ * @internal — used by BaseApiService, not part of the public API.
22
+ */
23
+ export function getConfiguredInstance(): AxiosInstance {
24
+ if (!_instance) {
25
+ throw new Error(
26
+ "[@void-snippets/client] No axios instance configured.\n" +
27
+ "Call configure(axiosInstance) once at your app entry point before using any resource service.\n\n" +
28
+ "Example:\n" +
29
+ " import { configure } from '@void-snippets/client';\n" +
30
+ " configure(myAxiosInstance);"
31
+ );
32
+ }
33
+ return _instance;
34
+ }
@@ -0,0 +1,4 @@
1
+ export { configure } from "./configure";
2
+ export { BaseApiService } from "./services/base-api.service";
3
+ export { ResourceService } from "./services/resource-api.service";
4
+ export { handleApiError } from "./utils/handle-api-error";
@@ -0,0 +1,26 @@
1
+ import type { AxiosInstance } from "axios";
2
+ import { getConfiguredInstance } from "../configure";
3
+
4
+ /**
5
+ * Abstract base for all resource services.
6
+ * Automatically uses the axios instance registered via configure().
7
+ */
8
+ export abstract class BaseApiService {
9
+ protected readonly endpoint: string;
10
+
11
+ constructor(endpoint: string) {
12
+ this.endpoint = endpoint;
13
+ }
14
+
15
+ /**
16
+ * Returns the globally configured axios instance.
17
+ * Lazy-evaluated so configure() can be called after class instantiation.
18
+ */
19
+ protected get http(): AxiosInstance {
20
+ return getConfiguredInstance();
21
+ }
22
+
23
+ protected getFullUrl(path: string): string {
24
+ return `${this.endpoint}${path}`;
25
+ }
26
+ }
@@ -0,0 +1,117 @@
1
+ import type {
2
+ VSQueryParams,
3
+ VSDefaultPaginatedResponse,
4
+ VSDefaultSingleResponse,
5
+ } from "@void-snippets/core";
6
+ import { handleApiError } from "../utils/handle-api-error";
7
+ import { BaseApiService } from "./base-api.service";
8
+
9
+ /**
10
+ * Generic CRUD resource service. Extend this class for each API resource.
11
+ *
12
+ * @typeParam TId - The type of the resource's identifier (e.g., string)
13
+ * @typeParam TBase - The base entity type returned in list responses
14
+ * @typeParam TDetail - The detailed entity type returned in single-item responses (defaults to TBase)
15
+ * @typeParam TCreate - The payload type for create operations (defaults to Partial<TBase>)
16
+ * @typeParam TUpdate - The payload type for update operations (defaults to Partial<TBase>)
17
+ * @typeParam TListRaw - Raw API list response shape (defaults to VSDefaultPaginatedResponse<TBase>)
18
+ * @typeParam TSingleRaw - Raw API single-item response shape (defaults to VSDefaultSingleResponse<TDetail>)
19
+ *
20
+ * @example
21
+ * import { ResourceService } from '@void-snippets/client';
22
+ * import type { Contact } from './contacts.types';
23
+ *
24
+ * export class ContactsApiService extends ResourceService<
25
+ * Contact.Id,
26
+ * Contact.Base,
27
+ * Contact.WithCreatedBy,
28
+ * Contact.Apis.CreatePayload,
29
+ * Contact.Apis.UpdatePayload
30
+ * > {
31
+ * constructor() {
32
+ * super('/contacts');
33
+ * }
34
+ * }
35
+ *
36
+ * export const ContactsApis = new ContactsApiService();
37
+ */
38
+ export class ResourceService<
39
+ TId,
40
+ TBase,
41
+ TDetail = TBase,
42
+ TCreate = Partial<TBase>,
43
+ TUpdate = Partial<TBase>,
44
+ TListRaw = VSDefaultPaginatedResponse<TBase>,
45
+ TSingleRaw = VSDefaultSingleResponse<TDetail>,
46
+ > extends BaseApiService {
47
+ declare readonly __types: {
48
+ id: TId;
49
+ base: TBase;
50
+ detail: TDetail;
51
+ create: TCreate;
52
+ update: TUpdate;
53
+ listRaw: TListRaw;
54
+ singleRaw: TSingleRaw;
55
+ };
56
+
57
+ constructor(endpoint: string) {
58
+ super(endpoint);
59
+ }
60
+
61
+ async list(params?: VSQueryParams): Promise<TListRaw> {
62
+ try {
63
+ const { data } = await this.http.get<TListRaw>(this.getFullUrl(""), {
64
+ params,
65
+ });
66
+ return data;
67
+ } catch (error) {
68
+ handleApiError(error);
69
+ }
70
+ }
71
+
72
+ async get(id: TId): Promise<TSingleRaw> {
73
+ try {
74
+ const { data } = await this.http.get<TSingleRaw>(
75
+ `${this.getFullUrl("")}/${String(id)}`
76
+ );
77
+ return data;
78
+ } catch (error) {
79
+ handleApiError(error);
80
+ }
81
+ }
82
+
83
+ async create(payload: TCreate): Promise<TSingleRaw> {
84
+ try {
85
+ const { data } = await this.http.post<TSingleRaw>(
86
+ this.getFullUrl(""),
87
+ payload
88
+ );
89
+ return data;
90
+ } catch (error) {
91
+ handleApiError(error);
92
+ }
93
+ }
94
+
95
+ async update(id: TId, payload: TUpdate): Promise<TSingleRaw> {
96
+ try {
97
+ const { data } = await this.http.patch<TSingleRaw>(
98
+ `${this.getFullUrl("")}/${String(id)}`,
99
+ payload
100
+ );
101
+ return data;
102
+ } catch (error) {
103
+ handleApiError(error);
104
+ }
105
+ }
106
+
107
+ async delete(id: TId): Promise<TSingleRaw> {
108
+ try {
109
+ const { data } = await this.http.delete<TSingleRaw>(
110
+ `${this.getFullUrl("")}/${String(id)}`
111
+ );
112
+ return data;
113
+ } catch (error) {
114
+ handleApiError(error);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,20 @@
1
+ import { AxiosError } from "axios";
2
+
3
+ /**
4
+ * Normalizes axios and generic errors into a standard Error.
5
+ * Always throws — return type is `never`.
6
+ */
7
+ export function handleApiError(error: unknown): never {
8
+ if (error instanceof AxiosError) {
9
+ const serverMessage =
10
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
11
+ (error.response?.data as { message?: string })?.message;
12
+ throw new Error(serverMessage ?? error.message);
13
+ }
14
+
15
+ if (error instanceof Error) {
16
+ throw error;
17
+ }
18
+
19
+ throw new Error("An unexpected error occurred.");
20
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ external: ["@void-snippets/core"],
10
+ });
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@void-snippets/core",
3
+ "version": "0.3.0",
4
+ "description": "Core types and utilities for void-snippets packages",
5
+ "homepage": "https://void-snippets.vercel.app",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/shahtirthhh/void-snippets.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/shahtirthhh/void-snippets/issues"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.mjs",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.js",
21
+ "default": "./dist/index.mjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch"
30
+ },
31
+ "keywords": [
32
+ "typescript",
33
+ "void-snippets",
34
+ "types"
35
+ ],
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.4.0"
40
+ }
41
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Creates a branded (nominal) type from a primitive.
3
+ *
4
+ * Branded types prevent accidental mixing of structurally identical
5
+ * but semantically different IDs at compile time.
6
+ *
7
+ * @typeParam K - The underlying primitive type (usually `string`)
8
+ * @typeParam T - A unique brand tag per entity (use a string literal)
9
+ *
10
+ * @example
11
+ * type ContactId = VSId<string, 'Contact'>;
12
+ * type UserId = VSId<string, 'User'>;
13
+ *
14
+ * declare const contactId: ContactId;
15
+ * declare let userId: UserId;
16
+ *
17
+ * userId = contactId; // ✅ TypeScript error — brands don't match
18
+ */
19
+ export type VSId<K, T> = K & { __brand: T };
@@ -0,0 +1,4 @@
1
+ export * from "./types/index";
2
+ export type { VSId } from "./id";
3
+ export { stringToId } from "./string-to-id";
4
+ export { catchError } from "./utils/catch-error";
@@ -0,0 +1,22 @@
1
+ import type { VSId } from "./id";
2
+
3
+ /**
4
+ * Casts a plain `string` to a branded `VSId` type.
5
+ *
6
+ * Use at runtime boundaries (e.g. URL params, API responses, localStorage)
7
+ * where you receive a raw string and need the correct branded ID type.
8
+ *
9
+ * @typeParam T - Must be a `VSId<string, Brand>` type
10
+ *
11
+ * @example
12
+ * import type { VSId } from '@void-snippets/core';
13
+ * import { stringToId } from '@void-snippets/core';
14
+ *
15
+ * type ContactId = VSId<string, 'Contact'>;
16
+ *
17
+ * const raw = params.contactId; // string
18
+ * const id = stringToId<ContactId>(raw); // ContactId ✅
19
+ */
20
+ export const stringToId = <T extends VSId<string, unknown>>(
21
+ id: string
22
+ ): T => id as unknown as T;
@@ -0,0 +1,86 @@
1
+ // ============================================================================
2
+ // PAGINATION
3
+ // ============================================================================
4
+
5
+ export interface VSPagination {
6
+ page: number;
7
+ limit: number;
8
+ totalPages: number;
9
+ totalDocuments: number;
10
+ }
11
+
12
+ export interface VSQueryParams {
13
+ page?: number;
14
+ limit?: number;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ // ============================================================================
19
+ // DEFAULT API RESPONSE SHAPES
20
+ // These are the shapes the library uses out-of-the-box.
21
+ // If your API returns a different shape, provide custom adapters.
22
+ // ============================================================================
23
+
24
+ export interface VSDefaultPaginatedData<T> {
25
+ items: T[];
26
+ page: number;
27
+ limit: number;
28
+ totalPages: number;
29
+ totalDocuments: number;
30
+ }
31
+
32
+ export interface VSDefaultPaginatedResponse<T> {
33
+ data: VSDefaultPaginatedData<T>;
34
+ }
35
+
36
+ export interface VSDefaultSingleResponse<T> {
37
+ data: T;
38
+ }
39
+
40
+ // ============================================================================
41
+ // ADAPTER INTERFACES
42
+ // Adapters decouple the library from any specific API response shape.
43
+ // Pass them to createResourceHooks() if your API shape differs from defaults.
44
+ //
45
+ // @example
46
+ // const adapters: VSAdapters<MyListRes, User, MySingleRes, UserDetail> = {
47
+ // fromList: (raw) => ({ items: raw.results, pagination: { ... } }),
48
+ // fromSingle: (raw) => raw.payload,
49
+ // };
50
+ // ============================================================================
51
+
52
+ export interface VSListResult<TBase> {
53
+ items: TBase[];
54
+ pagination: VSPagination;
55
+ }
56
+
57
+ export interface VSAdapters<TListRaw, TBase, TSingleRaw, TDetail> {
58
+ fromList: (raw: TListRaw) => VSListResult<TBase>;
59
+ fromSingle: (raw: TSingleRaw) => TDetail;
60
+ }
61
+
62
+ // ============================================================================
63
+ // DEFAULT ADAPTERS FACTORY
64
+ // Works out-of-the-box if your API matches VSDefaultPaginatedResponse /
65
+ // VSDefaultSingleResponse. Zero config needed in that case.
66
+ // ============================================================================
67
+
68
+ export function createDefaultAdapters<TBase, TDetail>(): VSAdapters<
69
+ VSDefaultPaginatedResponse<TBase>,
70
+ TBase,
71
+ VSDefaultSingleResponse<TDetail>,
72
+ TDetail
73
+ > {
74
+ return {
75
+ fromList: (raw) => ({
76
+ items: raw.data.items,
77
+ pagination: {
78
+ page: raw.data.page,
79
+ limit: raw.data.limit,
80
+ totalPages: raw.data.totalPages,
81
+ totalDocuments: raw.data.totalDocuments,
82
+ },
83
+ }),
84
+ fromSingle: (raw) => raw.data,
85
+ };
86
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Wraps a Promise and returns a [error, null] | [null, data] tuple.
3
+ * Eliminates try/catch at the call site — Go-style error handling for TypeScript.
4
+ *
5
+ * @example
6
+ * const [err, data] = await catchError(fetchUser(id));
7
+ * if (err) { // handle error }
8
+ * // data is correctly typed as T here
9
+ */
10
+ export async function catchError<T>(
11
+ promise: Promise<T>
12
+ ): Promise<[Error, null] | [null, T]> {
13
+ try {
14
+ const data = await promise;
15
+ return [null, data];
16
+ } catch (error) {
17
+ if (error instanceof Error) return [error, null];
18
+ return [new Error(String(error)), null];
19
+ }
20
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ });
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@void-snippets/react",
3
+ "version": "0.6.0",
4
+ "description": "TanStack Query resource hooks factory, Socket.IO hooks factory, type-safe routing contract, and general-purpose React hooks",
5
+ "homepage": "https://void-snippets.vercel.app",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/shahtirthhh/void-snippets.git",
9
+ "directory": "packages/react"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/shahtirthhh/void-snippets/issues"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.mjs",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.js",
21
+ "default": "./dist/index.mjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch"
30
+ },
31
+ "keywords": [
32
+ "react",
33
+ "tanstack-query",
34
+ "react-query",
35
+ "hooks",
36
+ "api",
37
+ "socket.io",
38
+ "websocket",
39
+ "routing",
40
+ "react-router",
41
+ "type-safe",
42
+ "void-snippets",
43
+ "createResourceHooks",
44
+ "createSocketHooks",
45
+ "createRouteContract",
46
+ "useTypedSearchParams",
47
+ "useModal",
48
+ "usePagination",
49
+ "useAsyncState",
50
+ "useAlertMessage",
51
+ "useCallTimer"
52
+ ],
53
+ "license": "MIT",
54
+ "peerDependencies": {
55
+ "react": ">=17.0.0",
56
+ "@void-snippets/client": ">=0.1.0",
57
+ "socket.io-client": ">=4.6.0",
58
+ "react-router": ">=7.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "socket.io-client": {
62
+ "optional": true
63
+ },
64
+ "react-router": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "dependencies": {
69
+ "@tanstack/react-query": "^5.0.0",
70
+ "@void-snippets/core": "workspace:*"
71
+ },
72
+ "devDependencies": {
73
+ "@types/react": "^18.0.0",
74
+ "react": "^18.0.0",
75
+ "react-router": "^7.0.0",
76
+ "socket.io-client": "^4.7.0",
77
+ "tsup": "^8.0.0",
78
+ "typescript": "^5.4.0"
79
+ }
80
+ }