itube-specs 0.0.649 → 0.0.652

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.
@@ -15,6 +15,7 @@
15
15
  :label="$t('email')"
16
16
  :placeholder="$t('email')"
17
17
  hide-label
18
+ inputmode="email"
18
19
  autocomplete="email"
19
20
  :error="error.email"
20
21
  @update:error="(val: boolean) => error.email = val"
@@ -40,7 +41,7 @@
40
41
 
41
42
  <script setup lang="ts">
42
43
  import type { IPasswordForm } from '../../types';
43
- import { validateEmail } from '../../runtime';
44
+ import { validateEmail } from 'itube-specs/runtime';
44
45
  import { AuthorizationApiService } from '~/services/api/authorization.service';
45
46
 
46
47
  const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
@@ -15,6 +15,7 @@
15
15
  :placeholder="t('email')"
16
16
  :label="t('email')"
17
17
  hide-label
18
+ inputmode="email"
18
19
  autocomplete="email"
19
20
  :error="error.email"
20
21
  @update:error="(val: boolean) => error.email = val"
@@ -80,8 +81,9 @@
80
81
 
81
82
  <script setup lang="ts">
82
83
  import type { IRegistrateForm } from '../../types';
83
- import { validateEmail, validatePassword, validateUsername } from '../../runtime';
84
+ import { EPlaylistType, validateEmail, validatePassword, validateUsername } from 'itube-specs/runtime';
84
85
  import { AuthorizationApiService } from '~/services/api/authorization.service';
86
+ import { PlaylistsApiService } from '~/services/api/playlists.service';
85
87
 
86
88
  const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
87
89
 
