@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,177 @@
1
+ <script setup lang="ts">
2
+ import type { CSSProperties } from "vue";
3
+
4
+ interface Props {
5
+ transparent?: boolean;
6
+ background?: string;
7
+ backgroundDark?: string;
8
+ zIndex?: number | string;
9
+ ariaLabel?: string;
10
+ class?: string;
11
+ contentClass?: string;
12
+ minimumDuration?: number;
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ transparent: false,
17
+ background: "var(--ui-bg)",
18
+ zIndex: 9999,
19
+ ariaLabel: "Page loading",
20
+ class: "",
21
+ contentClass: "",
22
+ minimumDuration: 250,
23
+ });
24
+
25
+ const nuxtApp = useNuxtApp();
26
+ const autoVisible = ref(true);
27
+ let visibleSince = import.meta.client ? performance.now() : 0;
28
+ let hideTimer: ReturnType<typeof setTimeout> | undefined;
29
+ let waitingForWindowLoad = true;
30
+
31
+ const showAutomatically = () => {
32
+ if (hideTimer) clearTimeout(hideTimer);
33
+ hideTimer = undefined;
34
+ visibleSince = import.meta.client ? performance.now() : 0;
35
+ autoVisible.value = true;
36
+ };
37
+
38
+ const hideAutomatically = () => {
39
+ if (!import.meta.client || waitingForWindowLoad) return;
40
+
41
+ const elapsed = performance.now() - visibleSince;
42
+ const minimumDuration = Number.isFinite(props.minimumDuration)
43
+ ? Math.max(0, props.minimumDuration)
44
+ : 250;
45
+ const remaining = Math.max(0, minimumDuration - elapsed);
46
+
47
+ if (hideTimer) clearTimeout(hideTimer);
48
+ hideTimer = setTimeout(() => {
49
+ autoVisible.value = false;
50
+ hideTimer = undefined;
51
+ }, remaining);
52
+ };
53
+
54
+ const removePageStartHook = nuxtApp.hook("page:start", showAutomatically);
55
+ const removePageFinishHook = nuxtApp.hook("page:finish", hideAutomatically);
56
+
57
+ const handleWindowLoad = () => {
58
+ waitingForWindowLoad = false;
59
+ hideAutomatically();
60
+ };
61
+
62
+ onMounted(() => {
63
+ if (document.readyState === "complete") {
64
+ handleWindowLoad();
65
+ } else {
66
+ window.addEventListener("load", handleWindowLoad, { once: true });
67
+ }
68
+ });
69
+
70
+ onBeforeUnmount(() => {
71
+ if (hideTimer) clearTimeout(hideTimer);
72
+ window.removeEventListener("load", handleWindowLoad);
73
+ removePageStartHook();
74
+ removePageFinishHook();
75
+ });
76
+
77
+ type FullscreenPreloaderStyles = CSSProperties &
78
+ Record<`--page-fullscreen-preloader-${string}`, string>;
79
+
80
+ const rootStyles = computed<FullscreenPreloaderStyles>(() => ({
81
+ "--page-fullscreen-preloader-background-light": props.transparent
82
+ ? "transparent"
83
+ : props.background,
84
+ "--page-fullscreen-preloader-background-dark": props.transparent
85
+ ? "transparent"
86
+ : props.backgroundDark || props.background,
87
+ "--page-fullscreen-preloader-z-index": String(props.zIndex),
88
+ }));
89
+
90
+ const rootClasses = computed(() =>
91
+ ["page-fullscreen-preloader", props.class].filter(Boolean).join(" "),
92
+ );
93
+
94
+ const contentClasses = computed(() =>
95
+ ["page-fullscreen-preloader__content", props.contentClass]
96
+ .filter(Boolean)
97
+ .join(" "),
98
+ );
99
+ </script>
100
+
101
+ <template>
102
+ <Transition name="page-fullscreen-preloader">
103
+ <div
104
+ v-if="autoVisible"
105
+ :class="rootClasses"
106
+ :style="rootStyles"
107
+ role="status"
108
+ aria-live="polite"
109
+ aria-busy="true"
110
+ :aria-label="ariaLabel"
111
+ @wheel.prevent
112
+ @touchmove.prevent
113
+ >
114
+ <div :class="contentClasses">
115
+ <slot />
116
+ </div>
117
+ </div>
118
+ </Transition>
119
+ </template>
120
+
121
+ <style>
122
+ .page-fullscreen-preloader {
123
+ position: fixed;
124
+ z-index: var(--page-fullscreen-preloader-z-index);
125
+ inset: 0;
126
+ display: none;
127
+ place-items: center;
128
+ width: 100%;
129
+ height: 100vh;
130
+ height: 100dvh;
131
+ overflow: hidden;
132
+ overscroll-behavior: contain;
133
+ background: var(--page-fullscreen-preloader-background-light);
134
+ }
135
+
136
+ html.js .page-fullscreen-preloader {
137
+ display: grid;
138
+ }
139
+
140
+ .page-fullscreen-preloader__content {
141
+ display: flex;
142
+ max-width: 100%;
143
+ max-height: 100%;
144
+ align-items: center;
145
+ justify-content: center;
146
+ padding: 1rem;
147
+ }
148
+
149
+ .page-fullscreen-preloader-leave-active {
150
+ pointer-events: none;
151
+ transition: opacity 160ms ease-out;
152
+ }
153
+
154
+ .page-fullscreen-preloader-leave-to {
155
+ opacity: 0;
156
+ }
157
+
158
+ @media (prefers-color-scheme: dark) {
159
+ .page-fullscreen-preloader {
160
+ background: var(--page-fullscreen-preloader-background-dark);
161
+ }
162
+ }
163
+
164
+ [data-theme="light"] .page-fullscreen-preloader {
165
+ background: var(--page-fullscreen-preloader-background-light);
166
+ }
167
+
168
+ [data-theme="dark"] .page-fullscreen-preloader {
169
+ background: var(--page-fullscreen-preloader-background-dark);
170
+ }
171
+
172
+ @media (prefers-reduced-motion: reduce) {
173
+ .page-fullscreen-preloader-leave-active {
174
+ transition: none;
175
+ }
176
+ }
177
+ </style>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div v-show="isLoading" id="preloader" class="preloader" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ const pinia = useNuxtApp().$pinia;
7
+ const settingsStore = useSettingsStore(pinia);
8
+ const isLoading = computed(() => settingsStore.isLoading);
9
+ </script>
10
+
11
+ <style scoped>
12
+ #preloader {
13
+ position: fixed;
14
+ top: 7px;
15
+ right: 10px;
16
+ margin: 0 auto;
17
+ }
18
+ </style>
@@ -0,0 +1,19 @@
1
+ <script setup lang="ts" generic="T">
2
+ defineProps<{
3
+ contentByLocale: Record<string, T>;
4
+ }>();
5
+
6
+ defineSlots<{
7
+ default(props: { content: T; locale: string }): unknown;
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <UiLangVisible
13
+ v-for="(content, locale) in contentByLocale"
14
+ :key="locale"
15
+ :only="locale"
16
+ >
17
+ <slot :content="content" :locale="locale" />
18
+ </UiLangVisible>
19
+ </template>
File without changes
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ user: string;
4
+ domain: string;
5
+ tld?: string;
6
+ subject?: string;
7
+ class?: string;
8
+ placeholder?: string;
9
+ }
10
+
11
+ const props = withDefaults(defineProps<Props>(), {
12
+ tld: "com",
13
+ placeholder: "[email hidden]",
14
+ });
15
+
16
+ const email = ref("");
17
+ const href = ref("");
18
+ const visible = ref(false);
19
+
20
+ onMounted(() => {
21
+ const el = document.querySelector(
22
+ `[data-email-user="${props.user}"][data-email-domain="${props.domain}"]`,
23
+ ) as HTMLElement | null;
24
+ if (!el) return;
25
+
26
+ const user = el.dataset.emailUser ?? "";
27
+ const domain = el.dataset.emailDomain ?? "";
28
+ const tld = el.dataset.emailTld ?? "";
29
+ const full = `${user}@${domain}.${tld}`;
30
+
31
+ email.value = full;
32
+ href.value = `mailto:${full}${props.subject ? `?subject=${encodeURIComponent(props.subject)}` : ""}`;
33
+ visible.value = true;
34
+ });
35
+ </script>
36
+
37
+ <template>
38
+ <span
39
+ v-if="!visible"
40
+ :class="props.class"
41
+ :data-email-user="props.user"
42
+ :data-email-domain="props.domain"
43
+ :data-email-tld="props.tld"
44
+ >
45
+ <noscript>[email&nbsp;protected]</noscript>
46
+ {{ props.placeholder }}
47
+ </span>
48
+
49
+ <a v-else :href="href" :class="props.class" @click.stop>
50
+ <slot>{{ email }}</slot>
51
+ </a>
52
+ </template>
@@ -0,0 +1,180 @@
1
+ <template>
2
+ <div
3
+ ref="wrapper"
4
+ class="lazy-image-wrapper"
5
+ :style="{
6
+ maxWidth: width ? width : undefined,
7
+ height: height ? height : undefined,
8
+ }"
9
+ >
10
+ <ClientOnly>
11
+ <template v-if="isStatic">
12
+ <img
13
+ v-if="isVisible"
14
+ :src="currentSrc"
15
+ :alt="alt"
16
+ :width="width"
17
+ :height="height"
18
+ :sizes="sizes"
19
+ :class="[
20
+ 'lazy-image',
21
+ imgClass,
22
+ { 'image-visible': isLoaded },
23
+ ]"
24
+ @load="onLoad"
25
+ @error="onError"
26
+ loading="lazy"
27
+ fit="cover"
28
+ />
29
+ </template>
30
+
31
+ <template v-else>
32
+ <NuxtImg
33
+ v-if="isVisible"
34
+ :src="currentSrc"
35
+ :alt="alt"
36
+ :width="width"
37
+ :height="height"
38
+ :sizes="sizes"
39
+ :class="[
40
+ 'lazy-image',
41
+ imgClass,
42
+ { 'image-visible': isLoaded },
43
+ ]"
44
+ @load="onLoad"
45
+ @error="onError"
46
+ loading="lazy"
47
+ fit="cover"
48
+ format="avif,webp"
49
+ />
50
+ </template>
51
+ </ClientOnly>
52
+
53
+ <div v-if="!isLoaded" class="loaderparent imgpreloader">
54
+ <svg
55
+ width="26"
56
+ height="26"
57
+ viewBox="0 0 32 32"
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ class="loadersvg"
60
+ >
61
+ <g fill="none" fill-rule="evenodd">
62
+ <g fill="var(--loader-fill-color, #333)">
63
+ <path
64
+ d="M16 32a16 16 0 110-32 16 16 0 010 32zm0-2a14 14 0 100-28 14 14 0 000 28z"
65
+ fill-rule="nonzero"
66
+ opacity=".1"
67
+ />
68
+ <path d="M16 2V0A16 16 0 000 16h2A14 14 0 0116 2z" />
69
+ </g>
70
+ </g>
71
+ </svg>
72
+ </div>
73
+ </div>
74
+ </template>
75
+
76
+ <script setup lang="ts">
77
+ const isStatic = useStatic();
78
+
79
+ const props = defineProps<{
80
+ src: string;
81
+ alt?: string;
82
+ width?: number | string;
83
+ height?: number | string;
84
+ sizes?: string;
85
+ imgClass?: string;
86
+ }>();
87
+
88
+ const isLoaded = ref(false);
89
+ const isVisible = ref(false);
90
+ const currentSrc = ref("");
91
+ const wrapper = ref<HTMLElement | null>(null);
92
+
93
+ let observer: IntersectionObserver | null = null;
94
+
95
+ function onLoad() {
96
+ isLoaded.value = true;
97
+ }
98
+
99
+ const img404 = "/img/image404.png";
100
+
101
+ function onError() {
102
+ currentSrc.value = img404;
103
+ }
104
+
105
+ onMounted(() => {
106
+ if (!wrapper.value) return;
107
+
108
+ observer = new IntersectionObserver(
109
+ ([entry]) => {
110
+ if (entry?.isIntersecting) {
111
+ isVisible.value = true;
112
+ currentSrc.value = props.src;
113
+ observer?.disconnect();
114
+ }
115
+ },
116
+ {
117
+ rootMargin: "100px",
118
+ threshold: 0.1,
119
+ },
120
+ );
121
+
122
+ observer.observe(wrapper.value);
123
+ });
124
+
125
+ onBeforeUnmount(() => {
126
+ observer?.disconnect();
127
+ });
128
+ </script>
129
+
130
+ <style scoped>
131
+ .lazy-image-wrapper {
132
+ position: relative;
133
+ width: 100%;
134
+ height: 100%;
135
+ display: block;
136
+ overflow: hidden;
137
+ }
138
+
139
+ .lazy-image {
140
+ position: absolute;
141
+ top: 0;
142
+ left: 0;
143
+ width: 100%;
144
+ height: 100%;
145
+ object-fit: cover;
146
+ opacity: 0;
147
+ transition: opacity 0.4s ease-in-out;
148
+ z-index: 2;
149
+ }
150
+
151
+ .image-visible {
152
+ opacity: 1;
153
+ }
154
+
155
+ .loaderparent {
156
+ position: absolute;
157
+ top: 0;
158
+ left: 0;
159
+ width: 100%;
160
+ height: 100%;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ z-index: 1;
165
+ background: transparent;
166
+ }
167
+
168
+ .loadersvg {
169
+ animation: rotate 0.8s linear infinite;
170
+ }
171
+
172
+ @keyframes rotate {
173
+ from {
174
+ transform: rotate(0deg);
175
+ }
176
+ to {
177
+ transform: rotate(360deg);
178
+ }
179
+ }
180
+ </style>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ [key: string]: any;
4
+ }>();
5
+
6
+ const attrs = useAttrs();
7
+ const hasOnError = computed(
8
+ () => "onerror" in attrs || "onError" in attrs || "@error" in attrs,
9
+ );
10
+ const finalOnError = computed(() => {
11
+ return hasOnError.value ? undefined : null;
12
+ });
13
+
14
+ const isStatic = useStatic();
15
+ </script>
16
+
17
+ <template>
18
+ <template v-if="isStatic">
19
+ <img v-bind="{ ...props, ...attrs }" :onerror="finalOnError" />
20
+ </template>
21
+
22
+ <template v-else>
23
+ <NuxtImg v-bind="{ ...props, ...attrs }" :onerror="finalOnError" />
24
+ </template>
25
+ </template>
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ [key: string]: any;
4
+ }>();
5
+
6
+ const attrs = useAttrs();
7
+
8
+ const hasOnError = computed(() => {
9
+ return (
10
+ "onerror" in attrs ||
11
+ "onError" in attrs ||
12
+ "@error" in attrs ||
13
+ Object.keys(attrs).some(
14
+ (key) =>
15
+ key.toLowerCase().includes("error") && key.startsWith("on"),
16
+ )
17
+ );
18
+ });
19
+
20
+ const finalOnError = computed(() => {
21
+ return hasOnError.value ? undefined : null;
22
+ });
23
+ </script>
24
+
25
+ <template>
26
+ <NuxtPicture v-bind="{ ...props, ...attrs }" :onerror="finalOnError">
27
+ <slot />
28
+ </NuxtPicture>
29
+ </template>
File without changes
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ enabled?: boolean;
4
+ context?: string;
5
+ type?: string | string[];
6
+ id?: string;
7
+ name?: string;
8
+ alternateName?: string | string[];
9
+ additionalType?: string | string[];
10
+ description?: string;
11
+ disambiguatingDescription?: string;
12
+ url?: string;
13
+ logo?: string | JsonLdNode;
14
+ image?: string | JsonLdNode | JsonLdValue[];
15
+ sameAs?: string[];
16
+ identifier?: string | JsonLdNode | JsonLdValue[];
17
+ slogan?: string;
18
+ owner?: JsonLdNode | JsonLdValue[];
19
+ aggregateRating?: JsonLdNode;
20
+ review?: JsonLdNode | JsonLdValue[];
21
+ mainEntityOfPage?: string | JsonLdNode;
22
+ potentialAction?: JsonLdNode | JsonLdValue[];
23
+ subjectOf?: JsonLdNode | JsonLdValue[];
24
+ graph?: JsonLdNode[];
25
+ data?: JsonLdNode;
26
+ scriptKey?: string;
27
+ scriptId?: string;
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ enabled: true,
32
+ context: "https://schema.org",
33
+ type: "Brand",
34
+ data: () => ({}),
35
+ });
36
+
37
+ const instanceId = useId();
38
+ const schema = computed<JsonLdNode | null>(() => {
39
+ if (!props.enabled) return null;
40
+
41
+ const record: JsonLdNode = {
42
+ ...props.data,
43
+ "@context": props.context,
44
+ "@type": props.type,
45
+ ...compactJsonLd({
46
+ "@id": props.id,
47
+ name: props.name,
48
+ alternateName: props.alternateName,
49
+ additionalType: props.additionalType,
50
+ description: props.description,
51
+ disambiguatingDescription: props.disambiguatingDescription,
52
+ url: props.url,
53
+ logo: props.logo,
54
+ image: props.image,
55
+ sameAs: props.sameAs,
56
+ identifier: props.identifier,
57
+ slogan: props.slogan,
58
+ owner: props.owner,
59
+ aggregateRating: props.aggregateRating,
60
+ review: props.review,
61
+ mainEntityOfPage: props.mainEntityOfPage,
62
+ potentialAction: props.potentialAction,
63
+ subjectOf: props.subjectOf,
64
+ }),
65
+ };
66
+
67
+ return wrapJsonLdGraph(record, props.graph);
68
+ });
69
+
70
+ useJsonLd(schema, {
71
+ key: computed(() => props.scriptKey ?? `seo-brand-info-${instanceId}`),
72
+ id: computed(() => props.scriptId),
73
+ });
74
+ </script>
75
+
76
+ <template />
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <div data-nosnippet>
3
+ <ClientOnly>
4
+ <slot />
5
+ <template #placeholder>
6
+ <div
7
+ :style="{ height: fallbackHeight }"
8
+ :class="['no-index-placeholder', placeholderClass]"
9
+ >
10
+ <slot name="placeholder" />
11
+ </div>
12
+ </template>
13
+ </ClientOnly>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ interface Props {
19
+ fallbackHeight?: string;
20
+ placeholderClass?: string;
21
+ }
22
+
23
+ withDefaults(defineProps<Props>(), {
24
+ fallbackHeight: "auto",
25
+ placeholderClass: "",
26
+ });
27
+ </script>
28
+
29
+ <style scoped>
30
+ .no-index-placeholder {
31
+ opacity: 0;
32
+ pointer-events: none;
33
+ user-select: none;
34
+ }
35
+ </style>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <span v-if="false" aria-hidden="true" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ const props = defineProps<{
7
+ statusCode?: number;
8
+ }>();
9
+
10
+ const is4xx = computed(
11
+ () =>
12
+ props.statusCode !== undefined &&
13
+ props.statusCode >= 400 &&
14
+ props.statusCode < 500,
15
+ );
16
+
17
+ if (is4xx.value) {
18
+ useSeoMeta({
19
+ robots: "noindex, nofollow",
20
+ });
21
+ }
22
+ </script>