itube-specs 0.0.195

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 (169) hide show
  1. package/README.md +121 -0
  2. package/components/cards/f-video-mini-card.vue +49 -0
  3. package/components/grids/f-grid-categories.vue +20 -0
  4. package/components/grids/f-grid-channels.vue +23 -0
  5. package/components/grids/f-grid-models.vue +25 -0
  6. package/components/grids/f-grid-playlists.vue +21 -0
  7. package/components/grids/f-grid-videos.vue +33 -0
  8. package/components/page-components/f-breadcrumbs.vue +44 -0
  9. package/components/page-components/f-chips-panel.vue +101 -0
  10. package/components/page-components/f-pagination.vue +206 -0
  11. package/components/page-components/f-report.vue +221 -0
  12. package/components/page-components/f-share.vue +96 -0
  13. package/components/page-components/f-sort.vue +57 -0
  14. package/components/page-components/f-videos-title.vue +20 -0
  15. package/components/ui/f-button.vue +50 -0
  16. package/components/ui/f-checkbox.vue +55 -0
  17. package/components/ui/f-chips.vue +116 -0
  18. package/components/ui/f-count.vue +12 -0
  19. package/components/ui/f-country.vue +26 -0
  20. package/components/ui/f-dropdown.vue +122 -0
  21. package/components/ui/f-icon.vue +19 -0
  22. package/components/ui/f-img.vue +46 -0
  23. package/components/ui/f-input.vue +162 -0
  24. package/components/ui/f-label.vue +20 -0
  25. package/components/ui/f-link.vue +33 -0
  26. package/components/ui/f-model-tag.vue +28 -0
  27. package/components/ui/f-notification.vue +77 -0
  28. package/components/ui/f-popup.vue +136 -0
  29. package/components/ui/f-radio.vue +56 -0
  30. package/components/ui/f-select.vue +88 -0
  31. package/components/ui/f-slider.vue +55 -0
  32. package/components/ui/f-snackbar.vue +47 -0
  33. package/components/ui/f-timestamp.vue +51 -0
  34. package/components/ui/f-toggle.vue +29 -0
  35. package/composables/use-antiadblock-domains.ts +20 -0
  36. package/composables/use-auth-popup.ts +25 -0
  37. package/composables/use-convert-query-categories.ts +7 -0
  38. package/composables/use-generate-link.ts +30 -0
  39. package/composables/use-get-pure-route-name.ts +5 -0
  40. package/composables/use-get-videos-filter-request.ts +30 -0
  41. package/composables/use-meta.ts +42 -0
  42. package/composables/use-playlist-edit.ts +36 -0
  43. package/composables/use-report-popup.ts +21 -0
  44. package/composables/use-seo-links.ts +87 -0
  45. package/composables/use-share-popup.ts +23 -0
  46. package/composables/use-snackbar.ts +52 -0
  47. package/composables/use-test-composable.ts +3 -0
  48. package/lib/alphabet-items.ts +2 -0
  49. package/lib/contact-forms-scheme.ts +98 -0
  50. package/lib/contacts/report-issue-items.ts +5 -0
  51. package/lib/contacts/report-malware-items.ts +6 -0
  52. package/lib/contacts/report-reasons-items.ts +12 -0
  53. package/lib/contacts/report-wrong-items.ts +6 -0
  54. package/lib/index.ts +7 -0
  55. package/lib/report-forms-scheme.ts +205 -0
  56. package/nuxt.config.ts +20 -0
  57. package/package.json +53 -0
  58. package/runtime/enums/async-data.ts +48 -0
  59. package/runtime/enums/auth-step.ts +5 -0
  60. package/runtime/enums/contacts-subjects.ts +7 -0
  61. package/runtime/enums/languages.ts +9 -0
  62. package/runtime/enums/niche.ts +6 -0
  63. package/runtime/enums/playlist-step.ts +5 -0
  64. package/runtime/enums/playlist-type.ts +4 -0
  65. package/runtime/enums/report-forms-subjects.ts +7 -0
  66. package/runtime/index.ts +51 -0
  67. package/runtime/utils/cleaners/clean-category-card.ts +9 -0
  68. package/runtime/utils/cleaners/clean-category-info.ts +9 -0
  69. package/runtime/utils/cleaners/clean-channel-card.ts +12 -0
  70. package/runtime/utils/cleaners/clean-channel-info.ts +13 -0
  71. package/runtime/utils/cleaners/clean-model-card.ts +9 -0
  72. package/runtime/utils/cleaners/clean-model-info.ts +11 -0
  73. package/runtime/utils/cleaners/clean-playlist-card.ts +16 -0
  74. package/runtime/utils/cleaners/clean-playlist-data.ts +15 -0
  75. package/runtime/utils/cleaners/clean-playlist-video.ts +12 -0
  76. package/runtime/utils/cleaners/clean-profile-data.ts +11 -0
  77. package/runtime/utils/cleaners/clean-user-playlists-card.ts +11 -0
  78. package/runtime/utils/cleaners/clean-video-card.ts +19 -0
  79. package/runtime/utils/cleaners/clean-video-data.ts +27 -0
  80. package/runtime/utils/compress-image.ts +27 -0
  81. package/runtime/utils/converters/convert-categories-to-chips.ts +13 -0
  82. package/runtime/utils/converters/convert-categories-to-footer.ts +11 -0
  83. package/runtime/utils/converters/convert-date-to-timestamp.ts +37 -0
  84. package/runtime/utils/converters/convert-model-card-to-chips.ts +13 -0
  85. package/runtime/utils/converters/convert-string.ts +56 -0
  86. package/runtime/utils/converters/group-categories-by-first-letter.ts +24 -0
  87. package/runtime/utils/converters/group-objects-by-first-letter.ts +16 -0
  88. package/runtime/utils/format-date.ts +12 -0
  89. package/runtime/utils/format-number.ts +12 -0
  90. package/runtime/utils/format-time-ago.ts +21 -0
  91. package/runtime/utils/get-duration.ts +17 -0
  92. package/runtime/utils/get-month.ts +22 -0
  93. package/runtime/utils/get-multiple-query.ts +26 -0
  94. package/runtime/utils/get-selected-query.ts +6 -0
  95. package/runtime/utils/is-mobile.ts +15 -0
  96. package/runtime/utils/normalize-url.ts +43 -0
  97. package/runtime/utils/on-backdrop-click.ts +5 -0
  98. package/runtime/utils/scroll-lock.ts +28 -0
  99. package/runtime/utils/server/abort-controller.ts +14 -0
  100. package/runtime/utils/server/api-helper.ts +41 -0
  101. package/runtime/utils/server/get-url-with-proxied-params.ts +6 -0
  102. package/runtime/utils/server/parse-api-error.ts +14 -0
  103. package/runtime/utils/server/server-api-helper.ts +28 -0
  104. package/runtime/utils/validate-email.ts +4 -0
  105. package/runtime/utils/validate-password.ts +3 -0
  106. package/runtime/utils/validate-phone.ts +4 -0
  107. package/runtime/utils/validate-username.ts +4 -0
  108. package/runtime/utils/video-data-add-model-icon.ts +20 -0
  109. package/runtime/utils/vtt-helper.ts +86 -0
  110. package/types/authorization-forms.d.ts +16 -0
  111. package/types/breadcrumb-item.d.ts +4 -0
  112. package/types/button-sizes.d.ts +1 -0
  113. package/types/button-themes.d.ts +1 -0
  114. package/types/card-info.d.ts +22 -0
  115. package/types/category-card.d.ts +8 -0
  116. package/types/change-email-form.d.ts +3 -0
  117. package/types/change-password-form.d.ts +4 -0
  118. package/types/channel-card.d.ts +10 -0
  119. package/types/chips-item.d.ts +8 -0
  120. package/types/contacts-form.d.ts +10 -0
  121. package/types/contacts-scheme.d.ts +14 -0
  122. package/types/country.d.ts +5 -0
  123. package/types/css-breakpoints.d.ts +1 -0
  124. package/types/filter-scheme.d.ts +37 -0
  125. package/types/fluid-player.d.ts +226 -0
  126. package/types/gender.d.ts +5 -0
  127. package/types/group-categories.d.ts +15 -0
  128. package/types/index.d.ts +59 -0
  129. package/types/input-types.d.ts +1 -0
  130. package/types/link-item.d.ts +6 -0
  131. package/types/model-card.d.ts +7 -0
  132. package/types/model-filter-payload.d.ts +4 -0
  133. package/types/model-filter.d.ts +15 -0
  134. package/types/model-group.d.ts +5 -0
  135. package/types/model-tag.d.ts +5 -0
  136. package/types/multi-suggest.d.ts +105 -0
  137. package/types/navigation-items.d.ts +10 -0
  138. package/types/paginated-response.d.ts +8 -0
  139. package/types/parameter-model.d.ts +14 -0
  140. package/types/playlist-card.d.ts +16 -0
  141. package/types/playlist-data.d.ts +15 -0
  142. package/types/playlist-info-type.d.ts +28 -0
  143. package/types/playlist-video-form.d.ts +9 -0
  144. package/types/profile-data.d.ts +9 -0
  145. package/types/raw/raw-category-card.d.ts +23 -0
  146. package/types/raw/raw-category-info.d.ts +23 -0
  147. package/types/raw/raw-channel-card.d.ts +29 -0
  148. package/types/raw/raw-channel-info.d.ts +29 -0
  149. package/types/raw/raw-model-card.d.ts +53 -0
  150. package/types/raw/raw-model-info.d.ts +54 -0
  151. package/types/raw/raw-playlist-card.d.ts +27 -0
  152. package/types/raw/raw-playlist-data.d.ts +29 -0
  153. package/types/raw/raw-playlist-user.d.ts +24 -0
  154. package/types/raw/raw-playlist-video.d.ts +18 -0
  155. package/types/raw/raw-profile-data.d.ts +22 -0
  156. package/types/raw/raw-video-card.d.ts +78 -0
  157. package/types/raw/raw-video-data.d.ts +53 -0
  158. package/types/recovery-password-form.d.ts +4 -0
  159. package/types/related-phrases.d.ts +6 -0
  160. package/types/report-form.d.ts +9 -0
  161. package/types/report-scheme.d.ts +21 -0
  162. package/types/request-filters.d.ts +13 -0
  163. package/types/request-pagination.d.ts +5 -0
  164. package/types/search-top-models.d.ts +6 -0
  165. package/types/select-item.d.ts +10 -0
  166. package/types/tab-item.d.ts +6 -0
  167. package/types/thumbs-urls.d.ts +13 -0
  168. package/types/video-card.d.ts +18 -0
  169. package/types/video-data.d.ts +36 -0