@@ -156,11 +158,13 @@ async function submit() {
156
158
 
157
159
  await useUser(AuthorizationApiService).register(form.value);
158
160
 
161
+ // Создаем сразу Favorites плейлист, возможно костыль убрать когда будет нормальная логика с беком
162
+ // const { FAVORITES_KEY } = useFavorites();
163
+ // await PlaylistsApiService.postPlaylist(FAVORITES_KEY, EPlaylistType.Private);
164
+
159
165
  useAuthPopup().closeAuthPopup();
160
166
  snackbarTheme.value = 'success';
161
167
  snackbarText.value = 'Registration success';
162
-
163
- // ... успех
164
168
  } catch (err) {
165
169
  setErrorState(err);
166
170
  console.error('Ошибка reCAPTCHA или регистрации:', err);
@@ -25,8 +25,8 @@
25
25
  </template>
26
26
 
27
27
  <script setup lang="ts">
28
- import type { IVideoCard } from '../../types';
29
- import { getDuration } from '../../runtime';
28
+ import type { IVideoCard } from 'itube-specs/types';
29
+ import { getDuration } from 'itube-specs/runtime';
30
30
 
31
31
  const props = defineProps<{
32
32
  card: IVideoCard
@@ -31,7 +31,6 @@
31
31
  :name="item.name"
32
32
  :model-value="getSelectValue(item.name)"
33
33
  :items="selectItems(item)"
34
- :count="getCount(item)"
35
34
  size="s"
36
35
  :active="isActiveSelect(item.name)"
37
36
  :label="item.title"
@@ -29,6 +29,10 @@
29
29
  :name="item.title"
30
30
  :items="item.items"
31
31
  :label="item?.label"
32
+ :label-icon="{
33
+ icon: item.icon,
34
+ prefix: 'categories'
35
+ }"
32
36
  :placeholder="item.placeholder"
33
37
  />
34
38
  </div>
@@ -35,7 +35,7 @@
35
35
  <span class="s-like__control-icon-wrapper">
36
36
  <SIcon class="s-like__control-icon --dislike" name="thumbs-down" size="16"/>
37
37
  </span>
38
- <span class="s-like__control-text">{{ formatNumber(Number(resultDislikes)) }}</span>
38
+ <span class="s-like__control-text">{{ formatNumber(Math.abs(Number(resultDislikes))) }}</span>
39
39
  </button>
40
40
  </div>
41
41
  </template>
@@ -10,13 +10,15 @@
10
10
  @back="activeStep === 'list' ? handleBack() : goToList()"
11
11
  >
12
12
  <template #title>{{ activeStep !== 'list' && activeCategory ? t(`report_form.${activeCategory.title}`) : t('report') }}</template>
13
-
14
- <!-- Step 1: Category list -->
15
- <div v-if="activeStep === 'list'" class="s-report__popup-wrapper">
13
+ <template #fixedContent>
16
14
  <SVideoMiniCard
17
15
  class="s-report__video-card"
18
16
  :card="reportedVideoCard"
19
17
  />
18
+ </template>
19
+
20
+ <!-- Step 1: Category list -->
21
+ <div v-if="activeStep === 'list'" class="s-report__popup-wrapper">
20
22
  <div class="s-report__categories">
21
23
  <button
22
24
  v-for="(item, index) in reportFormsScheme"
@@ -40,10 +42,6 @@
40
42
  class="s-report__popup-wrapper"
41
43
  :class="{'_loading': loading}"
42
44
  >
43
- <SVideoMiniCard
44
- class="s-report__video-card"
45
- :card="reportedVideoCard"
46
- />
47
45
  <div class="s-report__wrapper" v-if="activeCategory">
48
46
  <p
49
47
  v-if="activeCategory.text"
@@ -124,8 +122,8 @@
124
122
  </template>
125
123
 
126
124
  <script setup lang="ts">
127
- import { reportFormsScheme } from '../../lib/report-forms-scheme';
128
- import { EReportFormsSubjects, validateEmail, validatePhone } from '../../runtime';
125
+ import { reportFormsScheme } from 'itube-specs/lib/report-forms-scheme';
126
+ import { EReportFormsSubjects, validateEmail, validatePhone } from 'itube-specs/runtime';
129
127
  import type { InputTypes, IReportForm, IReportRequest } from '../../types';
130
128
 
131
129
  const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
@@ -1,17 +1,17 @@
1
1
  <template>
2
2
  <transition mode="out-in">
3
3
  <SPopup
4
- v-if="isSharePopupOpen"
5
- v-model="isSharePopupOpen"
4
+ v-if="model"
5
+ v-model="model"
6
6
  sheet
7
7
  >
8
8
  <template #title>{{ $t('share') }}</template>
9
+ <template v-if="$slots.card" #fixedContent>
10
+ <div class="s-share__card">
11
+ <slot name="card"/>
12
+ </div>
13
+ </template>
9
14
  <div class="s-share__popup-wrapper">
10
- <SVideoMiniCard
11
- v-if="sharedVideoCard && Object.keys(sharedVideoCard).length"
12
- class="s-share__video-card"
13
- :card="sharedVideoCard"
14
- />
15
15
  <div class="s-share__buttons">
16
16
  <a
17
17
  v-for="(item, index) in buttons"
@@ -25,37 +25,25 @@
25
25
  sizes="28px"
26
26
  width="28"
27
27
  height="28"
28
- :src="`/img/socials/${item.img}`"
28
+ :src="`/icons/socials/${item.img}`"
29
29
  />
30
30
  </a>
31
31
  </div>
32
- <div class="s-share__wrapper">
33
- <div class="s-share__copy">
34
- <span class="s-share__copy-label _from-sm">{{ inputText }}</span>
35
- <span class="s-share__copy-url">{{ fullUrl }}</span>
36
- <button
37
- class="s-share__copy-button"
38
- :title="$t('copy')"
39
- @click="copyUrl"
40
- type="button"
41
- >
42
- <SIcon
43
- class="s-share__copy-icon"
44
- :class="{'--accent': isCopied}"
45
- :name="copyIcon"
46
- size="20"
47
- />
48
- </button>
49
- </div>
50
- <div class="s-share__cancel-wrapper">
51
- <SButton
52
- class="s-share__cancel"
53
- wide
54
- theme="secondary"
55
- @click="closeSharePopup"
56
- >{{ $t('cancel') }}
57
- </SButton>
58
- </div>
32
+ <div class="s-share__copy">
33
+ <span class="s-share__copy-url">{{ fullUrl }}</span>
34
+ <button
35
+ class="s-share__copy-button"
36
+ :title="$t('copy')"
37
+ @click="copyUrl"
38
+ type="button"
39
+ >
40
+ <SIcon
41
+ class="s-share__copy-icon"
42
+ :class="{'--accent': isCopied}"
43
+ :name="copyIcon"
44
+ size="20"
45
+ />
46
+ </button>
59
47
  </div>
60
48
  </div>
61
49
  </SPopup>
@@ -63,15 +51,21 @@
63
51
  </template>
64
52
 
65
53
  <script setup lang="ts">
66
- const { isSharePopupOpen, closeSharePopup, sharedVideoCard } = useSharePopup();
54
+ const model = defineModel<boolean>({ default: false });
55
+
56
+ const props = defineProps<{
57
+ url?: string
58
+ }>();
67
59
 
68
60
  const route = useRoute();
69
61
 
70
62
  const isCopied = ref(false);
71
63
  let copyTimeout: ReturnType<typeof setTimeout> | null = null;
72
64
 
65
+ const origin = computed(() => process.client ? window.location.origin : '');
66
+
73
67
  const fullUrl = computed(() =>
74
- (process.client ? window.location.origin : '') + route.fullPath
68
+ props.url ? origin.value + props.url : origin.value + route.fullPath
75
69
  );
76
70
 
77
71
  function copyUrl() {
@@ -82,7 +76,6 @@ function copyUrl() {
82
76
  }
83
77
 
84
78
  const copyIcon = computed(() => isCopied.value ? 'check' : 'copy');
85
- const inputText = computed(() => isCopied.value ? t('link_copied') : t('link'));
86
79
 
87
80
  watch(() => isCopied.value, (value) => {
88
81
  if (value) {
@@ -94,8 +87,6 @@ onBeforeUnmount(() => {
94
87
  if (copyTimeout) clearTimeout(copyTimeout);
95
88
  });
96
89
 
97
- const { t } = useI18n()
98
-
99
90
  const buttons = computed(() => {
100
91
  return [
101
92
  {
@@ -21,73 +21,86 @@
21
21
  class="s-playlist-add__wrapper"
22
22
  :class="{'_loading': loadingUserPlaylists || loadingPostVideo}"
23
23
  >
24
+ <SCheckbox
25
+ right
26
+ class="s-playlist-add__item"
27
+ :model-value="selectValue.includes('favorites')"
28
+ @update:model-value="togglePlaylist('favorites')"
29
+ >
30
+ <div class="s-playlist-add__item-wrapper --favorites">
31
+ <SIcon name="heart" size="20" />
32
+ {{ $t('favorites')}}
33
+ </div>
34
+ </SCheckbox>
24
35
  <template
25
36
  v-if="playlistsItems && playlistsItems.length"
26
37
  >
27
- <SCheckbox
28
- right
29
- class="s-playlist-add__item"
30
- :model-value="selectValue.includes('favorites')"
31
- @update:model-value="togglePlaylist('favorites')"
32
- >
33
- <div class="s-playlist-add__item-wrapper --favorites">
34
- <SIcon name="heart" size="20" />
35
- {{ $t('favorites')}}
36
- </div>
37
- </SCheckbox>
38
- <SCheckbox
39
- v-for="(item, index) in playlistsItems"
40
- class="s-playlist-add__item"
41
- right
42
- :key="`playlist-${index}`"
43
- :model-value="selectValue.includes(item.value)"
44
- @update:model-value="togglePlaylist(item.value)"
45
- >
46
- <div class="s-playlist-add__item-wrapper">
47
- <SIcon
48
- class="s-playlist-add__item-icon"
49
- name="list"
50
- size="20"
51
- />
52
- <span class="s-playlist-add__item-text">{{ item.title }}</span>
53
- <div class="s-playlist-add__item-icon">
54
- <SIcon name="video-camera" size="16" />
55
- {{ item.videosCount }}
38
+ <template v-for="(item, index) in playlistsItems" :key="`playlist-${index}`">
39
+ <SPlaylistInput
40
+ v-if="editingId === item.value"
41
+ v-model="editName"
42
+ v-model:type="editType"
43
+ :placeholder="$t('playlist_name')"
44
+ edit-playlist
45
+ @close="editingId = null"
46
+ @submit="saveEdit(item.value)"
47
+ @delete="deleteEdit(item.value)"
48
+ />
49
+ <SCheckbox
50
+ v-else
51
+ class="s-playlist-add__item"
52
+ right
53
+ :model-value="selectValue.includes(item.value)"
54
+ @update:model-value="togglePlaylist(item.value)"
55
+ >
56
+ <div class="s-playlist-add__item-wrapper">
57
+ <SIcon
58
+ class="s-playlist-add__item-icon"
59
+ name="list"
60
+ size="20"
61
+ />
62
+ <span class="s-playlist-add__item-text">{{ item.title }}</span>
63
+ <SIcon
64
+ class="s-playlist-add__item-icon"
65
+ :name="item.typeIcon"
66
+ size="16"
67
+ />
68
+ <div class="s-playlist-add__item-icon">
69
+ <SIcon name="video-camera" size="16" />
70
+ {{ item.videosCount }}
71
+ </div>
56
72
  </div>
57
- </div>
58
- </SCheckbox>
73
+ <button class="s-playlist-add__item-edit" type="button" @click.prevent="startEdit(item)">
74
+ <SIcon name="pen" size="16"/>
75
+ </button>
76
+ </SCheckbox>
77
+ </template>
59
78
  </template>
60
- <button
61
- v-if="step !== 'new'"
62
- type="button"
63
- class="s-playlist-add__item --new"
64
- @click="step = 'new'"
65
- >
66
- <SIcon
67
- class="s-playlist-add__item-icon --new"
68
- name="plus"
69
- size="20"
70
- />
71
- {{ $t('playlist.new_playlist') }}
72
- </button>
73
- <div v-else class="s-playlist-add__new">
74
- <SInput
79
+ <template v-if="!editingId">
80
+ <button
81
+ v-if="step !== 'new'"
82
+ type="button"
83
+ class="s-playlist-add__item --new"
84
+ @click="step = 'new'"
85
+ >
86
+ <SIcon
87
+ class="s-playlist-add__item-icon --new"
88
+ name="plus"
89
+ size="20"
90
+ />
91
+ {{ $t('playlist.new_playlist') }}
92
+ </button>
93
+ <SPlaylistInput
94
+ v-else
75
95
  v-model="newPlaylistName"
76
- :placeholder="$t('playlist.new_playlist')"
77
- :icon="newPlaylistName ? 'close' : undefined"
96
+ v-model:type="playlistType"
78
97
  :error="nameError"
79
- @icon-click="clearNewPlaylist"
98
+ :placeholder="$t('playlist_name')"
80
99
  @update:error="nameError = $event"
100
+ @close="clearNewPlaylist"
101
+ @submit="onSaveClick"
81
102
  />
82
- <SButton
83
- size="s"
84
- theme="primary"
85
- :disabled="!newPlaylistName.trim()"
86
- @click="createPlaylist"
87
- >
88
- {{ $t('create') }}
89
- </SButton>
90
- </div>
103
+ </template>
91
104
  </div>
92
105
 
93
106
  <template #footer>
@@ -105,8 +118,8 @@
105
118
  size="l"
106
119
  :disabled="loadingUserPlaylists || loadingPostVideo"
107
120
  theme="primary"
108
- @click="onSaveClick"
109
- >{{ $t('add') }}
121
+ @click="editingId ? saveEdit(editingId) : onSaveClick()"
122
+ >{{ editingId ? $t('update') : $t('add') }}
110
123
  </SButton>
111
124
  </div>
112
125
  </template>
@@ -128,12 +141,14 @@ const props = defineProps<{
128
141
  }>()
129
142
 
130
143
  const emit = defineEmits<{
131
- (eventName: 'post-playlist', eventValue: IPlaylistShort): void
132
144
  (eventName: 'execute-user-playlists'): void
133
145
  (eventName: 'save-video-to-playlist', eventValue: {
134
146
  form: IPlaylistVideoFormType
135
147
  id: string
148
+ newPlaylist?: IPlaylistShort
136
149
  }): void
150
+ (eventName: 'edit-playlist', eventValue: { id: string, playlistName: string, playlistType: EPlaylistType }): void
151
+ (eventName: 'delete-playlist', eventValue: string): void
137
152
  }>()
138
153
 
139
154
  const { videoCard, closePlaylistAdd, isPlaylistAdd, selectValue, step } = usePlaylistAdd();
@@ -147,6 +162,8 @@ const playlistsItems = computed(() => {
147
162
  title: item.name,
148
163
  value: item.id,
149
164
  videosCount: item.videosCount,
165
+ typeIcon: item.playlistType === EPlaylistType.Private ? 'lock' : 'user-group',
166
+ playlistType: item.playlistType === EPlaylistType.Private ? 'private' as const : 'public' as const,
150
167
  }))
151
168
  })
152
169
 
@@ -161,45 +178,55 @@ function togglePlaylist(value: string) {
161
178
  }
162
179
 
163
180
  watch(isPlaylistAdd, (val) => {
164
- if (val) emit('execute-user-playlists');
181
+ if (val) {
182
+ selectValue.value = ['favorites'];
183
+ emit('execute-user-playlists');
184
+ }
165
185
  }, { immediate: true });
166
186
 
167
187
  async function closePopup() {
168
188
  step.value = 'add';
189
+ selectValue.value = [];
190
+ newPlaylistName.value = '';
191
+ playlistType.value = 'public';
192
+ editingId.value = null;
169
193
  isPlaylistAdd.value = false;
170
194
  closePlaylistAdd();
171
195
  }
172
196
 
173
197
  const newPlaylistName = ref('');
174
- const isPrivate = ref(false);
198
+ const playlistType = ref<'public' | 'private'>('public');
175
199
  const nameError = ref(false);
176
200
 
177
201
  function clearNewPlaylist() {
178
202
  newPlaylistName.value = '';
203
+ playlistType.value = 'public';
204
+ step.value = 'add';
179
205
  }
180
206
 
181
- function createPlaylist() {
182
- const name = newPlaylistName.value.trim();
183
- if (!name) return;
184
-
185
- const nameExists = props.userPlaylists?.items?.some(
186
- (item: IPlaylistCard) => item.name.toLowerCase() === name.toLowerCase()
187
- );
207
+ const editingId = ref<string | null>(null);
208
+ const editName = ref('');
209
+ const editType = ref<'public' | 'private'>('public');
188
210
 
189
- if (nameExists) {
190
- nameError.value = true;
191
- snackbarTheme.value = 'error';
192
- snackbarText.value = 'playlist_name_exists';
193
- return;
194
- }
211
+ function startEdit(item: { value: string, title: string, playlistType: 'public' | 'private' }) {
212
+ clearNewPlaylist();
213
+ editingId.value = item.value;
214
+ editName.value = item.title;
215
+ editType.value = item.playlistType;
216
+ }
195
217
 
196
- emit('post-playlist', {
197
- name,
198
- type: isPrivate.value ? EPlaylistType.Private : EPlaylistType.Public,
218
+ function saveEdit(id: string) {
219
+ emit('edit-playlist', {
220
+ id,
221
+ playlistName: editName.value,
222
+ playlistType: editType.value === 'private' ? EPlaylistType.Private : EPlaylistType.Public,
199
223
  });
200
- newPlaylistName.value = '';
201
- isPrivate.value = false;
202
- step.value = 'add';
224
+ editingId.value = null;
225
+ }
226
+
227
+ function deleteEdit(id: string) {
228
+ editingId.value = null;
229
+ emit('delete-playlist', id);
203
230
  }
204
231
 
205
232
  const postVideoForm = computed(() => ({
@@ -215,10 +242,34 @@ const postVideoForm = computed(() => ({
215
242
  const { setErrorState, snackbarText, snackbarTheme } = useSnackbar();
216
243
 
217
244
  async function onSaveClick() {
245
+ let newPlaylist: IPlaylistShort | undefined;
246
+
247
+ if (step.value === 'new') {
248
+ const name = newPlaylistName.value.trim();
249
+ if (!name) return;
250
+
251
+ const nameExists = props.userPlaylists?.items?.some(
252
+ (item: IPlaylistCard) => item.name.toLowerCase() === name.toLowerCase()
253
+ );
254
+
255
+ if (nameExists) {
256
+ nameError.value = true;
257
+ snackbarTheme.value = 'error';
258
+ snackbarText.value = 'playlist_name_exists';
259
+ return;
260
+ }
261
+
262
+ newPlaylist = {
263
+ name,
264
+ type: playlistType.value === 'private' ? EPlaylistType.Private : EPlaylistType.Public,
265
+ };
266
+ }
267
+
218
268
  try {
219
269
  emit('save-video-to-playlist', {
220
270
  form: postVideoForm.value as IPlaylistVideoFormType,
221
- id: String(selectValue.value)
271
+ id: String(selectValue.value),
272
+ newPlaylist,
222
273
  })
223
274
  closePopup();
224
275
  snackbarTheme.value = 'success';
@@ -6,14 +6,14 @@
6
6
  sheet
7
7
  transparent-backdrop
8
8
  >
9
- <template #title>{{ $t('playlist.delete_video') }}</template>
9
+ <template #title>{{ $t('remove_video') }}</template>
10
10
 
11
11
  <div
12
12
  class="s-playlist-delete-video__wrapper"
13
13
  :class="{'_loading': loadingAll}"
14
14
  >
15
15
  <SVideoMiniCard v-if="deletedVideo" :card="deletedVideo"/>
16
- <p class="s-playlist-delete-video__text">{{ $t('playlist.confirm_delete_video') }}</p>
16
+ <p class="s-playlist-delete-video__text">{{ $t('confirm_remove_video') }}</p>
17
17
  </div>
18
18
  <template #footer>
19
19
  <div class="s-playlist-delete-video__buttons">
@@ -31,7 +31,7 @@
31
31
  size="l"
32
32
  theme="primary"
33
33
  @click="onBinClick"
34
- >{{ $t('playlist.delete') }}
34
+ >{{ $t('remove') }}
35
35
  </SButton>
36
36
  </div>
37
37
  </template>
@@ -41,7 +41,7 @@
41
41
  </template>
42
42
 
43
43
  <script setup lang="ts">
44
- import { EPlaylistStep } from '../../runtime';
44
+ import { EPlaylistStep } from 'itube-specs/runtime';
45
45
 
46
46
  const { isPlaylistEditOpen, closePlaylistEdit, currentStep, selectedPlaylist, deletedVideo } = usePlaylistEdit();
47
47
  const { snackbarText, snackbarTheme } = useSnackbar();
@@ -69,7 +69,7 @@ async function onBinClick() {
69
69
  deletedVideoId: deletedVideo.value?.id || ''
70
70
  })
71
71
  snackbarTheme.value = 'success';
72
- snackbarText.value = 'video_deleted';
72
+ snackbarText.value = 'video_removed';
73
73
  closePlaylistEdit();
74
74
  } catch {
75
75
  console.log('error delete')
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div
3
+ class="s-playlist-input"
4
+ @keydown.enter="$emit('submit')"
5
+ >
6
+ <SInput
7
+ v-model="name"
8
+ :placeholder="placeholder"
9
+ :error="error"
10
+ :icon="name.length > 0 ? 'close' : ''"
11
+ @icon-click="name = ''"
12
+ @update:error="$emit('update:error', $event)"
13
+ />
14
+ <button
15
+ type="button"
16
+ class="s-playlist-input__back"
17
+ @click="$emit('close')"
18
+ >
19
+ <SIcon name="close" size="20"/>
20
+ </button>
21
+ <div class="s-playlist-input__visibility">
22
+ <SToggle
23
+ :model-value="currentType === 'public'"
24
+ @update:model-value="currentType = $event ? 'public' : 'private'"
25
+ >
26
+ <div class="s-playlist-input__toggle-wrapper">
27
+ <SIcon class="s-playlist-input__toggle-icon" :name="PLAYLIST_TYPE_ICON[EPlaylistType.Public]" size="14"/>
28
+ {{ $t('public') }}
29
+ </div>
30
+ </SToggle>
31
+ <STooltip class="s-playlist-input__tooltip" right>
32
+ <template #trigger>
33
+ <span class="s-playlist-input__hint">?</span>
34
+ </template>
35
+ <p class="s-playlist-input__tooltip-text"><span>{{ $t('public') }}: </span>{{ $t('public_hint') }}</p>
36
+ <p class="s-playlist-input__tooltip-text"><span>{{ $t('private') }}: </span>{{ $t('private_hint') }}</p>
37
+ </STooltip>
38
+ <button
39
+ v-if="editPlaylist"
40
+ type="button"
41
+ class="s-playlist-input__delete"
42
+ @click="$emit('delete')"
43
+ >
44
+ {{ $t('delete') }}
45
+ </button>
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { EPlaylistType } from 'itube-specs/runtime';
52
+
53
+ const PLAYLIST_TYPE_ICON: Record<EPlaylistType, string> = {
54
+ [EPlaylistType.Public]: 'user-group',
55
+ [EPlaylistType.Private]: 'lock',
56
+ };
57
+
58
+ const props = withDefaults(defineProps<{
59
+ modelValue: string
60
+ type: 'public' | 'private'
61
+ error?: boolean
62
+ placeholder?: string
63
+ editPlaylist?: boolean
64
+ }>(), {
65
+ error: false,
66
+ placeholder: '',
67
+ editPlaylist: false,
68
+ });
69
+
70
+ const emit = defineEmits<{
71
+ (eventName: 'update:modelValue', value: string): void
72
+ (eventName: 'update:type', value: 'public' | 'private'): void
73
+ (eventName: 'update:error', value: boolean): void
74
+ (eventName: 'close'): void
75
+ (eventName: 'submit'): void
76
+ (eventName: 'delete'): void
77
+ }>();
78
+
79
+ const name = computed({
80
+ get: () => props.modelValue,
81
+ set: (val: string) => emit('update:modelValue', val),
82
+ });
83
+
84
+ const currentType = computed({
85
+ get: () => props.type,
86
+ set: (val: 'public' | 'private') => emit('update:type', val),
87
+ });
88
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div class="s-avatar">
3
+ <img
4
+ v-if="src || data.avatar"
5
+ :src="avatar"
6
+ :width="size"
7
+ :height="size"
8
+ :alt="$t(alt)"
9
+ >
10
+ <span
11
+ v-else
12
+ class="s-avatar__placeholder"
13
+ :style="{ width: size + 'px', height: size + 'px' }"
14
+ >{{ avatar }}</span>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import type { IProfileData } from 'itube-specs';
20
+
21
+ const props = withDefaults(defineProps<{
22
+ size?: number
23
+ alt?: string
24
+ src?: string
25
+ }>(), {
26
+ size: 36,
27
+ alt: 'avatar'
28
+ });
29
+
30
+ const data = computed(() => useState<IProfileData>('profileData').value);
31
+
32
+ const avatar = computed(() => props.src || (data.value && (data.value.avatar || data.value.username.charAt(0))));
33
+ </script>
@@ -19,10 +19,12 @@
19
19
  </template>
20
20
 
21
21
  <script setup lang="ts">
22
+ import type { ButtonThemes } from '../../types';
23
+
22
24
  withDefaults(
23
25
  defineProps<{
24
26
  type?: 'button' | 'submit' | 'reset',
25
- theme?: 'primary' | 'secondary' | 'ghost' | 'bordered' | 'tab' | 'warning' | 'muted'
27
+ theme?: ButtonThemes
26
28
  size?: 'm' | 's' | 'l',
27
29
  icon?: boolean,
28
30
  wide?: boolean,
@@ -48,7 +48,7 @@
48
48
 
49
49
  <script setup lang="ts">
50
50
  // На компоненте обязательно вызывать пропсы слота и вешать слот пропс (className) на непосредственных детей
51
- import { isMobileWidth } from '../../runtime';
51
+ import { isMobileWidth } from 'itube-specs/runtime';
52
52
  import type { CssBreakpoints } from '../../types';
53
53
  import { onClickOutside } from '@vueuse/core';
54
54
 
@@ -93,11 +93,10 @@ function openDropdown() {
93
93
  open.value = !open.value
94
94
  if (open.value) nextTick(() => checkPosition())
95
95
 
96
- if (isMobile.value) {
96
+ if (isMobile.value && open.value) {
97
97
  nextTick(() => {
98
98
  if (menuRef.value?.showModal) {
99
99
  menuRef.value.showModal();
100
- scrollLock();
101
100
  }
102
101
  })
103
102
  }
@@ -107,13 +106,27 @@ function close() {
107
106
  open.value = false;
108
107
 
109
108
  if (isMobile.value) {
110
- scrollUnlock()
111
109
  nextTick(() => {
112
110
  menuRef.value?.close?.()
113
111
  })
114
112
  }
115
113
  }
116
114
 
115
+ watch(open, (val) => {
116
+ if (!isMobile.value) return;
117
+ if (val) {
118
+ scrollLock();
119
+ } else {
120
+ scrollUnlock();
121
+ }
122
+ });
123
+
124
+ onBeforeUnmount(() => {
125
+ if (open.value && isMobile.value) {
126
+ scrollUnlock();
127
+ }
128
+ });
129
+
117
130
  let hoverTimeout: ReturnType<typeof setTimeout> | null = null
118
131
 
119
132
  function mouseHandler(event: boolean) {
@@ -17,9 +17,11 @@
17
17
  </template>
18
18
 
19
19
  <script setup lang="ts">
20
+ import type { ButtonThemes } from '../../types';
21
+
20
22
  withDefaults(
21
23
  defineProps<{
22
- theme?: 'primary' | 'secondary' | 'ghost' | 'bordered' | 'tab' | 'warning' | 'muted',
24
+ theme?: ButtonThemes,
23
25
  size?: 'm' | 's',
24
26
  wide?: boolean,
25
27
  active?: boolean,
@@ -4,7 +4,6 @@
4
4
  aria-label="select"
5
5
  :class="[
6
6
  {
7
- '--wide': wide,
8
7
  '--active': active
9
8
  },
10
9
  `--${size}`,
@@ -14,11 +13,11 @@
14
13
  <SIcon
15
14
  v-if="labelIcon"
16
15
  class="s-select__label-icon"
17
- :name="labelIcon"
16
+ :name="labelIcon.icon"
18
17
  size="16"
18
+ :prefix="labelIcon.prefix"
19
19
  />
20
20
  {{ label }}
21
- <!-- <SCount class="s-select__count" v-if="count">{{ count }}</SCount>-->
22
21
  </span>
23
22
  <span class="s-select__wrapper">
24
23
  <SIcon
@@ -58,22 +57,26 @@
58
57
 
59
58
  <script setup lang="ts">
60
59
  import type { ISelectItem } from '../../types';
61
- import type { LocaleObject } from '#internal-i18n-types';
62
- import { convertString } from '~/runtime';
60
+ import { convertString } from 'itube-specs/runtime';
63
61
 
64
- const props = withDefaults(defineProps<{
62
+ export interface ISelectLabelIcon {
63
+ icon: string
64
+ prefix?: string
65
+ }
66
+
67
+ export interface ISelectProps {
65
68
  name: string
66
- icon?: string
67
- labelIcon?: string
68
- placeholder?: string
69
69
  modelValue: string | undefined
70
+ items: ISelectItem[]
71
+ placeholder?: string
70
72
  label?: string
71
- items: ISelectItem[] | LocaleObject[]
72
- wide?: boolean
73
+ icon?: string
74
+ labelIcon?: ISelectLabelIcon
73
75
  size?: 'm' | 's'
74
- // count?: string | number
75
76
  active?: boolean
76
- }>(), {
77
+ }
78
+
79
+ const props = withDefaults(defineProps<ISelectProps>(), {
77
80
  size: 'm'
78
81
  })
79
82
 
@@ -4,7 +4,8 @@
4
4
  aria-label="Toggle"
5
5
  :class="[
6
6
  {'s-toggle--checked': modelValue},
7
- {'s-toggle--disabled': disabled}
7
+ {'s-toggle--disabled': disabled},
8
+ {'--left': left}
8
9
  ]"
9
10
  >
10
11
  <input
@@ -16,7 +17,6 @@
16
17
  >
17
18
  <span v-if="$slots.default" class="s-toggle__text">
18
19
  <slot/>
19
- {{ modelValue}}
20
20
  </span>
21
21
  </label>
22
22
  </template>
@@ -25,5 +25,6 @@
25
25
  defineProps<{
26
26
  modelValue: boolean
27
27
  disabled?: boolean
28
+ left?: boolean
28
29
  }>()
29
30
  </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div
3
+ ref="tooltipRef"
4
+ class="s-tooltip"
5
+ @mouseenter="onEnter"
6
+ @mouseleave="onLeave"
7
+ >
8
+ <div class="s-tooltip__trigger" @click="onClick">
9
+ <slot name="trigger"/>
10
+ </div>
11
+ <transition name="fade">
12
+ <div v-if="isOpen" class="s-tooltip__content" :class="{'--right': right}">
13
+ <slot/>
14
+ </div>
15
+ </transition>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { isMobileWidth } from 'itube-specs/runtime';
21
+ import { onClickOutside } from '@vueuse/core';
22
+ import type { CssBreakpoints } from '../../types';
23
+
24
+ defineProps<{
25
+ right?: boolean
26
+ }>();
27
+
28
+ const tooltipRef = ref<HTMLElement>();
29
+ const isOpen = ref(false);
30
+
31
+ const breakpoints = useAppConfig().cssBreakpoints as Record<CssBreakpoints, number>;
32
+ const isMobile = isMobileWidth(breakpoints);
33
+
34
+ onClickOutside(tooltipRef, () => {
35
+ if (isMobile.value) {
36
+ isOpen.value = false;
37
+ }
38
+ });
39
+
40
+ function onEnter() {
41
+ if (!isMobile.value) {
42
+ isOpen.value = true;
43
+ }
44
+ }
45
+
46
+ function onLeave() {
47
+ if (!isMobile.value) {
48
+ isOpen.value = false;
49
+ }
50
+ }
51
+
52
+ function onClick() {
53
+ if (isMobile.value) {
54
+ isOpen.value = !isOpen.value;
55
+ }
56
+ }
57
+ </script>
@@ -1,5 +1,5 @@
1
1
  import type { IFilterScheme, IGroupCategories } from '../types';
2
- import { convertString } from '../runtime';
2
+ import { convertString } from 'itube-specs/runtime';
3
3
 
4
4
  export const useFilterScheme = (t, groupsCategories: IGroupCategories[] | null) => {
5
5
  const getFilterSchemeCategories = (): IFilterScheme[] => {
@@ -7,6 +7,7 @@ export const useFilterScheme = (t, groupsCategories: IGroupCategories[] | null)
7
7
  label: item.title,
8
8
  name: item.name,
9
9
  title: item.title,
10
+ icon: item.icon,
10
11
  placeholder: t('choose_category'),
11
12
  items: item.categories?.map((subItem) => ({
12
13
  title: convertString().toCapitalize(subItem.title),
@@ -1,21 +1,29 @@
1
- import { useScrollLock } from '@vueuse/core';
1
+ function hasScrollableParent(target: HTMLElement): boolean {
2
+ let el: HTMLElement | null = target;
3
+ while (el && el !== document.body) {
4
+ const { overflowY } = window.getComputedStyle(el);
5
+ if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) {
6
+ return true;
7
+ }
8
+ el = el.parentElement;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function preventTouchScroll(e: TouchEvent) {
14
+ if (!hasScrollableParent(e.target as HTMLElement)) {
15
+ e.preventDefault();
16
+ }
17
+ }
2
18
 
3
19
  export function useGlobalScrollLock() {
4
20
  const lockCount = useState('global-scroll-lock-count', () => 0);
5
- let isLockedRef: ReturnType<typeof useScrollLock> | null = null;
6
-
7
- function getLockedRef() {
8
- if (!isLockedRef && import.meta.client) {
9
- isLockedRef = useScrollLock(document.body);
10
- }
11
- return isLockedRef;
12
- }
13
21
 
14
22
  function scrollLock() {
15
23
  lockCount.value++;
16
- const ref = getLockedRef();
17
- if (lockCount.value === 1 && ref) {
18
- ref.value = true;
24
+ if (lockCount.value === 1 && import.meta.client) {
25
+ document.body.style.overflow = 'hidden';
26
+ document.addEventListener('touchmove', preventTouchScroll, { passive: false });
19
27
  }
20
28
  }
21
29
 
@@ -23,12 +31,12 @@ export function useGlobalScrollLock() {
23
31
  lockCount.value--;
24
32
  if (lockCount.value <= 0) {
25
33
  lockCount.value = 0;
26
- const ref = getLockedRef();
27
- if (ref) {
28
- ref.value = false;
34
+ if (import.meta.client) {
35
+ document.body.style.overflow = '';
36
+ document.removeEventListener('touchmove', preventTouchScroll);
29
37
  }
30
38
  }
31
39
  }
32
40
 
33
- return { scrollLock, scrollUnlock }
41
+ return { scrollLock, scrollUnlock };
34
42
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.649",
4
+ "version": "0.0.652",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -1 +1 @@
1
- export type ButtonThemes = 'primary' | 'secondary' | 'ghost' | 'bordered' | 'tab' | undefined
1
+ export type ButtonThemes = 'primary' | 'secondary' | 'warning' | 'muted' | undefined