@harpy-js/core 0.4.7 → 0.4.9

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/dist/cli.js CHANGED
File without changes
@@ -1,7 +1,6 @@
1
1
  interface UseI18nReturn {
2
- switchLocale: (locale: string) => void;
3
- getCurrentLocale: () => string | null;
4
- buildUrl: (path: string, locale?: string) => string;
2
+ switchLocale: (locale: string) => Promise<void>;
3
+ isLoading: boolean;
5
4
  }
6
5
  export declare function useI18n(): UseI18nReturn;
7
6
  export {};
@@ -1,64 +1,47 @@
1
1
  "use strict";
2
- "use client";
2
+ 'use client';
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.useI18n = useI18n;
5
5
  const react_1 = require("react");
6
6
  function useI18n() {
7
- (0, react_1.useEffect)(() => {
8
- if (typeof window !== "undefined" &&
9
- !window.__HARPY_I18N_INITIALIZED__) {
10
- window.__HARPY_I18N_INITIALIZED__ = true;
11
- const script = document.createElement("script");
12
- script.textContent = `
13
- (function() {
14
- if (window.__HARPY_I18N_NAV_INSTALLED__) return;
15
- window.__HARPY_I18N_NAV_INSTALLED__ = true;
16
-
17
- document.addEventListener('click', function(e) {
18
- var target = e.target;
19
- while (target && target.tagName !== 'A') {
20
- target = target.parentElement;
7
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
8
+ const switchLocale = async (locale) => {
9
+ if (typeof window === 'undefined')
10
+ return;
11
+ setIsLoading(true);
12
+ try {
13
+ console.log('[useI18n] Switching locale to:', locale);
14
+ const formData = new URLSearchParams();
15
+ formData.append('locale', locale);
16
+ formData.append('redirect', window.location.pathname + window.location.search);
17
+ const response = await fetch('/api/i18n/switch-locale', {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/x-www-form-urlencoded',
21
+ },
22
+ body: formData.toString(),
23
+ redirect: 'manual',
24
+ });
25
+ console.log('[useI18n] Response status:', response.status);
26
+ if (response.type === 'opaqueredirect' || response.status === 302 || response.status === 0) {
27
+ window.location.reload();
28
+ }
29
+ else if (response.ok || response.redirected) {
30
+ window.location.reload();
21
31
  }
22
- if (target && target.tagName === 'A' && target.href) {
23
- var url = new URL(target.href, window.location.origin);
24
- var currentLang = new URLSearchParams(window.location.search).get('lang');
25
- if (currentLang && url.origin === window.location.origin && !url.searchParams.has('lang')) {
26
- url.searchParams.set('lang', currentLang);
27
- target.href = url.toString();
28
- }
32
+ else {
33
+ console.error('[useI18n] Unexpected response:', response.status);
34
+ window.location.reload();
29
35
  }
30
- });
31
- })();
32
- `;
33
- document.head.appendChild(script);
34
36
  }
35
- }, []);
36
- const switchLocale = (locale) => {
37
- if (typeof window === "undefined")
38
- return;
39
- const url = new URL(window.location.href);
40
- url.searchParams.set("lang", locale);
41
- window.location.href = url.toString();
42
- };
43
- const getCurrentLocale = () => {
44
- if (typeof window === "undefined")
45
- return null;
46
- const url = new URL(window.location.href);
47
- return url.searchParams.get("lang");
48
- };
49
- const buildUrl = (path, locale) => {
50
- if (typeof window === "undefined")
51
- return path;
52
- const currentLocale = locale || getCurrentLocale();
53
- if (!currentLocale)
54
- return path;
55
- const url = new URL(path, window.location.origin);
56
- url.searchParams.set("lang", currentLocale);
57
- return url.pathname + url.search;
37
+ catch (err) {
38
+ const errorMsg = err instanceof Error ? err.message : String(err);
39
+ console.error('[useI18n] Error:', errorMsg);
40
+ window.location.reload();
41
+ }
58
42
  };
59
43
  return {
60
44
  switchLocale,
61
- getCurrentLocale,
62
- buildUrl,
45
+ isLoading,
63
46
  };
64
47
  }
@@ -1,9 +1,4 @@
1
1
  import { NestFastifyApplication } from "@nestjs/platform-fastify";
2
- import * as React from "react";
3
- import { MetaOptions } from "../decorators/jsx.decorator";
4
- export interface JsxLayoutProps {
5
- children: React.ReactNode;
6
- meta?: MetaOptions;
7
- }
8
- export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
2
+ import { JsxLayout, JsxLayoutProps, PageProps } from "../types/jsx.types";
3
+ export type { JsxLayout, JsxLayoutProps, PageProps };
9
4
  export declare function withJsxEngine(app: NestFastifyApplication, defaultLayout: JsxLayout): void;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { StaticAssetsController } from "./core/static-assets.controller";
7
7
  export { JsxRender } from "./decorators/jsx.decorator";
8
8
  export { WithLayout } from "./decorators/layout.decorator";