@@ -0,0 +1,15 @@
1
+ import type { IRawPlaylistData, IPlaylistData } from '../../../types';
2
+
3
+ export const cleanPlaylistData = (card: IRawPlaylistData): IPlaylistData => ({
4
+ updated: card.updated,
5
+ id: card.id,
6
+ name: card.name,
7
+ username: card.username,
8
+ playlistType: card.playlist_type,
9
+ videosCount: card.videos_count,
10
+ firstVideoId: card.first_video_id,
11
+ searchTags: card.search_tags,
12
+ views: card.views,
13
+ likes: card.likes,
14
+ dislikes: card.dislikes,
15
+ })
@@ -0,0 +1,12 @@
1
+ import type { IRawPlaylistVideo, IVideoCard } from '../../../types';
2
+
3
+ export const cleanPlaylistVideo = (card: IRawPlaylistVideo): IVideoCard => ({
4
+ guid: card.guid,
5
+ duration: card.duration,
6
+ title: card.name,
7
+ id: card.videoId,
8
+ thumbUrl: card.thumb,
9
+ playlistId: card.playlistId,
10
+ md5: card.videoMd5,
11
+ thumbNum: card.thumbNum,
12
+ })
@@ -0,0 +1,11 @@
1
+ import type { IRawProfileData, IProfileData } from '../../../types';
2
+
3
+ export const cleanProfileData = (data: IRawProfileData): IProfileData => ({
4
+ username: data.username,
5
+ email: data.email,
6
+ avatar: data.avatar,
7
+ country: data.country,
8
+ city: data.city,
9
+ birthday: data.birthday,
10
+ gender: data.gender,
11
+ })
@@ -0,0 +1,11 @@
1
+ import type { IRawPlaylistsUser, IPlaylistCard } from '../../../types';
2
+
3
+ export const cleanUserPlaylistCard = (card: IRawPlaylistsUser): IPlaylistCard => ({
4
+ created: card.created,
5
+ id: card.id,
6
+ name: card.name,
7
+ username: card.username,
8
+ playlistType: card.playlistType,
9
+ thumbUrls: card.thumbs.slice(0, 4),
10
+ videosCount: card.videoCount,
11
+ })
@@ -0,0 +1,19 @@
1
+ import type { IRawVideoCard, IVideoCard } from '../../../types';
2
+
3
+ export const cleanVideoCard = (card: IRawVideoCard): IVideoCard => ({
4
+ guid: card.guid,
5
+ quality: card.quality,
6
+ duration: card.duration,
7
+ title: card.title,
8
+ views: card.views,
9
+ id: card.id,
10
+ tags: card.tags,
11
+ isNew: card.is_new,
12
+ channelAvatar: card.channel?.avatar_url,
13
+ channelName: card.channel?.name,
14
+ url: card.url,
15
+ previewUrl: card.preview_url,
16
+ thumbUrl: card.thumb_urls?.webp['480x270'],
17
+ md5: card.md5,
18
+ thumbNum: card.thumb_number,
19
+ })
@@ -0,0 +1,27 @@
1
+ import type { IRawVideoData, IVideoData } from '../../../types';
2
+
3
+ export const cleanVideoData = (data: IRawVideoData): IVideoData => ({
4
+ guid: data.guid || '',
5
+ md5: data.md5 || '',
6
+ quality: data.quality || '',
7
+ duration: data.duration || 0,
8
+ title: data.title || '',
9
+ views: data.views || 0,
10
+ likes: data.likes || 0,
11
+ id: data.id || '',
12
+ tags: data.tags || '',
13
+ models: data.models || '',
14
+ categories: data.categories || '',
15
+ actions: data.actions || null,
16
+ channelName: data.channel?.name || '',
17
+ channelAvatar: data.channel?.avatar || '',
18
+ url: data.url || '',
19
+ previewUrl: data.preview_url || '',
20
+ thumbUrl: data.thumb_urls?.webp['480x270'] || '',
21
+ vttUrl: data.vtt_url || '',
22
+ vttSpriteUrl: data.vtt_sprite_url || '',
23
+ created: data.created || 0,
24
+ videoUrl: data.video_url || '',
25
+ isDeleted: data.is_deleted,
26
+ thumbNum: data.thumb_number,
27
+ })
@@ -0,0 +1,27 @@
1
+ export const compressImage = (blob: Blob, maxSize = 512, quality = 0.8): Promise<Blob> => {
2
+ return new Promise((resolve, reject) => {
3
+ const img = new Image();
4
+ img.onload = () => {
5
+ const canvas = document.createElement('canvas');
6
+ const scale = maxSize / Math.max(img.width, img.height);
7
+ canvas.width = img.width * scale;
8
+ canvas.height = img.height * scale;
9
+
10
+ const ctx = canvas.getContext('2d');
11
+ if (!ctx) return reject(new Error('Canvas context not available'));
12
+
13
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
14
+ canvas.toBlob(
15
+ (compressedBlob) => {
16
+ if (compressedBlob) resolve(compressedBlob);
17
+ else reject(new Error('Compression failed'));
18
+ },
19
+ 'image/jpeg',
20
+ quality
21
+ );
22
+ };
23
+
24
+ img.onerror = reject;
25
+ img.src = URL.createObjectURL(blob);
26
+ });
27
+ };
@@ -0,0 +1,13 @@
1
+ import type { IRawCategoryCard, IChipsItem } from '../../../types';
2
+
3
+ /** Конвертирует обьекты категорий в чипсы */
4
+ export function convertCategoriesToChips(
5
+ items: Array<IRawCategoryCard>,
6
+ prefix: string
7
+ ): Array<IChipsItem> {
8
+ return items?.map(item => ({
9
+ title: item.title,
10
+ value: item.title,
11
+ prefix: prefix,
12
+ }));
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { IRawCategoryCard, ILinkItem } from '../../../types';
2
+
3
+ /** Конвертирует обьекты категорий в футер категории */
4
+ export function convertCategoriesToFooter(
5
+ items: Array<IRawCategoryCard>,
6
+ ): Array<ILinkItem> {
7
+ return items.map(item => ({
8
+ title: item.title,
9
+ value: item.title,
10
+ }));
11
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Преобразует строку вида "дд.мм.гггг" в Unix timestamp (в секундах)
3
+ */
4
+ export const convertDateToTimestamp = () => {
5
+ const toUnix = (dateStr: string): number | null => {
6
+ const [year, month, day] = dateStr.split('-').map(Number);
7
+ if (!day || !month || !year) {
8
+ return null;
9
+ }
10
+
11
+ const date = new Date(year, month - 1, day);
12
+ if (isNaN(date.getTime())) {
13
+ return null;
14
+ }
15
+
16
+ return Math.floor(date.getTime() / 1000);
17
+ };
18
+
19
+ /**
20
+ * Преобразует Unix timestamp в строку "yyyy-MM-dd"
21
+ * (то, что понимает input[type="date"])
22
+ */
23
+ const fromUnix = (timestamp: number): string => {
24
+ const date = new Date(timestamp * 1000);
25
+
26
+ const yyyy = date.getFullYear();
27
+ const mm = String(date.getMonth() + 1).padStart(2, '0'); // месяцы с 0
28
+ const dd = String(date.getDate()).padStart(2, '0');
29
+
30
+ return `${yyyy}-${mm}-${dd}`;
31
+ };
32
+
33
+ return {
34
+ toUnix,
35
+ fromUnix,
36
+ };
37
+ };
@@ -0,0 +1,13 @@
1
+ import type { IRawModelCard, IChipsItem } from '../../../types';
2
+
3
+ /** Конвертация карточки модели в chips */
4
+ export function convertModelCardToChips(
5
+ items: Array<IRawModelCard>,
6
+ ): Array<IChipsItem> {
7
+ return items.map(item => ({
8
+ title: item.title,
9
+ value: item.title,
10
+ icon: item.video_thumb_urls?.webp['320x180'],
11
+ prefix: 'models',
12
+ }));
13
+ }
@@ -0,0 +1,56 @@
1
+ import type { IChipsItem } from '../../../types';
2
+
3
+ export const convertString = () => {
4
+ const toSlug = (input: string): string => {
5
+ return input
6
+ .toLowerCase()
7
+ .replace(/-/g, '\u0000')
8
+ .replace(/\s+/g, '-')
9
+ .replace(/\u0000/g, '_');
10
+ };
11
+
12
+ const fromSlug = (slug: string): string => {
13
+ return slug
14
+ .replace(/_/g, '\u0000')
15
+ .replace(/-/g, ' ')
16
+ .replace(/\u0000/g, '-');
17
+ };
18
+
19
+ const toSnakeCase = (slug: string): string => {
20
+ return slug
21
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
22
+ .replace(/\s+/g, '_')
23
+ .replace(/-/g, '_')
24
+ .toLowerCase();
25
+ };
26
+
27
+ const toKebabCase = (str: string): string => {
28
+ return str
29
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
30
+ .replace(/\s+/g, '-')
31
+ .replace(/_/g, '-')
32
+ .replace(/-+/g, '-')
33
+ .toLowerCase();
34
+ };
35
+
36
+ const toChips = (string: string, prefix: string): IChipsItem => {
37
+ return {
38
+ title: string,
39
+ value: string,
40
+ prefix: prefix,
41
+ }
42
+ }
43
+
44
+ const toCapitalize = (string: string): string => {
45
+ return string.split(' ').map(i => i.charAt(0).toUpperCase() + i.slice(1)).join(' ');
46
+ }
47
+
48
+ return {
49
+ toChips,
50
+ toSlug,
51
+ toSnakeCase,
52
+ fromSlug,
53
+ toKebabCase,
54
+ toCapitalize,
55
+ };
56
+ };
@@ -0,0 +1,24 @@
1
+ import type { IRawCategoryCard, ICategoryCard } from '../../../types';
2
+
3
+ /** Метод для группировки мини категорий по первой букве свойства */
4
+ export function groupCategoriesByFirstLetter(
5
+ items: Array<IRawCategoryCard>,
6
+ prop: string,
7
+ nonLetterKey = 'POST',
8
+ ): Record<string, Array<ICategoryCard>> {
9
+ return items.reduce((acc, item) => {
10
+ const firstLetter = (item[prop as keyof IRawCategoryCard] as string).charAt(0).toUpperCase();
11
+ const key = /[A-Za-z]/.test(firstLetter) ? firstLetter : nonLetterKey;
12
+ if (!acc[key]) {
13
+ acc[key] = [];
14
+ }
15
+ acc[key].push({
16
+ isTop: item.is_top,
17
+ title: item.title,
18
+ name: item.name,
19
+ videosCount: item.videosCount,
20
+ guid: item.guid
21
+ });
22
+ return acc;
23
+ }, {} as Record<string, Array<ICategoryCard>>);
24
+ }
@@ -0,0 +1,16 @@
1
+ /** Метод для группировки категорий по первой букве свойства */
2
+ export function groupObjectsByFirstLetter<T>(
3
+ items: Array<T>,
4
+ prop: string,
5
+ nonLetterKey = 'POST',
6
+ ): Record<string, Array<T>> {
7
+ return items.reduce((acc, item) => {
8
+ const firstLetter = (item[prop as keyof T] as string).charAt(0).toUpperCase();
9
+ const key = /[A-Za-z]/.test(firstLetter) ? firstLetter : nonLetterKey;
10
+ if (!acc[key]) {
11
+ acc[key] = [];
12
+ }
13
+ acc[key].push(item);
14
+ return acc;
15
+ }, {} as Record<string, Array<T>>);
16
+ }
@@ -0,0 +1,12 @@
1
+ export function formatDate(seconds: number): string {
2
+ const date = new Date(1000 * seconds);
3
+
4
+ const day = date.getDate();
5
+ const month = date.getMonth() + 1;
6
+ const year = date.getFullYear();
7
+
8
+ const formattedDay = String(day).padStart(2, '0');
9
+ const formattedMonth = String(month).padStart(2, '0');
10
+
11
+ return `${formattedDay}.${formattedMonth}.${year}`;
12
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Форматирует число в короткий формат 10400 => 10.4K
3
+ * @param {number} number - число
4
+ */
5
+
6
+ export function formatNumber (number: number) {
7
+ return number.toLocaleString('en-US', {
8
+ maximumFractionDigits: 2,
9
+ notation: 'compact',
10
+ compactDisplay: 'short'
11
+ });
12
+ };
@@ -0,0 +1,21 @@
1
+ export function formatTimeAgo(date: number, t) {
2
+ const pastDate = new Date(date * 1000);
3
+ const now = new Date();
4
+
5
+ const diffInMs = Number(now) - Number(pastDate);
6
+ const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
7
+
8
+ if (diffInDays === 0) {
9
+ return t('today');
10
+ } else if (diffInDays === 1) {
11
+ return t('day_ago');
12
+ } else if (diffInDays < 30) {
13
+ return `${diffInDays} ${t('days_ago')}`;
14
+ } else if (diffInDays < 365) {
15
+ const diffInMonths = Math.floor(diffInDays / 30);
16
+ return `${diffInMonths} ${t('month')}${diffInMonths > 1 ? 's' : ''} ${t('ago')}`;
17
+ } else {
18
+ const diffInYears = Math.floor(diffInDays / 365);
19
+ return `${diffInYears} ${t('year')}${diffInYears > 1 ? 's' : ''} ${t('ago')}`;
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Возвращает продолжительность по переданным секундам в формате hh:mm:ss
3
+ * @param {seconds} number - число секунды
4
+ */
5
+ export function getDuration (seconds: number) {
6
+ const date = new Date(0);
7
+ date.setSeconds(seconds);
8
+ const setZero = (number: number) => number < 10 ? `0${number}` : number;
9
+
10
+ const setHours = () => {
11
+ if (date.getUTCHours() > 0) {
12
+ return `${setZero(date.getUTCHours())}:`;
13
+ }
14
+ return '';
15
+ };
16
+ return `${setHours()}${setZero(date.getUTCMinutes())}:${setZero(date.getUTCSeconds())}`;
17
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * возвращает название месяца по цифре
3
+ */
4
+ export function getMonth(t: (key: string) => string, number?: number) {
5
+ const months = [
6
+ 'january',
7
+ 'february',
8
+ 'march',
9
+ 'april',
10
+ 'may',
11
+ 'june',
12
+ 'july',
13
+ 'august',
14
+ 'september',
15
+ 'october',
16
+ 'november',
17
+ 'december'
18
+ ];
19
+ const index = typeof number === 'number' && number >= 0 && number < 12 ? number : 11;
20
+ const key = months[index];
21
+ return t(key) || key;
22
+ }
@@ -0,0 +1,26 @@
1
+ import type { RouteLocationNormalizedLoaded } from 'vue-router';
2
+ import type { IChipsItem, ISelectItem } from '../../types';
3
+ import { getSelectedQuery } from '../utils/get-selected-query';
4
+
5
+ export function getMultipleQuery (
6
+ route: RouteLocationNormalizedLoaded,
7
+ items: ISelectItem[] | IChipsItem[],
8
+ minKey: string,
9
+ maxKey: string,
10
+ ) {
11
+ const query = getSelectedQuery(route, items);
12
+ if (Array.isArray(query)) {
13
+ return {
14
+ [minKey]: query[0],
15
+ [maxKey]: query[1],
16
+ };
17
+ } else if (query === items[items.length - 1]?.query) {
18
+ return {
19
+ [minKey]: query,
20
+ };
21
+ } else {
22
+ return {
23
+ [maxKey]: query,
24
+ };
25
+ }
26
+ }
@@ -0,0 +1,6 @@
1
+ import type { RouteLocation } from 'vue-router';
2
+ import type { IChipsItem, ISelectItem } from '../../types';
3
+
4
+ export function getSelectedQuery(route: RouteLocation, items: IChipsItem[] | ISelectItem[]) {
5
+ return items.find(item => item.value === route.query[item.key || ''])?.query;
6
+ }
@@ -0,0 +1,15 @@
1
+ import { useBreakpoints } from '@vueuse/core';
2
+ import type { CssBreakpoints } from '../../types';
3
+
4
+ export const isMobile = (projectBreakpoints: Record<CssBreakpoints, number>) => {
5
+ const breakpoints = useBreakpoints(
6
+ {
7
+ xs: projectBreakpoints.xs,
8
+ sm: projectBreakpoints.sm,
9
+ md: projectBreakpoints.md,
10
+ lg: projectBreakpoints.lg,
11
+ xl: projectBreakpoints.xl,
12
+ }
13
+ );
14
+ return breakpoints.smaller('sm');
15
+ };
@@ -0,0 +1,43 @@
1
+ export function normalizeUrl(to: { path: string; query: Record<string, any> }) {
2
+ let path = to.path
3
+ let query = { ...to.query }
4
+
5
+ // 1) Удаляем page=1
6
+ if (query.page === '1') {
7
+ const { page, ...rest } = query
8
+ query = rest
9
+ }
10
+
11
+ // 2) Удаляем sort=trending
12
+ if (query.sort === 'trending') {
13
+ const { sort, ...rest } = query
14
+ query = rest
15
+ }
16
+
17
+ // Приводим query к нижнему регистру (ключи и строковые значения)
18
+ const loweredQuery: Record<string, any> = {}
19
+ for (const [key, value] of Object.entries(query)) {
20
+ const lowerKey = key.toLowerCase()
21
+ loweredQuery[lowerKey] = typeof value === 'string' ? value.toLowerCase() : value
22
+ }
23
+ query = loweredQuery
24
+
25
+ // 2) Приводим путь к нижнему регистру
26
+ path = path.toLowerCase()
27
+
28
+ // 3) Заменяем пробелы и %20 на дефисы
29
+ path = path
30
+ .replace(/%20/gi, '-') // encoded
31
+ .replace(/ /g, '-') // raw
32
+ .replace(/\+/g, '-') // some browsers encode spaces as +
33
+
34
+ // 4) Убираем двойные дефисы
35
+ path = path.replace(/--+/g, '-')
36
+
37
+ // 5) Убираем хвостовой /
38
+ if (path.length > 1) {
39
+ path = path.replace(/\/+$/, '')
40
+ }
41
+
42
+ return { path, query }
43
+ }
@@ -0,0 +1,5 @@
1
+ export function onBackdropClick(target: HTMLDialogElement, callback: () => void) {
2
+ if (target?.nodeName === 'DIALOG') {
3
+ callback();
4
+ }
5
+ }
@@ -0,0 +1,28 @@
1
+ let scrollY = 0;
2
+
3
+ const lockScroll = () => {
4
+ if (typeof window === 'undefined') return;
5
+
6
+ scrollY = window.scrollY;
7
+ document.body.style.position = 'fixed';
8
+ document.body.style.top = `-${scrollY}px`;
9
+ document.body.style.left = '0';
10
+ document.body.style.right = '0';
11
+ document.body.style.overflow = 'hidden';
12
+ };
13
+
14
+ const unlockScroll = () => {
15
+ if (typeof window === 'undefined') return;
16
+
17
+ document.body.style.position = '';
18
+ document.body.style.top = '';
19
+ document.body.style.left = '';
20
+ document.body.style.right = '';
21
+ document.body.style.overflow = '';
22
+ window.scrollTo(0, scrollY);
23
+ };
24
+
25
+ export const scrollLock = () => ({
26
+ lockScroll,
27
+ unlockScroll,
28
+ });
@@ -0,0 +1,14 @@
1
+ export class AbortControllerUtil {
2
+ private static abortController: AbortController | undefined;
3
+
4
+ static createAbortController(): AbortController {
5
+ this.abortController = new AbortController();
6
+ return this.abortController;
7
+ }
8
+
9
+ static getSignalAbortController(): AbortSignal | undefined {
10
+ const signal = this.abortController?.signal;
11
+ this.abortController = undefined;
12
+ return signal;
13
+ }
14
+ }
@@ -0,0 +1,41 @@
1
+ import type { NitroFetchOptions } from "nitropack/types";
2
+ import { AbortControllerUtil } from '../server/abort-controller';
3
+
4
+ /** Хелпер для работы с API */
5
+ export class ApiHelper {
6
+ /** Метод для получения данных */
7
+ static async fetch<T>(
8
+ path: string,
9
+ fetchOptions: NitroFetchOptions<string>,
10
+ runtimeConfig?: any,
11
+ ): Promise<T> {
12
+ // TODO поменяется апи ендпойнт, после переделать
13
+ // const apiDomain = useRuntimeConfig()?.public?.apiDomain;
14
+ const signal = AbortControllerUtil.getSignalAbortController();
15
+
16
+ const _fetchOptions = {
17
+ ...fetchOptions,
18
+ headers: {
19
+ ...(fetchOptions.headers || {}),
20
+ 'x-domain': runtimeConfig.public.xDomain,
21
+ },
22
+ body: fetchOptions.body !== undefined
23
+ ? fetchOptions.body : undefined,
24
+ };
25
+
26
+ try {
27
+ const res = await $fetch(`/bff${path}`, { retry: 0, ..._fetchOptions, signal, });
28
+
29
+ if (typeof res === 'string') {
30
+ return JSON.parse(res).data;
31
+ } else {
32
+ return res as T;
33
+ }
34
+ } catch (error) {
35
+ if (signal?.aborted) {
36
+ throw AbortControllerUtil.createAbortController();
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ export function getUrlWithProxiedParams(url: string, event) {
2
+ const _url = new URL(url);
3
+ _url.search = new URLSearchParams(getQuery(event));
4
+
5
+ return _url.toString();
6
+ }
@@ -0,0 +1,14 @@
1
+ export function parseApiError(error: any): string {
2
+ let errorMessage = 'Unknown error occurred.';
3
+
4
+ try {
5
+ const raw = error?.response?._data;
6
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
7
+
8
+ errorMessage = parsed?.error || error?.message || errorMessage;
9
+ } catch (parseError) {
10
+ console.warn('Failed to parse error._data', parseError);
11
+ }
12
+
13
+ return errorMessage;
14
+ }
@@ -0,0 +1,28 @@
1
+ import { H3Event } from "h3";
2
+
3
+ /** Класс для работы с серверным API (бекенд) */
4
+ export class ServerApiHelper {
5
+ /* Класс для запроса данных на бекенде */
6
+ public static async fetch<T>(url: string, event: H3Event, requestOptions: RequestInit): Promise<T> {
7
+ try {
8
+ const response = await fetch(url, {
9
+ headers: getHeaders(event),
10
+ ...requestOptions,
11
+ });
12
+ return (await response.json())?.data as T;
13
+ } catch (error) {
14
+ throw new Error(`Cannot fetch: ${error.data}`);
15
+ }
16
+ }
17
+ // TODO проверить а надо ли, если ничего не сломается оставить только один метод
18
+ public static async fetchNitro<T>(url: string, event: H3Event, requestOptions: RequestInit): Promise<T> {
19
+ try {
20
+ return JSON.parse(String(await $fetch(url, {
21
+ headers: getHeaders(event),
22
+ ...requestOptions,
23
+ })));
24
+ } catch (error) {
25
+ throw new Error(`Cannot fetch: ${error.data}`);
26
+ }
27
+ };
28
+ }
@@ -0,0 +1,4 @@
1
+ export function validateEmail(value: string) {
2
+ const re = /^[\w&*+-]+(?:\.[\w&*+-]+)*@(?:[\da-z-]+\.)+[a-z]{2,}$/gi;
3
+ return value?.length !== 0 && re.test(String(value).toLowerCase());
4
+ };
@@ -0,0 +1,3 @@
1
+ export function validatePassword(value: string) {
2
+ return value.length > 4;
3
+ };
@@ -0,0 +1,4 @@
1
+ export function validatePhone (value: string) {
2
+ const re = /^\+?[0-9\s\-().]{7,20}$/;
3
+ return value?.length !== 0 && re.test(String(value).trim());
4
+ };
@@ -0,0 +1,4 @@
1
+ export function validateUsername (value: string) {
2
+ const re = /^[a-zA-Z0-9_-]{2,20}$/;
3
+ return value.length !== 0 && re.test(String(value).toLowerCase());
4
+ };