@myelophone/nuxt 0.18.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 (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. package/vite/plugins/postcss-viewport-fallback.ts +49 -0
@@ -0,0 +1,37 @@
1
+ const activeModals = ref<string[]>([]);
2
+
3
+ export const useModalState = () => {
4
+ const register = (id: string) => {
5
+ if (!activeModals.value.includes(id)) {
6
+ activeModals.value.push(id);
7
+ }
8
+ };
9
+
10
+ const unregister = (id: string) => {
11
+ activeModals.value = activeModals.value.filter((m) => m !== id);
12
+ };
13
+
14
+ const isTop = (id: string) => {
15
+ return activeModals.value[activeModals.value.length - 1] === id;
16
+ };
17
+
18
+ watch(
19
+ () => activeModals.value.length,
20
+ (count) => {
21
+ if (import.meta.client) {
22
+ if (count > 0) {
23
+ document.body.style.overflow = 'hidden';
24
+ } else {
25
+ document.body.style.overflow = '';
26
+ }
27
+ }
28
+ },
29
+ );
30
+
31
+ return {
32
+ activeModals,
33
+ register,
34
+ unregister,
35
+ isTop,
36
+ };
37
+ };
@@ -0,0 +1,278 @@
1
+ type TranslationMap = Record<string, any>;
2
+ interface NamespaceCache {
3
+ [key: string]: TranslationMap;
4
+ }
5
+ type InterpolationParams = Record<string, string | number>;
6
+
7
+ export function useMultiLang(namespaces: string | string[]) {
8
+ const nuxtApp = useNuxtApp();
9
+ const pinia = nuxtApp.$pinia;
10
+ const settingsStore = useSettingsStore(pinia);
11
+
12
+ const activeLang = computed(() => settingsStore.currentLocale);
13
+ const defaultLang = settingsStore.defaultLocale || 'en';
14
+
15
+ const nsArray = (Array.isArray(namespaces) ? namespaces : [namespaces]).map(
16
+ (ns) => ns.replace(/^\/+|\/+$/g, ''),
17
+ );
18
+
19
+ const translationsState = useState<Record<string, NamespaceCache>>(
20
+ 'i18n_translations',
21
+ () => ({}),
22
+ );
23
+
24
+ const formatNumber = (num: number, l: string) =>
25
+ new Intl.NumberFormat(l).format(num);
26
+
27
+ const compactNumber = (num: number, l: string) =>
28
+ new Intl.NumberFormat(l, {
29
+ notation: 'compact',
30
+ maximumFractionDigits: 1,
31
+ }).format(num);
32
+
33
+ const isProdDist =
34
+ process.env.PROD_DIST === 'true' ||
35
+ import.meta.env.PROD_DIST === 'true';
36
+
37
+ const localesAlias = import.meta.glob('#myelophone-nuxt-locales/**/*.json');
38
+ const localesPlayground = import.meta.glob(
39
+ '~~/playground/locales/**/*.json',
40
+ );
41
+ const localesApp = import.meta.glob('~/locales/**/*.json');
42
+
43
+ const allLocales = {
44
+ ...localesAlias,
45
+ ...localesApp,
46
+ ...(!isProdDist ? localesPlayground : {}),
47
+ };
48
+
49
+ const isPlainObject = (value: unknown): value is Record<string, any> =>
50
+ Object.prototype.toString.call(value) === '[object Object]';
51
+
52
+ const deepMerge = (
53
+ target: Record<string, any>,
54
+ source: Record<string, any>,
55
+ ) => {
56
+ Object.entries(source).forEach(([key, value]) => {
57
+ if (isPlainObject(value) && isPlainObject(target[key])) {
58
+ deepMerge(target[key], value);
59
+ return;
60
+ }
61
+
62
+ target[key] = value;
63
+ });
64
+
65
+ return target;
66
+ };
67
+
68
+ const expandDottedKeys = (value: any): any => {
69
+ if (Array.isArray(value)) {
70
+ return value.map(expandDottedKeys);
71
+ }
72
+
73
+ if (!isPlainObject(value)) {
74
+ return value;
75
+ }
76
+
77
+ const result: Record<string, any> = {};
78
+
79
+ Object.entries(value).forEach(([key, rawValue]) => {
80
+ const expandedValue = expandDottedKeys(rawValue);
81
+ const keyParts = key.split('.');
82
+
83
+ if (keyParts.length === 1) {
84
+ if (
85
+ isPlainObject(result[key]) &&
86
+ isPlainObject(expandedValue)
87
+ ) {
88
+ deepMerge(result[key], expandedValue);
89
+ } else {
90
+ result[key] = expandedValue;
91
+ }
92
+
93
+ return;
94
+ }
95
+
96
+ let current = result;
97
+
98
+ keyParts.forEach((part, index) => {
99
+ const isLast = index === keyParts.length - 1;
100
+
101
+ if (isLast) {
102
+ if (
103
+ isPlainObject(current[part]) &&
104
+ isPlainObject(expandedValue)
105
+ ) {
106
+ deepMerge(current[part], expandedValue);
107
+ } else {
108
+ current[part] = expandedValue;
109
+ }
110
+
111
+ return;
112
+ }
113
+
114
+ if (!isPlainObject(current[part])) {
115
+ current[part] = {};
116
+ }
117
+
118
+ current = current[part];
119
+ });
120
+ });
121
+
122
+ return result;
123
+ };
124
+
125
+ const loadNamespace = async (lang: string, ns: string) => {
126
+ if (translationsState.value[lang]?.[ns]) return;
127
+
128
+ const targetSuffix = `locales/${lang}/${ns}.json`;
129
+ const matchEntry = Object.entries(allLocales).find(([filePath]) =>
130
+ filePath.replace(/\\/g, '/').endsWith(targetSuffix),
131
+ );
132
+
133
+ if (!matchEntry) return;
134
+
135
+ try {
136
+ const mod: any = await matchEntry[1]();
137
+ const data = expandDottedKeys(mod.default || mod);
138
+
139
+ if (!translationsState.value[lang]) {
140
+ translationsState.value[lang] = {};
141
+ }
142
+
143
+ translationsState.value[lang][ns] = data;
144
+ } catch (e) {
145
+ console.error(`Failed to load: ${ns}`, e);
146
+ }
147
+ };
148
+
149
+ const { pending, refresh } = useAsyncData(
150
+ `i18n-${activeLang.value}-${nsArray.join('_')}`,
151
+ async () => {
152
+ const mainPromises = nsArray.map((ns) =>
153
+ loadNamespace(activeLang.value, ns),
154
+ );
155
+ await Promise.all(mainPromises);
156
+
157
+ if (import.meta.server && activeLang.value !== defaultLang) {
158
+ const fallbackPromises = [];
159
+
160
+ for (const ns of nsArray) {
161
+ const mainDict =
162
+ translationsState.value[activeLang.value]?.[ns];
163
+
164
+ if (!mainDict) {
165
+ fallbackPromises.push(loadNamespace(defaultLang, ns));
166
+ }
167
+ }
168
+
169
+ await Promise.all(fallbackPromises);
170
+ }
171
+
172
+ return null;
173
+ },
174
+ { watch: [activeLang] },
175
+ );
176
+
177
+ const t = (
178
+ key: string,
179
+ arg1?: InterpolationParams | number,
180
+ arg2?: number,
181
+ forceRule?: string,
182
+ ): string => {
183
+ let params: InterpolationParams = {};
184
+ let count: number | undefined;
185
+
186
+ if (typeof arg1 === 'number') {
187
+ count = arg1;
188
+ } else if (typeof arg1 === 'object' && arg1 !== null) {
189
+ params = arg1;
190
+ if (typeof arg2 === 'number') count = arg2;
191
+ }
192
+
193
+ const lang = activeLang.value;
194
+
195
+ const lookup = (l: string) => {
196
+ const nsCache = translationsState.value[l];
197
+ if (!nsCache) return undefined;
198
+
199
+ for (const ns of nsArray) {
200
+ if (key === ns || key.startsWith(`${ns}.`)) {
201
+ const dict = nsCache[ns];
202
+ if (!dict) continue;
203
+
204
+ const internalKey = key.slice(ns.length + 1);
205
+ if (!internalKey && typeof count !== 'number')
206
+ return undefined;
207
+
208
+ if (typeof count === 'number') {
209
+ const pluralRule =
210
+ forceRule || new Intl.PluralRules(l).select(count);
211
+
212
+ const searchKeys =
213
+ count === 0
214
+ ? [
215
+ `${internalKey}_zero`,
216
+ `${internalKey}_${pluralRule}`,
217
+ `${internalKey}_other`,
218
+ ]
219
+ : [
220
+ `${internalKey}_${pluralRule}`,
221
+ `${internalKey}_other`,
222
+ ];
223
+
224
+ for (const pk of searchKeys) {
225
+ const val = pk
226
+ .split('.')
227
+ .reduce((acc: any, k) => acc?.[k], dict);
228
+ if (typeof val === 'string') return val;
229
+ }
230
+ }
231
+
232
+ const value = internalKey
233
+ .split('.')
234
+ .reduce((acc: any, k) => acc?.[k], dict);
235
+ if (typeof value === 'string') return value;
236
+ }
237
+ }
238
+ return undefined;
239
+ };
240
+
241
+ let val = lookup(activeLang.value);
242
+
243
+ if (!val && activeLang.value !== defaultLang) {
244
+ val = lookup(defaultLang);
245
+
246
+ if (!val && import.meta.client) {
247
+ const ns = nsArray.find((n) => key.startsWith(n + '.'));
248
+ if (ns) loadNamespace(defaultLang, ns);
249
+ }
250
+ }
251
+
252
+ const finalStr = val || key;
253
+
254
+ return finalStr.replace(/\{(\w+)\}/g, (_, k) => {
255
+ if (k === 'count' && typeof count === 'number')
256
+ return formatNumber(count, lang);
257
+ return params[k] !== undefined ? String(params[k]) : `{${k}}`;
258
+ });
259
+ };
260
+
261
+ const tn = (
262
+ key: string,
263
+ count: number,
264
+ params: InterpolationParams = {},
265
+ ): string => {
266
+ const lang = activeLang.value;
267
+
268
+ const rule = count >= 1000 ? 'many' : undefined;
269
+
270
+ const translated = t(key, params, count, rule);
271
+
272
+ const formattedCount = compactNumber(count, lang);
273
+
274
+ return translated.replace(formatNumber(count, lang), formattedCount);
275
+ };
276
+
277
+ return { t, tn, loadPromise: pending, refresh };
278
+ }
@@ -0,0 +1,49 @@
1
+ export const useNetwork = () => {
2
+ const effectiveType = ref<string>('4g');
3
+ const isSlowConnection = ref(false);
4
+ const isOnline = ref(true);
5
+
6
+ const updateStatus = () => {
7
+ if (import.meta.server) return;
8
+ isOnline.value = navigator.onLine;
9
+
10
+ const conn =
11
+ (navigator as any).connection ||
12
+ (navigator as any).mozConnection ||
13
+ (navigator as any).webkitConnection;
14
+
15
+ if (conn) {
16
+ effectiveType.value = conn.effectiveType;
17
+ isSlowConnection.value =
18
+ ['slow-2g', '2g', '3g'].includes(conn.effectiveType) ||
19
+ conn.saveData === true;
20
+ }
21
+ };
22
+
23
+ if (import.meta.client) {
24
+ updateStatus();
25
+
26
+ const conn = (navigator as any).connection;
27
+
28
+ const addListeners = () => {
29
+ if (conn) conn.addEventListener('change', updateStatus);
30
+ window.addEventListener('online', updateStatus);
31
+ window.addEventListener('offline', updateStatus);
32
+ };
33
+
34
+ const removeListeners = () => {
35
+ if (conn) conn.removeEventListener('change', updateStatus);
36
+ window.removeEventListener('online', updateStatus);
37
+ window.removeEventListener('offline', updateStatus);
38
+ };
39
+
40
+ if (getCurrentInstance()) {
41
+ onMounted(addListeners);
42
+ onUnmounted(removeListeners);
43
+ } else {
44
+ addListeners();
45
+ }
46
+ }
47
+
48
+ return { effectiveType, isSlowConnection, isOnline };
49
+ };
@@ -0,0 +1,5 @@
1
+ import { version } from 'nuxt/package.json';
2
+
3
+ export function useNuxtVersion() {
4
+ return version || 'v4.x';
5
+ }
@@ -0,0 +1,5 @@
1
+ // biome-ignore lint/suspicious/noExplicitAny: generic type
2
+ export function usePiniaStore<S extends (...args: any) => any>(storeFn: S) {
3
+ const pinia = useNuxtApp().$pinia;
4
+ return storeFn(pinia);
5
+ }
@@ -0,0 +1,57 @@
1
+ export const usePopupControl = (
2
+ popupId: string,
3
+ daysUntilReappear: number = 7,
4
+ ) => {
5
+ const isOpen = useState<boolean>(`popup-state-${popupId}`, () => false);
6
+
7
+ const outputCookie = useCookie(`popup-viewed-${popupId}`, {
8
+ maxAge: 60 * 60 * 24 * daysUntilReappear,
9
+ path: '/',
10
+ });
11
+
12
+ const open = (force: boolean = false) => {
13
+ if (force) {
14
+ isOpen.value = true;
15
+ } else {
16
+ if (!outputCookie.value) {
17
+ isOpen.value = true;
18
+ }
19
+ }
20
+ };
21
+
22
+ const close = (persist: boolean = true) => {
23
+ isOpen.value = false;
24
+ if (persist) {
25
+ outputCookie.value = new Date().toISOString();
26
+ }
27
+ };
28
+
29
+ const reset = () => {
30
+ outputCookie.value = null;
31
+ };
32
+
33
+ const handleKeydown = (e: KeyboardEvent) => {
34
+ if (isOpen.value && e.key === 'Escape') {
35
+ close(false);
36
+ }
37
+ };
38
+
39
+ onMounted(() => {
40
+ if (getCurrentInstance()) {
41
+ window.addEventListener('keydown', handleKeydown);
42
+ }
43
+ });
44
+
45
+ onUnmounted(() => {
46
+ if (getCurrentInstance()) {
47
+ window.removeEventListener('keydown', handleKeydown);
48
+ }
49
+ });
50
+
51
+ return {
52
+ isOpen,
53
+ open,
54
+ close,
55
+ reset,
56
+ };
57
+ };