@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,158 @@
1
+ <script setup lang="ts">
2
+ import type { COL_SPAN } from "~/constants/layout";
3
+ import type { LayoutSize } from "~/constants/layout";
4
+
5
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
6
+ type HeadingSize = "xs" | "sm" | "md" | "lg" | "xl" | "display";
7
+ type StickyBreakpoint = "always" | "sm" | "md" | "lg";
8
+
9
+ interface Props {
10
+ as?: string;
11
+ title?: string;
12
+ headingSide?: "left" | "right";
13
+ headingLevel?: HeadingLevel;
14
+ headingSize?: HeadingSize;
15
+ headingSpan?: keyof typeof COL_SPAN;
16
+ contentSpan?: keyof typeof COL_SPAN;
17
+ containerSize?: LayoutSize;
18
+ sticky?: boolean;
19
+ stickyFrom?: StickyBreakpoint;
20
+ stickyOffset?: string;
21
+ class?: string;
22
+ containerClass?: string;
23
+ rowClass?: string;
24
+ headingColumnClass?: string;
25
+ headingClass?: string;
26
+ contentColumnClass?: string;
27
+ contentClass?: string;
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ as: "section",
32
+ title: "",
33
+ headingSide: "left",
34
+ headingLevel: 2,
35
+ headingSize: "lg",
36
+ headingSpan: 5,
37
+ contentSpan: 7,
38
+ containerSize: "content",
39
+ sticky: true,
40
+ stickyFrom: "lg",
41
+ stickyOffset: "6rem",
42
+ class: "",
43
+ containerClass: "",
44
+ rowClass: "",
45
+ headingColumnClass: "",
46
+ headingClass: "",
47
+ contentColumnClass: "",
48
+ contentClass: "",
49
+ });
50
+
51
+ const stickyClasses: Record<StickyBreakpoint, string> = {
52
+ always: "sticky",
53
+ sm: "sm:sticky",
54
+ md: "md:sticky",
55
+ lg: "lg:sticky",
56
+ };
57
+
58
+ const rootClasses = computed(() =>
59
+ ["w-full", props.class].filter(Boolean).join(" "),
60
+ );
61
+
62
+ const containerClasses = computed(() => props.containerClass);
63
+
64
+ const rowClasses = computed(() =>
65
+ ["gap-y-8 md:-mx-6", props.rowClass].filter(Boolean).join(" "),
66
+ );
67
+
68
+ const headingColumnClasses = computed(() =>
69
+ [
70
+ "view-sticky-heading-content__heading-column md:px-6",
71
+ props.headingSide === "right" ? "md:order-2" : "md:order-1",
72
+ props.headingColumnClass,
73
+ ]
74
+ .filter(Boolean)
75
+ .join(" "),
76
+ );
77
+
78
+ const contentColumnClasses = computed(() =>
79
+ [
80
+ "view-sticky-heading-content__content-column md:px-6",
81
+ props.headingSide === "right" ? "md:order-1" : "md:order-2",
82
+ props.contentColumnClass,
83
+ ]
84
+ .filter(Boolean)
85
+ .join(" "),
86
+ );
87
+
88
+ const headingColumnStyles = computed(() => ({
89
+ "--view-sticky-heading-column-width": `${(Number(props.headingSpan) / 12) * 100}%`,
90
+ }));
91
+
92
+ const contentColumnStyles = computed(() => ({
93
+ "--view-sticky-content-column-width": `${(Number(props.contentSpan) / 12) * 100}%`,
94
+ }));
95
+
96
+ const stickyHeadingClasses = computed(() =>
97
+ ["w-full self-start", props.sticky && stickyClasses[props.stickyFrom]]
98
+ .filter(Boolean)
99
+ .join(" "),
100
+ );
101
+
102
+ const stickyHeadingStyles = computed(() =>
103
+ props.sticky ? { top: props.stickyOffset } : undefined,
104
+ );
105
+ </script>
106
+
107
+ <template>
108
+ <component :is="as" :class="rootClasses">
109
+ <GridContainer :size="containerSize" :class="containerClasses">
110
+ <GridRow align="stretch" :class="rowClasses">
111
+ <GridCol
112
+ :span="headingSpan"
113
+ :class="headingColumnClasses"
114
+ :style="headingColumnStyles"
115
+ >
116
+ <div
117
+ :class="stickyHeadingClasses"
118
+ :style="stickyHeadingStyles"
119
+ >
120
+ <UiHeading
121
+ :level="headingLevel"
122
+ :size="headingSize"
123
+ :class="headingClass"
124
+ >
125
+ <slot name="heading">{{ title }}</slot>
126
+ </UiHeading>
127
+
128
+ <slot name="heading-after" />
129
+ </div>
130
+ </GridCol>
131
+
132
+ <GridCol
133
+ :span="contentSpan"
134
+ :class="contentColumnClasses"
135
+ :style="contentColumnStyles"
136
+ >
137
+ <div :class="contentClass">
138
+ <slot />
139
+ </div>
140
+ </GridCol>
141
+ </GridRow>
142
+ </GridContainer>
143
+ </component>
144
+ </template>
145
+
146
+ <style>
147
+ @media (min-width: 768px) {
148
+ .view-sticky-heading-content__heading-column {
149
+ flex-basis: var(--view-sticky-heading-column-width);
150
+ max-width: var(--view-sticky-heading-column-width);
151
+ }
152
+
153
+ .view-sticky-heading-content__content-column {
154
+ flex-basis: var(--view-sticky-content-column-width);
155
+ max-width: var(--view-sticky-content-column-width);
156
+ }
157
+ }
158
+ </style>
@@ -0,0 +1,295 @@
1
+ <script setup lang="ts">
2
+ export interface TallyOptions {
3
+ hideTitle?: boolean;
4
+ transparentBackground?: boolean;
5
+ dynamicHeight?: boolean;
6
+ alignLeft?: boolean;
7
+ overlay?: boolean;
8
+ emoji?: { text: string; animation: string };
9
+ autoClose?: number;
10
+ doNotShowAfterSubmit?: boolean;
11
+ [key: string]: any;
12
+ }
13
+
14
+ export interface TallyColors {
15
+ light?: { bg?: string; text?: string };
16
+ dark?: { bg?: string; text?: string };
17
+ }
18
+
19
+ interface Props {
20
+ formId: string;
21
+ mode?: "standard" | "popup" | "fullscreen";
22
+ domain?: string;
23
+ initialHeight?: number | string;
24
+ hiddenFields?: Record<string, any>;
25
+ options?: TallyOptions;
26
+ colors?: TallyColors;
27
+ popupWidth?: number;
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ mode: "standard",
32
+ initialHeight: 400,
33
+ hiddenFields: () => ({}),
34
+ options: () => ({}),
35
+ colors: () => ({}),
36
+ popupWidth: 900,
37
+ });
38
+
39
+ const iframeId = `tally-${Math.random().toString(36).slice(2, 11)}`;
40
+
41
+ const config = useMyelophoneConfig();
42
+ const rawDomain = props.domain || config.tally?.domain || "tally.so";
43
+ const baseUrl = `https://${rawDomain}`;
44
+ const scriptSrc = `${baseUrl}/widgets/embed.js`;
45
+
46
+ const settingsStore = useSettingsStore();
47
+ const currentTheme = computed(() => settingsStore.theme || "light");
48
+
49
+ const getPayload = (isFullscreenOverride = false) => {
50
+ const payload: Record<string, any> = {
51
+ transparentBackground: 1,
52
+ dynamicHeight: isFullscreenOverride ? 0 : 1,
53
+ ...props.options,
54
+ };
55
+
56
+ if (props.colors.light?.bg) payload.lightbg = props.colors.light.bg;
57
+ if (props.colors.light?.text) payload.lighttext = props.colors.light.text;
58
+ if (props.colors.dark?.bg) payload.darkbg = props.colors.dark.bg;
59
+ if (props.colors.dark?.text) payload.darktext = props.colors.dark.text;
60
+
61
+ payload.theme = currentTheme.value;
62
+ if (props.hiddenFields) Object.assign(payload, props.hiddenFields);
63
+
64
+ return payload;
65
+ };
66
+
67
+ const generateUrl = (includeTheme: boolean) => {
68
+ const urlParams = new URLSearchParams();
69
+ const payload = getPayload(false);
70
+ if (!includeTheme) delete payload.theme;
71
+
72
+ Object.entries(payload).forEach(([key, value]) => {
73
+ if (
74
+ typeof value === "boolean" &&
75
+ props.options &&
76
+ key in props.options
77
+ ) {
78
+ urlParams.set(key, value ? "1" : "0");
79
+ } else if (value !== undefined && value !== null) {
80
+ urlParams.set(key, String(value));
81
+ }
82
+ });
83
+ return `${baseUrl}/embed/${props.formId}?${urlParams.toString()}`;
84
+ };
85
+
86
+ const staticUrl = computed(() => generateUrl(false));
87
+ const fullUrl = computed(() => generateUrl(true));
88
+
89
+ const { load, status } = useScript(scriptSrc, {
90
+ trigger: "manual",
91
+ use() {
92
+ return { Tally: (window as any).Tally };
93
+ },
94
+ });
95
+
96
+ const getTallyApi = async () => {
97
+ if ((window as any).Tally) return (window as any).Tally;
98
+ if (status.value !== "loading" && status.value !== "loaded") load();
99
+
100
+ return new Promise((resolve) => {
101
+ let attempts = 0;
102
+ const check = setInterval(() => {
103
+ attempts++;
104
+ if ((window as any).Tally) {
105
+ clearInterval(check);
106
+ resolve((window as any).Tally);
107
+ }
108
+ if (attempts > 50) {
109
+ clearInterval(check);
110
+ resolve(null);
111
+ }
112
+ }, 100);
113
+ });
114
+ };
115
+
116
+ const iframeRef = ref<HTMLIFrameElement | null>(null);
117
+ const iframeLoaded = ref(false);
118
+
119
+ onMounted(() => {
120
+ if (props.mode === "standard") getTallyApi();
121
+ });
122
+
123
+ const onIframeLoad = async () => {
124
+ iframeLoaded.value = true;
125
+ updateThemeInIframe(iframeRef.value?.contentWindow);
126
+
127
+ const Tally = await getTallyApi();
128
+ if (Tally?.loadEmbeds) {
129
+ setTimeout(() => {
130
+ Tally.loadEmbeds();
131
+ }, 200);
132
+ }
133
+ };
134
+
135
+ const openPopup = async () => {
136
+ const Tally = await getTallyApi();
137
+ if (!Tally) return;
138
+
139
+ const isFullscreen = props.mode === "fullscreen";
140
+ const payload = getPayload(isFullscreen);
141
+
142
+ const popupOptions = {
143
+ layout: isFullscreen ? "modal" : "default",
144
+ width: isFullscreen ? 10000 : undefined,
145
+ ...payload,
146
+ };
147
+
148
+ if (isFullscreen) {
149
+ document.body.classList.add("tally-fullscreen-mode");
150
+ } else {
151
+ document.body.classList.remove("tally-fullscreen-mode");
152
+ }
153
+
154
+ setTimeout(() => {
155
+ Tally.openPopup(props.formId, popupOptions);
156
+ setTimeout(syncPopupTheme, 300);
157
+ }, 50);
158
+ };
159
+
160
+ const updateThemeInIframe = (w: Window | null | undefined) => {
161
+ w?.postMessage({ type: "setTheme", theme: currentTheme.value }, "*");
162
+ };
163
+ const syncPopupTheme = () => {
164
+ const popupFrame = document.querySelector(
165
+ ".tally-popup iframe",
166
+ ) as HTMLIFrameElement;
167
+ updateThemeInIframe(popupFrame?.contentWindow);
168
+ };
169
+ watch(currentTheme, () => {
170
+ props.mode === "standard"
171
+ ? updateThemeInIframe(iframeRef.value?.contentWindow)
172
+ : syncPopupTheme();
173
+ });
174
+
175
+ defineExpose({ open: openPopup });
176
+ </script>
177
+
178
+ <template>
179
+ <component
180
+ :is="mode === 'standard' ? 'div' : 'span'"
181
+ class="tally-component-wrapper"
182
+ :class="{ 'cursor-pointer': mode !== 'standard' }"
183
+ @click="mode !== 'standard' && openPopup()"
184
+ >
185
+ <ClientOnly v-if="mode === 'standard'">
186
+ <div
187
+ class="relative w-full overflow-hidden"
188
+ :style="{
189
+ minHeight: iframeLoaded ? undefined : `${initialHeight}px`,
190
+ }"
191
+ >
192
+ <div
193
+ v-if="!iframeLoaded"
194
+ class="absolute inset-0 z-0 flex items-center justify-center bg-gray-50 dark:bg-gray-900"
195
+ style="background-color: var(--ui-bg)"
196
+ >
197
+ <a
198
+ :href="staticUrl"
199
+ target="_blank"
200
+ class="text-sm underline opacity-60"
201
+ >Загрузка...</a
202
+ >
203
+ </div>
204
+
205
+ <iframe
206
+ ref="iframeRef"
207
+ :id="iframeId"
208
+ :src="fullUrl"
209
+ class="relative z-10 w-full border-0 transition-opacity duration-500"
210
+ :class="iframeLoaded ? 'opacity-100' : 'opacity-0'"
211
+ loading="lazy"
212
+ width="100%"
213
+ height="100%"
214
+ title="Form"
215
+ @load="onIframeLoad"
216
+ />
217
+ </div>
218
+ <template #fallback>
219
+ <div
220
+ class="relative w-full flex items-center justify-center"
221
+ :style="{
222
+ height: `${initialHeight}px`,
223
+ backgroundColor: 'var(--ui-bg)',
224
+ }"
225
+ >
226
+ <a
227
+ :href="staticUrl"
228
+ target="_blank"
229
+ class="text-sm underline opacity-60"
230
+ >Открыть форму</a
231
+ >
232
+ </div>
233
+ </template>
234
+ </ClientOnly>
235
+ <slot v-else />
236
+ </component>
237
+ </template>
238
+
239
+ <style scoped>
240
+ .tally-component-wrapper {
241
+ display: inherit;
242
+ cursor: auto !important;
243
+ }
244
+ :deep(iframe) {
245
+ cursor: auto !important;
246
+ }
247
+ </style>
248
+
249
+ <style>
250
+ body.tally-fullscreen-mode .tally-overlay {
251
+ z-index: 2147483647 !important;
252
+ padding: 0 !important;
253
+ display: flex !important;
254
+ align-items: center !important;
255
+ justify-content: center !important;
256
+ background-color: transparent !important;
257
+ }
258
+
259
+ body.tally-fullscreen-mode .tally-overlay .tally-popup {
260
+ width: 100vw !important;
261
+ height: 100vh !important;
262
+ max-width: none !important;
263
+ max-height: none !important;
264
+ border-radius: 0 !important;
265
+ margin: 0 !important;
266
+ top: 0 !important;
267
+ left: 0 !important;
268
+ background-color: var(--ui-bg, #fff) !important;
269
+ display: flex !important;
270
+ flex-direction: column !important;
271
+ }
272
+
273
+ body.tally-fullscreen-mode .tally-overlay .tally-popup iframe {
274
+ width: 100% !important;
275
+ height: 100% !important;
276
+ flex: 1 !important;
277
+ min-height: 100vh !important;
278
+ border-radius: 0 !important;
279
+ }
280
+
281
+ body.tally-fullscreen-mode .tally-overlay .tally-close {
282
+ position: fixed !important;
283
+ top: 24px !important;
284
+ right: 24px !important;
285
+ color: var(--ui-text, #000) !important;
286
+ background: rgba(127, 127, 127, 0.2) !important;
287
+ border-radius: 50% !important;
288
+ z-index: 2147483648 !important;
289
+ width: 40px !important;
290
+ height: 40px !important;
291
+ display: flex !important;
292
+ align-items: center !important;
293
+ justify-content: center !important;
294
+ }
295
+ </style>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div
3
+ v-if="showWarning"
4
+ class="fixed inset-0 bg-gray-50 flex flex-col items-center justify-center z-50 p-4"
5
+ >
6
+ <div
7
+ class="bg-white shadow-lg rounded-xl p-6 max-w-md w-full text-center flex flex-col items-center"
8
+ >
9
+ <svg
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ fill="none"
12
+ aria-label="Nuxt"
13
+ class="h-8 sm:h-12"
14
+ viewBox="0 0 800 200"
15
+ >
16
+ <path
17
+ fill="#00dc82"
18
+ d="M168.303 200h111.522c3.543 0 7.022-.924 10.09-2.679A20.1 20.1 0 0 0 297.3 190a19.86 19.86 0 0 0 2.7-10.001 19.86 19.86 0 0 0-2.709-9.998L222.396 41.429a20.1 20.1 0 0 0-7.384-7.32 20.3 20.3 0 0 0-10.088-2.679c-3.541 0-7.02.925-10.087 2.68a20.1 20.1 0 0 0-7.384 7.32l-19.15 32.896L130.86 9.998a20.1 20.1 0 0 0-7.387-7.32A20.3 20.3 0 0 0 113.384 0c-3.542 0-7.022.924-10.09 2.679a20.1 20.1 0 0 0-7.387 7.319L2.709 170A19.85 19.85 0 0 0 0 179.999c-.002 3.511.93 6.96 2.7 10.001a20.1 20.1 0 0 0 7.385 7.321A20.3 20.3 0 0 0 20.175 200h70.004c27.737 0 48.192-12.075 62.266-35.633l34.171-58.652 18.303-31.389 54.93 94.285h-73.233zm-79.265-31.421-48.854-.011 73.232-125.706 36.541 62.853-24.466 42.01c-9.347 15.285-19.965 20.854-36.453 20.854"
19
+ ></path>
20
+ <path
21
+ fill="currentColor"
22
+ d="M377 200a4 4 0 0 0 4-4v-93s5.244 8.286 15 25l38.707 66.961c1.789 3.119 5.084 5.039 8.649 5.039H470V50h-27a4 4 0 0 0-4 4v94l-17-30-36.588-62.98c-1.792-3.108-5.081-5.02-8.639-5.02H350v150zm299.203-56.143L710.551 92h-25.73a9.97 9.97 0 0 0-8.333 4.522L660.757 120.5l-15.731-23.978A9.97 9.97 0 0 0 636.693 92h-25.527l34.348 51.643L608.524 200h24.966a9.97 9.97 0 0 0 8.29-4.458l19.18-28.756 18.981 28.72a9.97 9.97 0 0 0 8.313 4.494h24.736zM724.598 92h19.714V60.071h28.251V92H800v24.857h-27.437V159.5c0 10.5 5.284 15.429 14.43 15.429H800V200h-16.869c-23.576 0-38.819-14.143-38.819-39.214v-43.929h-19.714zM590 92h-15c-3.489 0-6.218.145-8.5 2.523-2.282 2.246-2.5 3.63-2.5 7.066v52.486c0 8.058-.376 12.962-4 16.925-3.624 3.831-8.619 5-16 5-7.247 0-12.376-1.169-16-5-3.624-3.963-4-8.867-4-16.925v-52.486c0-3.435-.218-4.82-2.5-7.066C519.218 92.145 516.489 92 513 92h-15v62.422q0 21.006 11.676 33.292C517.594 195.905 529.103 200 544 200s26.204-4.095 34.123-12.286Q590 175.428 590 154.422z"
23
+ ></path>
24
+ </svg>
25
+ <h2 class="text-2xl font-semibold mb-2 text-gray-800">
26
+ {{ t("outdated.browserNotSupported") }}
27
+ </h2>
28
+ <p class="text-gray-600 mb-6">
29
+ {{ t("outdated.useModernBrowser") }}:
30
+ </p>
31
+ <div class="flex flex-wrap justify-center gap-4 mb-4">
32
+ <a
33
+ href="https://www.google.com/chrome/"
34
+ target="_blank"
35
+ class="flex flex-col items-center gap-1 hover:scale-105 transition-transform"
36
+ >
37
+ <img
38
+ src="/browsers/chrome.svg"
39
+ alt="Chrome"
40
+ class="w-12 h-12"
41
+ />
42
+ <span class="text-gray-700 text-sm">Chrome</span>
43
+ </a>
44
+ <a
45
+ href="https://www.microsoft.com/edge"
46
+ target="_blank"
47
+ class="flex flex-col items-center gap-1 hover:scale-105 transition-transform"
48
+ >
49
+ <img
50
+ src="/browsers/edge.svg"
51
+ alt="Edge"
52
+ class="w-12 h-12"
53
+ />
54
+ <span class="text-gray-700 text-sm">Edge</span>
55
+ </a>
56
+ <a
57
+ href="https://www.mozilla.org/firefox/new/"
58
+ target="_blank"
59
+ class="flex flex-col items-center gap-1 hover:scale-105 transition-transform"
60
+ >
61
+ <img
62
+ src="/browsers/firefox.svg"
63
+ alt="Firefox"
64
+ class="w-12 h-12"
65
+ />
66
+ <span class="text-gray-700 text-sm">Firefox</span>
67
+ </a>
68
+ </div>
69
+ <button
70
+ @click="dismiss"
71
+ class="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition"
72
+ >
73
+ {{ t("outdated.close") }}
74
+ </button>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <script setup lang="ts">
80
+ const showWarning = ref(false);
81
+ const dismissedKey = "browser-warning-dismissed";
82
+
83
+ const isBrowserUnsupported = () => {
84
+ if (!import.meta.client) return false;
85
+
86
+ if (window.location.search.includes("testBrowserWarning")) return true;
87
+
88
+ const ua = navigator.userAgent;
89
+ const supported = /(Chrome|Edg|Firefox|Safari)/i;
90
+ return !supported.test(ua);
91
+ };
92
+
93
+ const dismiss = () => {
94
+ showWarning.value = false;
95
+
96
+ if (import.meta.client) {
97
+ try {
98
+ sessionStorage.setItem(dismissedKey, "1");
99
+ } catch (e) {
100
+ console.warn("sessionStorage is unavailable:", e);
101
+ }
102
+ }
103
+ };
104
+
105
+ onMounted(() => {
106
+ const dismissed = localStorage.getItem(dismissedKey);
107
+ if (isBrowserUnsupported() && !dismissed) {
108
+ showWarning.value = true;
109
+ }
110
+ });
111
+
112
+ const { t } = useMultiLang(["outdated"]);
113
+ </script>
@@ -0,0 +1,88 @@
1
+ export const useApi = () => {
2
+ const config = useRuntimeConfig();
3
+ const baseURL = import.meta.server
4
+ ? ((config.apiBaseServer as string | undefined) ??
5
+ (config.public.apiBase as string | undefined))
6
+ : 'http://localhost:3000';
7
+
8
+ const token = useCookie('auth_token');
9
+
10
+ const fetch = $fetch.create({
11
+ baseURL,
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ ...(token.value && { Authorization: `Bearer ${token.value}` }),
15
+ },
16
+ retry: 3,
17
+ retryDelay: 1000,
18
+ onRequest({ options }) {
19
+ const headers = new Headers(options.headers);
20
+
21
+ if (
22
+ typeof FormData !== 'undefined' &&
23
+ options.body instanceof FormData
24
+ ) {
25
+ headers.delete('Content-Type');
26
+ }
27
+
28
+ if (token.value) {
29
+ headers.set('Authorization', `Bearer ${token.value}`);
30
+ }
31
+
32
+ options.headers = headers;
33
+ },
34
+ onResponseError({ response }) {
35
+ const message = response.statusText || `Error: ${response.status}`;
36
+ throw createError({
37
+ statusCode: response.status,
38
+ message,
39
+ data: response._data,
40
+ });
41
+ },
42
+ });
43
+
44
+ return {
45
+ get: <
46
+ TResponse,
47
+ TQuery extends Record<string, unknown> = Record<string, never>,
48
+ >(
49
+ url: string,
50
+ query?: TQuery,
51
+ ) =>
52
+ fetch<TResponse>(url, {
53
+ method: 'GET',
54
+ query,
55
+ }),
56
+
57
+ post: <
58
+ TResponse,
59
+ TBody extends Record<string, unknown> | FormData | undefined =
60
+ undefined,
61
+ >(
62
+ url: string,
63
+ body?: TBody,
64
+ ) =>
65
+ fetch<TResponse>(url, {
66
+ method: 'POST',
67
+ body,
68
+ }),
69
+
70
+ put: <
71
+ TResponse,
72
+ TBody extends Record<string, unknown> | FormData | undefined =
73
+ undefined,
74
+ >(
75
+ url: string,
76
+ body?: TBody,
77
+ ) =>
78
+ fetch<TResponse>(url, {
79
+ method: 'PUT',
80
+ body,
81
+ }),
82
+
83
+ delete: <TResponse>(url: string) =>
84
+ fetch<TResponse>(url, {
85
+ method: 'DELETE',
86
+ }),
87
+ };
88
+ };