9
9
  export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
10
- export type { JsxLayout, JsxLayoutProps } from "./core/jsx.engine";
10
+ export type { JsxLayout, JsxLayoutProps, PageProps } from "./types/jsx.types";
11
11
  export { RouterModule } from "./core/router.module";
12
12
  export { NavigationService } from "./core/navigation.service";
13
13
  export { AutoRegisterModule } from "./core/auto-register.module";
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { MetaOptions } from "../decorators/jsx.decorator";
3
+ export interface PageProps {
4
+ [key: string]: any;
5
+ }
6
+ export interface JsxLayoutProps {
7
+ children: React.ReactNode;
8
+ meta?: MetaOptions;
9
+ }
10
+ export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harpy-js/core",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Harpy - A powerful NestJS + React/JSX SSR framework with automatic hydration and i18n support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,111 +1,90 @@
1
1
  /**
2
2
  * React hook for i18n locale switching (Client-side only)
3
3
  *
4
- * This hook provides a simple way to switch locales from any component
5
- * (buttons, dropdowns, selects, etc.)
6
- *
7
- * The hook updates the URL with the selected locale and reloads the page.
8
- * The server-side interceptor will automatically set the cookie based on the URL.
9
- *
10
- * When used in a component, it automatically injects the language-aware navigation script.
4
+ * This hook provides a Next.js-style server action pattern for locale switching.
5
+ * It posts form data to the server, which sets the locale cookie and returns
6
+ * a redirect URL. Then we navigate to that URL to reload with the new locale.
11
7
  *
12
8
  * @example
13
9
  * ```tsx
14
10
  * import { useI18n } from '@harpy-js/core/client';
15
11
  *
16
12
  * function MyComponent() {
17
- * const { switchLocale } = useI18n();
18
- * return <button onClick={() => switchLocale('fr')}>French</button>;
13
+ * const { switchLocale, isLoading } = useI18n();
14
+ * return (
15
+ * <>
16
+ * <button onClick={() => switchLocale('fr')} disabled={isLoading}>
17
+ * {isLoading ? 'Loading...' : 'French'}
18
+ * </button>
19
+ * </>
20
+ * );
19
21
  * }
20
22
  * ```
21
23
  */
22
24
 
23
- "use client";
25
+ 'use client';
24
26
 
25
- import { useEffect } from "react";
27
+ import { useState } from 'react';
26
28
 
27
29
  interface UseI18nReturn {
28
- switchLocale: (locale: string) => void;
29
- getCurrentLocale: () => string | null;
30
- buildUrl: (path: string, locale?: string) => string;
30
+ switchLocale: (locale: string) => Promise<void>;
31
+ isLoading: boolean;
31
32
  }
32
33
 
