@sudobility/building_blocks 0.0.117 → 0.0.121
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/{SafeSubscriptionContext-BuMPCXVi.js → SafeSubscriptionContext-CNuEKeqJ.js} +3 -89
- package/dist/SafeSubscriptionContext-CNuEKeqJ.js.map +1 -0
- package/dist/components/app/SudobilityApp.d.ts +3 -4
- package/dist/components/layout/app-page-layout.d.ts +2 -0
- package/dist/firebase.js +1 -1
- package/dist/index.js +99 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/SafeSubscriptionContext-BuMPCXVi.js.map +0 -1
|
@@ -3,95 +3,12 @@ import { Suspense, useRef, useEffect, useMemo, createContext, useContext } from
|
|
|
3
3
|
import { useLocation, BrowserRouter } from "react-router-dom";
|
|
4
4
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
5
|
import { HelmetProvider } from "react-helmet-async";
|
|
6
|
-
import {
|
|
6
|
+
import { I18nextProvider } from "react-i18next";
|
|
7
7
|
import { NetworkProvider } from "@sudobility/devops-components";
|
|
8
8
|
import { getFirebaseService, getNetworkService } from "@sudobility/di/web";
|
|
9
9
|
import { InfoBanner } from "@sudobility/di_web";
|
|
10
10
|
import { ThemeProvider, FontSize, Theme } from "@sudobility/components";
|
|
11
11
|
import { useToast, ToastContainer, ToastProvider } from "@sudobility/components/ui/toast";
|
|
12
|
-
import i18n from "i18next";
|
|
13
|
-
import Backend from "i18next-http-backend";
|
|
14
|
-
import LanguageDetector from "i18next-browser-languagedetector";
|
|
15
|
-
const DEFAULT_SUPPORTED_LANGUAGES = ["en"];
|
|
16
|
-
const DEFAULT_NAMESPACES = [
|
|
17
|
-
"common",
|
|
18
|
-
"home",
|
|
19
|
-
"pricing",
|
|
20
|
-
"docs",
|
|
21
|
-
"dashboard",
|
|
22
|
-
"auth",
|
|
23
|
-
"privacy",
|
|
24
|
-
"terms",
|
|
25
|
-
"settings"
|
|
26
|
-
];
|
|
27
|
-
function detectLanguageFromPath(supportedLanguages) {
|
|
28
|
-
if (typeof window === "undefined") {
|
|
29
|
-
return "en";
|
|
30
|
-
}
|
|
31
|
-
const pathLang = window.location.pathname.split("/")[1];
|
|
32
|
-
if (pathLang && supportedLanguages.includes(pathLang)) {
|
|
33
|
-
return pathLang;
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const stored = localStorage.getItem("language");
|
|
37
|
-
if (stored && supportedLanguages.includes(stored)) {
|
|
38
|
-
return stored;
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
}
|
|
42
|
-
return "en";
|
|
43
|
-
}
|
|
44
|
-
let initialized = false;
|
|
45
|
-
function initializeI18n(config = {}) {
|
|
46
|
-
if (initialized) {
|
|
47
|
-
return i18n;
|
|
48
|
-
}
|
|
49
|
-
initialized = true;
|
|
50
|
-
const {
|
|
51
|
-
supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
|
|
52
|
-
namespaces = DEFAULT_NAMESPACES,
|
|
53
|
-
defaultNamespace = "common",
|
|
54
|
-
loadPath = "/locales/{{lng}}/{{ns}}.json",
|
|
55
|
-
debug = false
|
|
56
|
-
} = config;
|
|
57
|
-
i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({
|
|
58
|
-
lng: detectLanguageFromPath(supportedLanguages),
|
|
59
|
-
fallbackLng: {
|
|
60
|
-
zh: ["zh", "en"],
|
|
61
|
-
"zh-hant": ["zh-hant", "zh", "en"],
|
|
62
|
-
default: ["en"]
|
|
63
|
-
},
|
|
64
|
-
initImmediate: false,
|
|
65
|
-
supportedLngs: supportedLanguages,
|
|
66
|
-
debug,
|
|
67
|
-
nonExplicitSupportedLngs: true,
|
|
68
|
-
interpolation: {
|
|
69
|
-
escapeValue: false
|
|
70
|
-
},
|
|
71
|
-
backend: {
|
|
72
|
-
loadPath
|
|
73
|
-
},
|
|
74
|
-
detection: {
|
|
75
|
-
order: ["path", "localStorage", "navigator"],
|
|
76
|
-
caches: ["localStorage"],
|
|
77
|
-
lookupLocalStorage: "language",
|
|
78
|
-
lookupFromPathIndex: 0
|
|
79
|
-
},
|
|
80
|
-
load: "languageOnly",
|
|
81
|
-
preload: [],
|
|
82
|
-
cleanCode: false,
|
|
83
|
-
lowerCaseLng: false,
|
|
84
|
-
defaultNS: defaultNamespace,
|
|
85
|
-
ns: namespaces
|
|
86
|
-
});
|
|
87
|
-
return i18n;
|
|
88
|
-
}
|
|
89
|
-
function getI18n() {
|
|
90
|
-
if (!initialized) {
|
|
91
|
-
initializeI18n();
|
|
92
|
-
}
|
|
93
|
-
return i18n;
|
|
94
|
-
}
|
|
95
12
|
function DefaultLoadingFallback() {
|
|
96
13
|
return /* @__PURE__ */ jsx("div", { className: "min-h-screen flex items-center justify-center bg-theme-bg-primary", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }) });
|
|
97
14
|
}
|
|
@@ -199,7 +116,6 @@ function SudobilityApp({
|
|
|
199
116
|
storageKeyPrefix = "sudobility",
|
|
200
117
|
RouterWrapper
|
|
201
118
|
}) {
|
|
202
|
-
const i18nToUse = i18nInstance ?? getI18n();
|
|
203
119
|
const NetworkProviderComponent = NetworkProviderProp === false ? null : NetworkProviderProp ?? DefaultNetworkProvider;
|
|
204
120
|
const PageTrackerComponent = PageTrackerProp === false ? null : PageTrackerProp ?? DefaultPageTracker;
|
|
205
121
|
const ThemeProviderComponent = ThemeProviderProp ?? createDefaultThemeProvider(storageKeyPrefix);
|
|
@@ -223,7 +139,7 @@ function SudobilityApp({
|
|
|
223
139
|
routerContent = /* @__PURE__ */ jsx(NetworkProviderComponent, { children: routerContent });
|
|
224
140
|
}
|
|
225
141
|
routerContent = /* @__PURE__ */ jsx(ThemeProviderComponent, { children: routerContent });
|
|
226
|
-
routerContent = /* @__PURE__ */ jsx(I18nextProvider, { i18n:
|
|
142
|
+
routerContent = /* @__PURE__ */ jsx(I18nextProvider, { i18n: i18nInstance, children: routerContent });
|
|
227
143
|
return /* @__PURE__ */ jsx(HelmetProvider, { children: routerContent });
|
|
228
144
|
}
|
|
229
145
|
const STUB_SUBSCRIPTION_VALUE = {
|
|
@@ -248,8 +164,6 @@ export {
|
|
|
248
164
|
SudobilityApp as S,
|
|
249
165
|
SafeSubscriptionContext as a,
|
|
250
166
|
STUB_SUBSCRIPTION_VALUE as b,
|
|
251
|
-
getI18n as g,
|
|
252
|
-
initializeI18n as i,
|
|
253
167
|
useSafeSubscription as u
|
|
254
168
|
};
|
|
255
|
-
//# sourceMappingURL=SafeSubscriptionContext-
|
|
169
|
+
//# sourceMappingURL=SafeSubscriptionContext-CNuEKeqJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SafeSubscriptionContext-CNuEKeqJ.js","sources":["../src/components/app/SudobilityApp.tsx","../src/components/subscription/SafeSubscriptionContext.tsx"],"sourcesContent":["/**\n * SudobilityApp - Base app wrapper for all Sudobility apps\n *\n * Provides common infrastructure with sensible defaults:\n * - HelmetProvider for SEO\n * - I18nextProvider for localization\n * - ThemeProvider for theming (built-in default)\n * - NetworkProvider for online/offline status (built-in default)\n * - QueryClientProvider for TanStack Query (built-in default)\n * - ToastProvider for notifications (built-in default)\n * - BrowserRouter for routing\n * - InfoBanner for system notifications (built-in default)\n *\n * All providers have sensible defaults - only pass props to override.\n */\nimport {\n Suspense,\n ComponentType,\n ReactNode,\n useMemo,\n useRef,\n useEffect,\n} from 'react';\nimport { BrowserRouter } from 'react-router-dom';\nimport {\n QueryClient,\n QueryClientProvider,\n type QueryClient as QueryClientType,\n} from '@tanstack/react-query';\nimport { HelmetProvider } from 'react-helmet-async';\nimport { I18nextProvider } from 'react-i18next';\nimport type { i18n } from 'i18next';\nimport { useLocation } from 'react-router-dom';\nimport { NetworkProvider } from '@sudobility/devops-components';\nimport { getNetworkService, getFirebaseService } from '@sudobility/di/web';\nimport { InfoBanner } from '@sudobility/di_web';\nimport {\n ThemeProvider as SharedThemeProvider,\n Theme,\n FontSize,\n} from '@sudobility/components';\nimport {\n ToastProvider as SharedToastProvider,\n ToastContainer as SharedToastContainer,\n useToast,\n} from '@sudobility/components/ui/toast';\n\n/**\n * QueryClient type that's compatible across different package versions.\n * Uses structural typing to avoid cross-package type conflicts when using bun link.\n */\ntype QueryClientLike = {\n getDefaultOptions: () => unknown;\n getQueryCache: () => unknown;\n getMutationCache: () => unknown;\n};\n\n/**\n * i18n type that's compatible across different package versions.\n * Uses structural typing to avoid cross-package type conflicts when using bun link.\n */\ntype I18nLike = {\n language: string;\n languages: readonly string[];\n\n t: (...args: any[]) => any;\n};\n\nexport interface SudobilityAppProps {\n /** App routes and content */\n children: ReactNode;\n\n /**\n * i18next instance for localization.\n * Each app must pass its own configured i18n instance.\n */\n i18n: I18nLike;\n\n /**\n * TanStack Query client instance (optional).\n * Defaults to a QueryClient with sensible settings.\n */\n queryClient?: QueryClientLike;\n\n /**\n * Custom ThemeProvider component (optional).\n * Defaults to SharedThemeProvider from @sudobility/components.\n */\n ThemeProvider?: ComponentType<{ children: ReactNode }>;\n\n /**\n * Custom ToastProvider component (optional).\n * Defaults to ToastProvider from @sudobility/components.\n */\n ToastProvider?: ComponentType<{ children: ReactNode }>;\n\n /**\n * Toast container component rendered after routes (optional).\n * Defaults to a built-in container that renders toasts from context at bottom-right.\n */\n ToastContainer?: ComponentType;\n\n /**\n * Whether to show the InfoBanner for notifications/errors.\n * Defaults to true. Set to false to disable.\n */\n showInfoBanner?: boolean;\n\n /** Custom loading fallback for Suspense (optional) */\n LoadingFallback?: ComponentType;\n\n /**\n * Custom NetworkProvider component (optional).\n * By default, uses the built-in NetworkProvider with getNetworkService().\n * Set to false to disable network status tracking entirely.\n */\n NetworkProvider?: ComponentType<{ children: ReactNode }> | false;\n\n /**\n * Page tracker component for analytics (optional).\n * By default, uses Firebase Analytics to track page_view events.\n * Pass a custom component to override, or false to disable.\n */\n PageTracker?: ComponentType | false;\n\n /**\n * Additional providers to wrap around the router content.\n * These are rendered inside ToastProvider but outside BrowserRouter.\n * Use this for app-specific providers like ApiProvider, SettingsProvider, etc.\n */\n AppProviders?: ComponentType<{ children: ReactNode }>;\n\n /**\n * Storage key prefix for theme persistence (optional).\n * Defaults to \"sudobility\". Used for localStorage keys (e.g., \"sudobility-theme\").\n * Since localStorage is origin-scoped, apps on different domains don't need\n * different prefixes. Only override if running multiple app instances on the\n * same origin or if you want app-specific debugging visibility.\n */\n storageKeyPrefix?: string;\n\n /**\n * Custom router wrapper component (optional).\n * Defaults to BrowserRouter. Pass a fragment wrapper `({ children }) => <>{children}</>`\n * to skip the router entirely (useful when nesting inside an existing router).\n */\n RouterWrapper?: ComponentType<{ children: ReactNode }>;\n}\n\n/**\n * Default loading fallback component\n */\nfunction DefaultLoadingFallback() {\n return (\n <div className='min-h-screen flex items-center justify-center bg-theme-bg-primary'>\n <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600' />\n </div>\n );\n}\n\n/**\n * Default page tracker using Firebase Analytics.\n * Tracks page_view events on route changes.\n */\nfunction DefaultPageTracker(): null {\n const location = useLocation();\n const previousPathRef = useRef<string | null>(null);\n\n useEffect(() => {\n const currentPath = location.pathname;\n\n // Skip if same path\n if (previousPathRef.current === currentPath) {\n return;\n }\n\n previousPathRef.current = currentPath;\n\n // Track page view via Firebase Analytics\n try {\n const firebaseService = getFirebaseService();\n if (firebaseService?.analytics?.isSupported()) {\n firebaseService.analytics.logEvent('page_view', {\n page_path: currentPath,\n page_location: window.location.href,\n });\n }\n } catch {\n // Firebase not configured - silently skip tracking\n }\n }, [location.pathname]);\n\n return null;\n}\n\n/**\n * Default network provider that uses getNetworkService()\n */\nfunction DefaultNetworkProvider({ children }: { children: ReactNode }) {\n const networkService = useMemo(() => getNetworkService(), []);\n return (\n <NetworkProvider networkService={networkService}>\n {children}\n </NetworkProvider>\n );\n}\n\n/**\n * Create default QueryClient with sensible settings\n */\nfunction createDefaultQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n // Smart retry: don't retry on 4xx client errors, retry up to 3 times otherwise\n retry: (failureCount, error: unknown) => {\n const statusCode = (error as { statusCode?: number })?.statusCode;\n if (statusCode && statusCode >= 400 && statusCode < 500) {\n return false;\n }\n return failureCount < 3;\n },\n // Exponential backoff: 1s, 2s, 4s... up to 30s\n retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),\n refetchOnWindowFocus: true,\n },\n mutations: {\n retry: false,\n },\n },\n });\n}\n\n// Singleton default QueryClient\nlet defaultQueryClient: QueryClient | null = null;\nfunction getDefaultQueryClient(): QueryClient {\n if (!defaultQueryClient) {\n defaultQueryClient = createDefaultQueryClient();\n }\n return defaultQueryClient;\n}\n\n/**\n * Default theme provider using SharedThemeProvider from @sudobility/components\n */\nfunction createDefaultThemeProvider(storageKeyPrefix: string) {\n return function DefaultThemeProvider({ children }: { children: ReactNode }) {\n return (\n <SharedThemeProvider\n themeStorageKey={`${storageKeyPrefix}-theme`}\n fontSizeStorageKey={`${storageKeyPrefix}-font-size`}\n defaultTheme={Theme.LIGHT}\n defaultFontSize={FontSize.MEDIUM}\n >\n {children}\n </SharedThemeProvider>\n );\n };\n}\n\n/**\n * Default toast provider using SharedToastProvider from @sudobility/components\n */\nfunction DefaultToastProvider({ children }: { children: ReactNode }) {\n return <SharedToastProvider>{children}</SharedToastProvider>;\n}\n\n/**\n * Default toast container that renders toasts from context.\n * Wraps SharedToastContainer with useToast consumer.\n */\nfunction DefaultToastContainer() {\n const { toasts, removeToast } = useToast();\n\n if (toasts.length === 0) return null;\n\n return (\n <SharedToastContainer\n toasts={toasts}\n onDismiss={removeToast}\n position='bottom-right'\n />\n );\n}\n\n/**\n * SudobilityApp - Base app wrapper for all Sudobility apps\n *\n * @example\n * ```tsx\n * // Minimal usage - only i18n is required\n * import { SudobilityApp } from '@sudobility/building_blocks';\n * import i18n from './i18n';\n *\n * function App() {\n * return (\n * <SudobilityApp i18n={i18n}>\n * <Routes>\n * <Route path=\"/\" element={<HomePage />} />\n * </Routes>\n * </SudobilityApp>\n * );\n * }\n *\n * // With custom providers\n * function App() {\n * return (\n * <SudobilityApp\n * i18n={i18n}\n * ThemeProvider={MyCustomThemeProvider}\n * AppProviders={ApiProvider}\n * storageKeyPrefix=\"myapp\"\n * >\n * <Routes>\n * <Route path=\"/\" element={<HomePage />} />\n * </Routes>\n * </SudobilityApp>\n * );\n * }\n * ```\n */\nexport function SudobilityApp({\n children,\n i18n: i18nInstance,\n queryClient,\n ThemeProvider: ThemeProviderProp,\n ToastProvider: ToastProviderProp,\n ToastContainer,\n showInfoBanner = true,\n LoadingFallback = DefaultLoadingFallback,\n NetworkProvider: NetworkProviderProp,\n PageTracker: PageTrackerProp,\n AppProviders,\n storageKeyPrefix = 'sudobility',\n RouterWrapper,\n}: SudobilityAppProps) {\n // Determine which providers to use (custom or default)\n const NetworkProviderComponent =\n NetworkProviderProp === false\n ? null\n : (NetworkProviderProp ?? DefaultNetworkProvider);\n\n const PageTrackerComponent =\n PageTrackerProp === false ? null : (PageTrackerProp ?? DefaultPageTracker);\n\n const ThemeProviderComponent =\n ThemeProviderProp ?? createDefaultThemeProvider(storageKeyPrefix);\n\n const ToastProviderComponent = ToastProviderProp ?? DefaultToastProvider;\n\n const ToastContainerComponent = ToastContainer ?? DefaultToastContainer;\n\n const queryClientInstance = queryClient ?? getDefaultQueryClient();\n\n // Use custom RouterWrapper or default BrowserRouter\n const RouterWrapperComponent = RouterWrapper ?? BrowserRouter;\n\n // Build the router content\n let routerContent: ReactNode = (\n <>\n {PageTrackerComponent && <PageTrackerComponent />}\n <Suspense fallback={<LoadingFallback />}>{children}</Suspense>\n <ToastContainerComponent />\n {showInfoBanner && <InfoBanner />}\n </>\n );\n\n // Wrap with Router\n routerContent = (\n <RouterWrapperComponent>{routerContent}</RouterWrapperComponent>\n );\n\n // Wrap with AppProviders if provided\n if (AppProviders) {\n routerContent = <AppProviders>{routerContent}</AppProviders>;\n }\n\n // Wrap with ToastProvider\n routerContent = (\n <ToastProviderComponent>{routerContent}</ToastProviderComponent>\n );\n\n // Wrap with QueryClientProvider\n routerContent = (\n <QueryClientProvider client={queryClientInstance as QueryClientType}>\n {routerContent}\n </QueryClientProvider>\n );\n\n // Wrap with NetworkProvider (uses default if not explicitly disabled)\n if (NetworkProviderComponent) {\n routerContent = (\n <NetworkProviderComponent>{routerContent}</NetworkProviderComponent>\n );\n }\n\n // Wrap with ThemeProvider\n routerContent = (\n <ThemeProviderComponent>{routerContent}</ThemeProviderComponent>\n );\n\n // Wrap with I18nextProvider\n routerContent = (\n <I18nextProvider i18n={i18nInstance as i18n}>\n {routerContent}\n </I18nextProvider>\n );\n\n // Wrap with HelmetProvider\n return <HelmetProvider>{routerContent}</HelmetProvider>;\n}\n\nexport default SudobilityApp;\n","/**\n * Safe Subscription Context\n *\n * Provides a context that can be safely used even when subscription\n * provider isn't loaded yet. Returns stub values for unauthenticated users.\n */\n\nimport { createContext, useContext } from 'react';\nimport type { SubscriptionContextValue } from '@sudobility/subscription-components';\n\n/**\n * Stub subscription value for unauthenticated users or when\n * subscription provider hasn't loaded yet.\n */\nexport const STUB_SUBSCRIPTION_VALUE: SubscriptionContextValue = {\n products: [],\n currentSubscription: null,\n isLoading: false,\n error: null,\n initialize: () => Promise.resolve(),\n purchase: () => Promise.resolve(false),\n restore: () => Promise.resolve(false),\n refresh: () => Promise.resolve(),\n clearError: () => {},\n};\n\n/**\n * Context that provides subscription state with safe defaults.\n */\nexport const SafeSubscriptionContext = createContext<SubscriptionContextValue>(\n STUB_SUBSCRIPTION_VALUE\n);\n\n/**\n * Hook to safely access subscription context.\n * Returns stub values if provider isn't available.\n */\nexport function useSafeSubscription(): SubscriptionContextValue {\n return useContext(SafeSubscriptionContext);\n}\n"],"names":["SharedThemeProvider","SharedToastProvider","SharedToastContainer","ToastContainer"],"mappings":"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"}
|
|
@@ -36,11 +36,10 @@ export interface SudobilityAppProps {
|
|
|
36
36
|
/** App routes and content */
|
|
37
37
|
children: ReactNode;
|
|
38
38
|
/**
|
|
39
|
-
* i18next instance for localization
|
|
40
|
-
*
|
|
41
|
-
* Pass your own if you need custom configuration.
|
|
39
|
+
* i18next instance for localization.
|
|
40
|
+
* Each app must pass its own configured i18n instance.
|
|
42
41
|
*/
|
|
43
|
-
i18n
|
|
42
|
+
i18n: I18nLike;
|
|
44
43
|
/**
|
|
45
44
|
* TanStack Query client instance (optional).
|
|
46
45
|
* Defaults to a QueryClient with sensible settings.
|
|
@@ -28,6 +28,8 @@ export interface AppPageLayoutProps extends VariantProps<typeof layoutVariants>
|
|
|
28
28
|
contentClassName?: string;
|
|
29
29
|
/** Custom className for the main element */
|
|
30
30
|
mainClassName?: string;
|
|
31
|
+
/** Optional aspect ratio (width / height) for content area. When set, children are placed inside a container with fixed aspect ratio using AspectFit behavior. */
|
|
32
|
+
aspectRatio?: number;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* AppPageLayout - Layout wrapper combining TopBar, Breadcrumbs, Content, and Footer.
|
package/dist/firebase.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { S as SudobilityApp, a as SafeSubscriptionContext, b as STUB_SUBSCRIPTION_VALUE } from "./SafeSubscriptionContext-
|
|
2
|
+
import { S as SudobilityApp, a as SafeSubscriptionContext, b as STUB_SUBSCRIPTION_VALUE } from "./SafeSubscriptionContext-CNuEKeqJ.js";
|
|
3
3
|
import { useAuthStatus, AuthProvider } from "@sudobility/auth-components";
|
|
4
4
|
import { getFirebaseAuth, FirebaseAuthNetworkService, initializeFirebaseAuth, getFirebaseErrorMessage } from "@sudobility/auth_lib";
|
|
5
5
|
import { createContext, useState, useEffect, useCallback, useRef, useMemo, useContext, lazy, Suspense } from "react";
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,15 @@ import { ChevronDownIcon, CalendarDaysIcon, PaintBrushIcon, LanguageIcon, Chevro
|
|
|
7
7
|
import { GRADIENT_CLASSES, textVariants } from "@sudobility/design";
|
|
8
8
|
import { cva } from "class-variance-authority";
|
|
9
9
|
import { createUserWithEmailAndPassword, signInWithEmailAndPassword, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
|
|
10
|
-
import { b, a, S,
|
|
10
|
+
import { b, a, S, u } from "./SafeSubscriptionContext-CNuEKeqJ.js";
|
|
11
11
|
import { SubscriptionLayout, SubscriptionTile, SegmentedControl } from "@sudobility/subscription-components";
|
|
12
12
|
import { useSubscriptionPeriods, useSubscriptionForPeriod, useSubscribable, useUserSubscription, refreshSubscription } from "@sudobility/subscription_lib";
|
|
13
|
-
import
|
|
13
|
+
import i18n from "i18next";
|
|
14
14
|
import { default as default2 } from "i18next";
|
|
15
|
+
import { initReactI18next } from "react-i18next";
|
|
16
|
+
import Backend from "i18next-http-backend";
|
|
17
|
+
import LanguageDetector from "i18next-browser-languagedetector";
|
|
18
|
+
import { ToastContainer, ToastProvider, useToast } from "@sudobility/components/ui/toast";
|
|
15
19
|
function cn(...inputs) {
|
|
16
20
|
return twMerge(clsx(inputs));
|
|
17
21
|
}
|
|
@@ -989,8 +993,17 @@ const AppPageLayout = ({
|
|
|
989
993
|
layoutMode = "standard",
|
|
990
994
|
className,
|
|
991
995
|
contentClassName,
|
|
992
|
-
mainClassName
|
|
996
|
+
mainClassName,
|
|
997
|
+
aspectRatio
|
|
993
998
|
}) => {
|
|
999
|
+
const content = aspectRatio ? /* @__PURE__ */ jsx(
|
|
1000
|
+
"div",
|
|
1001
|
+
{
|
|
1002
|
+
className: "mx-auto max-h-full max-w-full overflow-auto",
|
|
1003
|
+
style: { aspectRatio },
|
|
1004
|
+
children
|
|
1005
|
+
}
|
|
1006
|
+
) : children;
|
|
994
1007
|
return /* @__PURE__ */ jsx(LayoutProvider, { mode: layoutMode, children: /* @__PURE__ */ jsxs("div", { className: cn(layoutVariants({ background }), className), children: [
|
|
995
1008
|
/* @__PURE__ */ jsx("header", { children: topBar }),
|
|
996
1009
|
breadcrumbs && breadcrumbs.items && breadcrumbs.items.length > 0 && /* @__PURE__ */ jsx(AppBreadcrumbs, { ...breadcrumbs }),
|
|
@@ -1003,7 +1016,7 @@ const AppPageLayout = ({
|
|
|
1003
1016
|
paddingClasses[contentPadding],
|
|
1004
1017
|
contentClassName
|
|
1005
1018
|
),
|
|
1006
|
-
children
|
|
1019
|
+
children: content
|
|
1007
1020
|
}
|
|
1008
1021
|
) }),
|
|
1009
1022
|
footer && /* @__PURE__ */ jsx("footer", { children: footer })
|
|
@@ -2508,6 +2521,86 @@ function AppPricingPage({
|
|
|
2508
2521
|
] })
|
|
2509
2522
|
] });
|
|
2510
2523
|
}
|
|
2524
|
+
const DEFAULT_SUPPORTED_LANGUAGES = ["en"];
|
|
2525
|
+
const DEFAULT_NAMESPACES = [
|
|
2526
|
+
"common",
|
|
2527
|
+
"home",
|
|
2528
|
+
"pricing",
|
|
2529
|
+
"docs",
|
|
2530
|
+
"dashboard",
|
|
2531
|
+
"auth",
|
|
2532
|
+
"privacy",
|
|
2533
|
+
"terms",
|
|
2534
|
+
"settings"
|
|
2535
|
+
];
|
|
2536
|
+
function detectLanguageFromPath(supportedLanguages) {
|
|
2537
|
+
if (typeof window === "undefined") {
|
|
2538
|
+
return "en";
|
|
2539
|
+
}
|
|
2540
|
+
const pathLang = window.location.pathname.split("/")[1];
|
|
2541
|
+
if (pathLang && supportedLanguages.includes(pathLang)) {
|
|
2542
|
+
return pathLang;
|
|
2543
|
+
}
|
|
2544
|
+
try {
|
|
2545
|
+
const stored = localStorage.getItem("language");
|
|
2546
|
+
if (stored && supportedLanguages.includes(stored)) {
|
|
2547
|
+
return stored;
|
|
2548
|
+
}
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
return "en";
|
|
2552
|
+
}
|
|
2553
|
+
let initialized = false;
|
|
2554
|
+
function initializeI18n(config = {}) {
|
|
2555
|
+
if (initialized) {
|
|
2556
|
+
return i18n;
|
|
2557
|
+
}
|
|
2558
|
+
initialized = true;
|
|
2559
|
+
const {
|
|
2560
|
+
supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
|
|
2561
|
+
namespaces = DEFAULT_NAMESPACES,
|
|
2562
|
+
defaultNamespace = "common",
|
|
2563
|
+
loadPath = "/locales/{{lng}}/{{ns}}.json",
|
|
2564
|
+
debug = false
|
|
2565
|
+
} = config;
|
|
2566
|
+
i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({
|
|
2567
|
+
lng: detectLanguageFromPath(supportedLanguages),
|
|
2568
|
+
fallbackLng: {
|
|
2569
|
+
zh: ["zh", "en"],
|
|
2570
|
+
"zh-hant": ["zh-hant", "zh", "en"],
|
|
2571
|
+
default: ["en"]
|
|
2572
|
+
},
|
|
2573
|
+
initImmediate: false,
|
|
2574
|
+
supportedLngs: supportedLanguages,
|
|
2575
|
+
debug,
|
|
2576
|
+
nonExplicitSupportedLngs: true,
|
|
2577
|
+
interpolation: {
|
|
2578
|
+
escapeValue: false
|
|
2579
|
+
},
|
|
2580
|
+
backend: {
|
|
2581
|
+
loadPath
|
|
2582
|
+
},
|
|
2583
|
+
detection: {
|
|
2584
|
+
order: ["path", "localStorage", "navigator"],
|
|
2585
|
+
caches: ["localStorage"],
|
|
2586
|
+
lookupLocalStorage: "language",
|
|
2587
|
+
lookupFromPathIndex: 0
|
|
2588
|
+
},
|
|
2589
|
+
load: "languageOnly",
|
|
2590
|
+
preload: [],
|
|
2591
|
+
cleanCode: false,
|
|
2592
|
+
lowerCaseLng: false,
|
|
2593
|
+
defaultNS: defaultNamespace,
|
|
2594
|
+
ns: namespaces
|
|
2595
|
+
});
|
|
2596
|
+
return i18n;
|
|
2597
|
+
}
|
|
2598
|
+
function getI18n() {
|
|
2599
|
+
if (!initialized) {
|
|
2600
|
+
initializeI18n();
|
|
2601
|
+
}
|
|
2602
|
+
return i18n;
|
|
2603
|
+
}
|
|
2511
2604
|
export {
|
|
2512
2605
|
AppBreadcrumbs,
|
|
2513
2606
|
AppFooter,
|
|
@@ -2536,9 +2629,9 @@ export {
|
|
|
2536
2629
|
ToastContainer,
|
|
2537
2630
|
ToastProvider,
|
|
2538
2631
|
cn,
|
|
2539
|
-
|
|
2632
|
+
getI18n,
|
|
2540
2633
|
default2 as i18n,
|
|
2541
|
-
|
|
2634
|
+
initializeI18n,
|
|
2542
2635
|
isRTL,
|
|
2543
2636
|
u as useSafeSubscription,
|
|
2544
2637
|
useToast
|