33
34
  export function useI18n(): UseI18nReturn {
34
- // Register that this component uses i18n, so the navigation script gets injected
35
- useEffect(() => {
36
- // Mark that i18n is being used in this render
37
- if (
38
- typeof window !== "undefined" &&
39
- !(window as any).__HARPY_I18N_INITIALIZED__
40
- ) {
41
- (window as any).__HARPY_I18N_INITIALIZED__ = true;
42
-
43
- // Inject language-aware navigation script
44
- const script = document.createElement("script");
45
- script.textContent = `
46
- (function() {
47
- if (window.__HARPY_I18N_NAV_INSTALLED__) return;
48
- window.__HARPY_I18N_NAV_INSTALLED__ = true;
49
-
50
- document.addEventListener('click', function(e) {
51
- var target = e.target;
52
- while (target && target.tagName !== 'A') {
53
- target = target.parentElement;
54
- }
55
- if (target && target.tagName === 'A' && target.href) {
56
- var url = new URL(target.href, window.location.origin);
57
- var currentLang = new URLSearchParams(window.location.search).get('lang');
58
- if (currentLang && url.origin === window.location.origin && !url.searchParams.has('lang')) {
59
- url.searchParams.set('lang', currentLang);
60
- target.href = url.toString();
61
- }
62
- }
63
- });
64
- })();
65
- `;
66
- document.head.appendChild(script);
67
- }
68
- }, []);
35
+ const [isLoading, setIsLoading] = useState(false);
69
36
 
70
37
  /**
71
- * Switch to a new locale by updating the URL and reloading the page
38
+ * Switch to a new locale by posting to the server endpoint
39
+ * The server will set the locale cookie and return a redirect URL
72
40
  */
73
- const switchLocale = (locale: string): void => {
74
- if (typeof window === "undefined") return;
41
+ const switchLocale = async (locale: string): Promise<void> => {
42
+ if (typeof window === 'undefined') return;
75
43
 
76
- const url = new URL(window.location.href);
77
- url.searchParams.set("lang", locale);
78
- window.location.href = url.toString();
79
- };
44
+ setIsLoading(true);
80
45
 
81
- /**
82
- * Get the current locale from the URL
83
- */
84
- const getCurrentLocale = (): string | null => {
85
- if (typeof window === "undefined") return null;
46
+ try {
47
+ console.log('[useI18n] Switching locale to:', locale);
86
48
 
87
- const url = new URL(window.location.href);
88
- return url.searchParams.get("lang");
89
- };
49
+ // Create FormData to send as application/x-www-form-urlencoded
50
+ const formData = new URLSearchParams();
51
+ formData.append('locale', locale);
52
+ formData.append('redirect', window.location.pathname + window.location.search);
90
53
 
91
- /**
92
- * Build a URL with the current locale preserved
93
- * Useful for navigation links that should maintain the language
94
- */
95
- const buildUrl = (path: string, locale?: string): string => {
96
- if (typeof window === "undefined") return path;
54
+ const response = await fetch('/api/i18n/switch-locale', {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/x-www-form-urlencoded',
58
+ },
59
+ body: formData.toString(),
60
+ redirect: 'manual', // Don't follow redirects automatically
61
+ });
97
62
 
98
- const currentLocale = locale || getCurrentLocale();
99
- if (!currentLocale) return path;
63
+ console.log('[useI18n] Response status:', response.status);
100
64
 
101
- const url = new URL(path, window.location.origin);
102
- url.searchParams.set("lang", currentLocale);
103
- return url.pathname + url.search;
65
+ // Server will return 302 redirect, which fetch sees as status 0 with 'opaqueredirect' type
66
+ // Or it might return the redirect URL in the response body
67
+ if (response.type === 'opaqueredirect' || response.status === 302 || response.status === 0) {
68
+ // Cookie is set, just reload the current page
69
+ window.location.reload();
70
+ } else if (response.ok || response.redirected) {
71
+ // If we got a response, reload
72
+ window.location.reload();
73
+ } else {
74
+ console.error('[useI18n] Unexpected response:', response.status);
75
+ // Try to reload anyway since cookie might be set
76
+ window.location.reload();
77
+ }
78
+ } catch (err) {
79
+ const errorMsg = err instanceof Error ? err.message : String(err);
80
+ console.error('[useI18n] Error:', errorMsg);
81
+ // Even on error, try to reload in case cookie was set
82
+ window.location.reload();
83
+ }
104
84
  };
105
85
 
106
86
  return {
107
87
  switchLocale,
108
- getCurrentLocale,
109
- buildUrl,
88
+ isLoading,
110
89
  };
111
90
  }
@@ -3,17 +3,18 @@ import { FastifyReply } from "fastify";
3
3
  import * as React from "react";
4
4
  import { renderToPipeableStream, renderToString } from "react-dom/server";
5
5
  import { MetaOptions, RenderOptions } from "../decorators/jsx.decorator";
6
+ import {
7
+ JsxLayout,
8
+ JsxLayoutProps,
9
+ PageProps,
10
+ } from "../types/jsx.types";
6
11
  import { hydrationContext, initializeHydrationContext } from "./hydration";
7
12
  import { getChunkPath, getHydrationManifest } from "./hydration-manifest";
8
13
  import { LiveReloadController } from "./live-reload.controller";
9
14
  import { StaticAssetsController } from "./static-assets.controller";
10
15
 
11
- export interface JsxLayoutProps {
12
- children: React.ReactNode;
13
- meta?: MetaOptions;
14
- }
15
-
16
- export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;
16
+ // Export types for use in applications
17
+ export type { JsxLayout, JsxLayoutProps, PageProps };
17
18
 
18
19
  // Cache for component-to-chunk path mappings (loaded once at startup)
19
20
  const chunkPathCache = new Map<string, string>();
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ export type { MetaOptions, RenderOptions } from "./decorators/jsx.decorator";
15
15
  // Consumers should import i18n types and modules from that package.
16
16
 
17
17
  // Types
18
- export type { JsxLayout, JsxLayoutProps } from "./core/jsx.engine";
18
+ export type { JsxLayout, JsxLayoutProps, PageProps } from "./types/jsx.types";
19
19
  export { RouterModule } from "./core/router.module";
20
20
  export { NavigationService } from "./core/navigation.service";
21
21
  export { AutoRegisterModule } from "./core/auto-register.module";
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { MetaOptions } from "../decorators/jsx.decorator";
3
+
4
+ /**
5
+ * Base props interface for all page components.
6
+ * Extend this interface to add custom props to your pages.
7
+ *
8
+ * @example
9
+ * interface HomePage extends PageProps {
10
+ * items: string[];
11
+ * user?: User;
12
+ * }
13
+ */
14
+ export interface PageProps {
15
+ [key: string]: any;
16
+ }
17
+
18
+ /**
19
+ * Props interface for layout components.
20
+ * Layouts receive the page content as children and optional metadata.
21
+ */
22
+ export interface JsxLayoutProps {
23
+ children: React.ReactNode;
24
+ meta?: MetaOptions;
25
+ }
26
+
27
+ /**
28
+ * Type for layout component functions.
29
+ */
30
+ export type JsxLayout = (props: JsxLayoutProps) => React.ReactElement;