itube-specs 0.0.902 → 0.0.910
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.
- package/assets/icons/common/flag.svg +1 -1
- package/assets/icons/common/heart-stroke.svg +1 -1
- package/assets/icons/common/heart.svg +1 -1
- package/assets/icons/common/share.svg +1 -1
- package/assets/icons/common/thumbs-down-filled.svg +1 -0
- package/assets/icons/common/thumbs-down.svg +1 -1
- package/assets/icons/common/thumbs-up-filled.svg +1 -0
- package/assets/icons/common/thumbs-up.svg +1 -1
- package/assets/scss/themes/dark/_theme.scss +49 -19
- package/assets/scss/themes/light/_theme.scss +46 -16
- package/assets/scss/vars/_palette.scss +5 -0
- package/assets/scss/vars/_vars.scss +1 -0
- package/components/auth/login.vue +2 -2
- package/components/auth/recovery.vue +2 -2
- package/components/auth/register.vue +2 -2
- package/components/card/collections-more.vue +133 -0
- package/components/card/playlist.vue +6 -1
- package/components/grid/playlists.vue +3 -0
- package/components/info/main.vue +18 -14
- package/components/library/feed.vue +20 -39
- package/components/library/follows.vue +19 -4
- package/components/player/interface.vue +132 -58
- package/components/player/like.vue +82 -60
- package/components/playlist/add.vue +2 -2
- package/components/playlist/delete-video.vue +2 -2
- package/components/playlist/edit.vue +3 -3
- package/components/profile/delete.vue +2 -2
- package/components/profile/setup.vue +2 -2
- package/components/report/main.vue +9 -4
- package/components/search/input.vue +9 -9
- package/components/section/collections.vue +110 -0
- package/components/ui/follow-btn.vue +162 -0
- package/components/ui/section-title.vue +2 -0
- package/components/ui/snackbar.vue +93 -44
- package/composables/__tests__/use-snackbar.test.ts +126 -20
- package/composables/use-snackbar.ts +75 -20
- package/composables/use-user.ts +4 -4
- package/package.json +1 -1
- package/pages/contacts.vue +2 -2
- package/pages/library/index.vue +1 -1
- package/pages/playlists/index.vue +55 -4
- package/assets/icons/common/crest-in-circle.svg +0 -1
|
@@ -15,43 +15,40 @@ describe('useSnackbar', () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('дефолтное состояние', () => {
|
|
18
|
-
const {
|
|
19
|
-
expect(snackbarTheme.value).toBe('success');
|
|
18
|
+
const { snackbarText, snackbarIcon, snackbarHighlight, snackbarActionText } = useSnackbar();
|
|
20
19
|
expect(snackbarText.value).toBe('');
|
|
21
20
|
expect(snackbarIcon.value).toBe('check');
|
|
21
|
+
expect(snackbarHighlight.value).toBe('');
|
|
22
|
+
expect(snackbarActionText.value).toBe('');
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
it('
|
|
25
|
-
const {
|
|
26
|
-
|
|
25
|
+
it('show показывает и авто-сбрасывает через 4000мс', () => {
|
|
26
|
+
const { show, snackbarText, snackbarIcon } = useSnackbar();
|
|
27
|
+
show('Done');
|
|
27
28
|
expect(snackbarText.value).toBe('Done');
|
|
28
|
-
expect(snackbarTheme.value).toBe('success');
|
|
29
29
|
expect(snackbarIcon.value).toBe('check');
|
|
30
30
|
|
|
31
|
-
vi.advanceTimersByTime(
|
|
31
|
+
vi.advanceTimersByTime(3999);
|
|
32
32
|
expect(snackbarText.value).toBe('Done');
|
|
33
33
|
vi.advanceTimersByTime(1);
|
|
34
34
|
expect(snackbarText.value).toBe('');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it('showError показывает и авто-сбрасывает через 4000мс', () => {
|
|
38
|
-
const { showError, snackbarText,
|
|
38
|
+
const { showError, snackbarText, snackbarIcon } = useSnackbar();
|
|
39
39
|
showError('Error!');
|
|
40
40
|
expect(snackbarText.value).toBe('Error!');
|
|
41
|
-
expect(snackbarTheme.value).toBe('error');
|
|
42
41
|
expect(snackbarIcon.value).toBe('close');
|
|
43
42
|
|
|
44
43
|
vi.advanceTimersByTime(3999);
|
|
45
44
|
expect(snackbarText.value).toBe('Error!');
|
|
46
45
|
vi.advanceTimersByTime(1);
|
|
47
46
|
expect(snackbarText.value).toBe('');
|
|
48
|
-
expect(snackbarTheme.value).toBe('success');
|
|
49
47
|
});
|
|
50
48
|
|
|
51
49
|
it('setErrorState нормализует ошибку и показывает её', () => {
|
|
52
|
-
const { setErrorState,
|
|
50
|
+
const { setErrorState, snackbarIcon, snackbarText } = useSnackbar();
|
|
53
51
|
setErrorState({ message: 'Something failed' });
|
|
54
|
-
expect(snackbarTheme.value).toBe('error');
|
|
55
52
|
expect(snackbarIcon.value).toBe('close');
|
|
56
53
|
expect(snackbarText.value).toBe('Something failed');
|
|
57
54
|
});
|
|
@@ -67,19 +64,128 @@ describe('useSnackbar', () => {
|
|
|
67
64
|
expect(snackbarText.value).toBe('');
|
|
68
65
|
});
|
|
69
66
|
|
|
70
|
-
it('
|
|
71
|
-
const {
|
|
72
|
-
|
|
73
|
-
expect(
|
|
74
|
-
expect(
|
|
67
|
+
it('опции задают иконку и подсветку', () => {
|
|
68
|
+
const { show, snackbarIcon, snackbarHighlight } = useSnackbar();
|
|
69
|
+
show('Unfollowed', { icon: 'minus', highlight: 'Top To Bottom' });
|
|
70
|
+
expect(snackbarIcon.value).toBe('minus');
|
|
71
|
+
expect(snackbarHighlight.value).toBe('Top To Bottom');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('onAction включает кнопку с ключом undo по умолчанию и держит снекбар 6000мс', () => {
|
|
75
|
+
const { show, snackbarActionText, snackbarText } = useSnackbar();
|
|
76
|
+
show('Unfollowed', { onAction: () => {} });
|
|
77
|
+
expect(snackbarActionText.value).toBe('undo');
|
|
78
|
+
|
|
79
|
+
vi.advanceTimersByTime(5999);
|
|
80
|
+
expect(snackbarText.value).toBe('Unfollowed');
|
|
81
|
+
vi.advanceTimersByTime(1);
|
|
82
|
+
expect(snackbarText.value).toBe('');
|
|
83
|
+
expect(snackbarActionText.value).toBe('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('actionText переопределяет ключ подписи кнопки', () => {
|
|
87
|
+
const { show, snackbarActionText } = useSnackbar();
|
|
88
|
+
show('Video removed', { onAction: () => {}, actionText: 'restore' });
|
|
89
|
+
expect(snackbarActionText.value).toBe('restore');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('без onAction кнопки нет', () => {
|
|
93
|
+
const { show, snackbarActionText } = useSnackbar();
|
|
94
|
+
show('Done');
|
|
95
|
+
expect(snackbarActionText.value).toBe('');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('runSnackbarAction вызывает колбэк и закрывает снекбар', () => {
|
|
99
|
+
const { show, runSnackbarAction, snackbarText, snackbarActionText } = useSnackbar();
|
|
100
|
+
const undo = vi.fn();
|
|
101
|
+
show('Unfollowed', { onAction: undo });
|
|
102
|
+
|
|
103
|
+
runSnackbarAction();
|
|
104
|
+
expect(undo).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(snackbarText.value).toBe('');
|
|
106
|
+
expect(snackbarActionText.value).toBe('');
|
|
107
|
+
|
|
108
|
+
runSnackbarAction();
|
|
109
|
+
expect(undo).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('новый показ без onAction сбрасывает предыдущий колбэк', () => {
|
|
113
|
+
const { show, runSnackbarAction } = useSnackbar();
|
|
114
|
+
const undo = vi.fn();
|
|
115
|
+
show('Unfollowed', { onAction: undo });
|
|
116
|
+
show('Done');
|
|
117
|
+
|
|
118
|
+
runSnackbarAction();
|
|
119
|
+
expect(undo).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('pauseDismiss останавливает автозакрытие', () => {
|
|
123
|
+
const { show, pauseDismiss, snackbarText } = useSnackbar();
|
|
124
|
+
show('Done');
|
|
125
|
+
|
|
126
|
+
vi.advanceTimersByTime(1000);
|
|
127
|
+
pauseDismiss();
|
|
128
|
+
|
|
129
|
+
vi.advanceTimersByTime(60_000);
|
|
130
|
+
expect(snackbarText.value).toBe('Done');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('resumeDismiss добирает остаток, а не начинает заново', () => {
|
|
134
|
+
const { show, pauseDismiss, resumeDismiss, snackbarText } = useSnackbar();
|
|
135
|
+
show('Done');
|
|
136
|
+
|
|
137
|
+
vi.advanceTimersByTime(3000);
|
|
138
|
+
pauseDismiss();
|
|
139
|
+
vi.advanceTimersByTime(10_000);
|
|
140
|
+
resumeDismiss();
|
|
141
|
+
|
|
142
|
+
vi.advanceTimersByTime(999);
|
|
143
|
+
expect(snackbarText.value).toBe('Done');
|
|
144
|
+
vi.advanceTimersByTime(1);
|
|
145
|
+
expect(snackbarText.value).toBe('');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('повторная пауза не съедает остаток дважды', () => {
|
|
149
|
+
const { show, pauseDismiss, resumeDismiss, snackbarText } = useSnackbar();
|
|
150
|
+
show('Done');
|
|
151
|
+
|
|
152
|
+
vi.advanceTimersByTime(1000);
|
|
153
|
+
pauseDismiss();
|
|
154
|
+
vi.advanceTimersByTime(5000);
|
|
155
|
+
pauseDismiss();
|
|
156
|
+
resumeDismiss();
|
|
157
|
+
|
|
158
|
+
vi.advanceTimersByTime(2999);
|
|
159
|
+
expect(snackbarText.value).toBe('Done');
|
|
160
|
+
vi.advanceTimersByTime(1);
|
|
161
|
+
expect(snackbarText.value).toBe('');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('resumeDismiss без показанного снекбара ничего не запускает', () => {
|
|
165
|
+
const { resumeDismiss, snackbarText } = useSnackbar();
|
|
166
|
+
resumeDismiss();
|
|
167
|
+
|
|
168
|
+
vi.advanceTimersByTime(10_000);
|
|
169
|
+
expect(snackbarText.value).toBe('');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('новый show перебивает паузу предыдущего снекбара', () => {
|
|
173
|
+
const { show, pauseDismiss, snackbarText } = useSnackbar();
|
|
174
|
+
show('First');
|
|
175
|
+
pauseDismiss();
|
|
176
|
+
show('Second');
|
|
177
|
+
|
|
178
|
+
vi.advanceTimersByTime(4000);
|
|
179
|
+
expect(snackbarText.value).toBe('');
|
|
75
180
|
});
|
|
76
181
|
|
|
77
182
|
it('resetSnackbar сбрасывает всё', () => {
|
|
78
|
-
const {
|
|
79
|
-
|
|
183
|
+
const { show, resetSnackbar, snackbarText, snackbarIcon, snackbarHighlight, snackbarActionText } = useSnackbar();
|
|
184
|
+
show('Unfollowed', { icon: 'minus', highlight: 'Top To Bottom', onAction: () => {} });
|
|
80
185
|
resetSnackbar();
|
|
81
186
|
expect(snackbarText.value).toBe('');
|
|
82
|
-
expect(snackbarTheme.value).toBe('success');
|
|
83
187
|
expect(snackbarIcon.value).toBe('check');
|
|
188
|
+
expect(snackbarHighlight.value).toBe('');
|
|
189
|
+
expect(snackbarActionText.value).toBe('');
|
|
84
190
|
});
|
|
85
191
|
});
|
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import { parseApiError } from '../runtime';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const DISMISS_TIME = 4000;
|
|
4
|
+
const ACTION_DISMISS_TIME = 6000;
|
|
5
5
|
|
|
6
6
|
// Один таймер на логическое уведомление. Снекбар физически переезжает между
|
|
7
|
-
// диалогом (
|
|
7
|
+
// диалогом (ui-popup) и общим макетом (default.vue), поэтому таймер не может жить
|
|
8
8
|
// в компоненте — при перемонтировании он сбрасывался бы заново. Идентификатор
|
|
9
9
|
// setTimeout создаётся только в браузере, на сервере он не заводится.
|
|
10
10
|
let dismissTimer: null | ReturnType<typeof setTimeout> = null;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Остаток автозакрытия и момент его запуска — чтобы навести на снекбар, прочитать
|
|
13
|
+
// сообщение и дожать undo: на ховере/фокусе таймер встаёт на паузу, после уходя
|
|
14
|
+
// курсора он добирает недоигранное время, а не начинает отсчёт заново.
|
|
15
|
+
let dismissRemaining = 0;
|
|
16
|
+
let dismissStartedAt = 0;
|
|
17
|
+
|
|
18
|
+
// Колбэк кнопки лежит рядом с таймером, а не в useState: функция не сериализуется
|
|
19
|
+
// в payload, да и показывается снекбар всегда с клиента.
|
|
20
|
+
let actionHandler: null | (() => unknown) = null;
|
|
21
|
+
|
|
22
|
+
export interface ISnackbarOptions {
|
|
23
|
+
/** Имя иконки в кружке слева. */
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** Часть сообщения, выводимая жирным — имя канала, плейлиста, видео. */
|
|
26
|
+
highlight?: string;
|
|
27
|
+
/** i18n-ключ внутри `snackbar.*` для подписи кнопки, по умолчанию `undo`. */
|
|
28
|
+
actionText?: string;
|
|
29
|
+
/** Действие кнопки — как правило откат того, о чём сообщает снекбар. */
|
|
30
|
+
onAction?: () => unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Управляет состоянием snackbar-уведомления: показ сообщения, кнопка отмены действия, сброс, иконка и таймер автозакрытия. */
|
|
13
34
|
export const useSnackbar = () => {
|
|
14
35
|
const snackbarIcon = useState('snackbar-icon', () => 'check');
|
|
15
36
|
const snackbarText = useState('snackbar-text', () => '');
|
|
16
|
-
const
|
|
37
|
+
const snackbarHighlight = useState('snackbar-highlight', () => '');
|
|
38
|
+
const snackbarActionText = useState('snackbar-action-text', () => '');
|
|
17
39
|
const isSnackBarInPopup = useState('snackbar-in-popup', () => false);
|
|
18
|
-
const snackbarTheme = useState<'success' | 'error' | 'default'>('snackbar-theme', () => 'success');
|
|
19
40
|
|
|
20
41
|
function clearTimer() {
|
|
21
42
|
if (dismissTimer) {
|
|
@@ -26,46 +47,80 @@ export const useSnackbar = () => {
|
|
|
26
47
|
|
|
27
48
|
function scheduleDismiss(ms: number) {
|
|
28
49
|
clearTimer();
|
|
50
|
+
dismissRemaining = ms;
|
|
29
51
|
if (import.meta.client) {
|
|
52
|
+
dismissStartedAt = Date.now();
|
|
30
53
|
dismissTimer = setTimeout(resetSnackbar, ms);
|
|
31
54
|
}
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
/** Пауза автозакрытия, пока на снекбаре курсор или фокус. */
|
|
58
|
+
function pauseDismiss() {
|
|
59
|
+
if (!dismissTimer) return;
|
|
60
|
+
|
|
61
|
+
dismissRemaining -= Date.now() - dismissStartedAt;
|
|
62
|
+
clearTimer();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Возврат к отсчёту с того места, где он остановился. */
|
|
66
|
+
function resumeDismiss() {
|
|
67
|
+
if (dismissTimer || !snackbarText.value) return;
|
|
68
|
+
|
|
69
|
+
if (dismissRemaining <= 0) {
|
|
70
|
+
resetSnackbar();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
scheduleDismiss(dismissRemaining);
|
|
39
75
|
}
|
|
40
76
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
snackbarIcon.value = '
|
|
77
|
+
function show(message: string, options: ISnackbarOptions = {}) {
|
|
78
|
+
actionHandler = options.onAction ?? null;
|
|
79
|
+
snackbarIcon.value = options.icon ?? 'check';
|
|
44
80
|
snackbarText.value = message;
|
|
45
|
-
|
|
81
|
+
snackbarHighlight.value = options.highlight ?? '';
|
|
82
|
+
snackbarActionText.value = actionHandler ? options.actionText ?? 'undo' : '';
|
|
83
|
+
scheduleDismiss(actionHandler ? ACTION_DISMISS_TIME : DISMISS_TIME);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Ошибка отличается от обычного сообщения только иконкой — вид у снекбара один. */
|
|
87
|
+
function showError(message: string, options: ISnackbarOptions = {}) {
|
|
88
|
+
show(message, { icon: 'close', ...options });
|
|
46
89
|
}
|
|
47
90
|
|
|
48
91
|
function setErrorState(error: any) {
|
|
49
92
|
showError(parseApiError(error));
|
|
50
93
|
}
|
|
51
94
|
|
|
95
|
+
/** Запускает действие кнопки и закрывает снекбар, чтобы отменить нельзя было дважды. */
|
|
96
|
+
function runSnackbarAction() {
|
|
97
|
+
const handler = actionHandler;
|
|
98
|
+
resetSnackbar();
|
|
99
|
+
handler?.();
|
|
100
|
+
}
|
|
101
|
+
|
|
52
102
|
function resetSnackbar() {
|
|
53
103
|
clearTimer();
|
|
104
|
+
dismissRemaining = 0;
|
|
105
|
+
actionHandler = null;
|
|
54
106
|
snackbarIcon.value = 'check';
|
|
55
107
|
snackbarText.value = '';
|
|
56
|
-
|
|
108
|
+
snackbarHighlight.value = '';
|
|
109
|
+
snackbarActionText.value = '';
|
|
57
110
|
}
|
|
58
111
|
|
|
59
112
|
return {
|
|
60
113
|
snackbarIcon,
|
|
61
114
|
snackbarText,
|
|
62
|
-
|
|
63
|
-
|
|
115
|
+
snackbarHighlight,
|
|
116
|
+
snackbarActionText,
|
|
64
117
|
isSnackBarInPopup,
|
|
65
|
-
|
|
118
|
+
show,
|
|
66
119
|
showError,
|
|
67
120
|
setErrorState,
|
|
121
|
+
runSnackbarAction,
|
|
68
122
|
resetSnackbar,
|
|
69
|
-
|
|
123
|
+
pauseDismiss,
|
|
124
|
+
resumeDismiss,
|
|
70
125
|
};
|
|
71
126
|
};
|
package/composables/use-user.ts
CHANGED
|
@@ -23,7 +23,7 @@ function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K
|
|
|
23
23
|
* @param apiService - сервис для API-запросов
|
|
24
24
|
*/
|
|
25
25
|
export const useUser = (apiService) => {
|
|
26
|
-
const {
|
|
26
|
+
const { show, showError } = useSnackbar();
|
|
27
27
|
const register = async (form: IRegistrateForm) => {
|
|
28
28
|
const response = await apiService.register(form);
|
|
29
29
|
if (response) {
|
|
@@ -72,7 +72,7 @@ export const useUser = (apiService) => {
|
|
|
72
72
|
const changePassword = async (form: IChangePasswordForm) => {
|
|
73
73
|
try {
|
|
74
74
|
await apiService.changePassword(form);
|
|
75
|
-
|
|
75
|
+
show('Password changed');
|
|
76
76
|
} catch (err) {
|
|
77
77
|
showError('Error while changing password');
|
|
78
78
|
console.log(err, 'error change password');
|
|
@@ -82,7 +82,7 @@ export const useUser = (apiService) => {
|
|
|
82
82
|
const changeEmail = async (form: IChangeEmail) => {
|
|
83
83
|
try {
|
|
84
84
|
await apiService.changeEmail(form);
|
|
85
|
-
|
|
85
|
+
show('email_changed');
|
|
86
86
|
} catch (err) {
|
|
87
87
|
showError('Error while changing email');
|
|
88
88
|
console.log(err, 'error change email');
|
|
@@ -93,7 +93,7 @@ export const useUser = (apiService) => {
|
|
|
93
93
|
try {
|
|
94
94
|
const { generateLink } = useGenerateLink();
|
|
95
95
|
await apiService.recoveryPassword(form);
|
|
96
|
-
|
|
96
|
+
show('Password recovered');
|
|
97
97
|
await useRouter().push(generateLink('/profile'));
|
|
98
98
|
} catch (err) {
|
|
99
99
|
showError('Error while recovery password');
|
package/package.json
CHANGED
package/pages/contacts.vue
CHANGED
|
@@ -175,7 +175,7 @@ const requiredFields = computed(() => getSubjectItems.value?.filter(item => item
|
|
|
175
175
|
|
|
176
176
|
const loading = ref(false);
|
|
177
177
|
|
|
178
|
-
const {
|
|
178
|
+
const { show, showError } = useSnackbar();
|
|
179
179
|
|
|
180
180
|
// Загружаем reCAPTCHA при открытии формы (onMounted) — даёт Google время собрать данные о поведении
|
|
181
181
|
onMounted(async () => {
|
|
@@ -247,7 +247,7 @@ async function submit() {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
await ContactsApiService.email(form.value);
|
|
250
|
-
|
|
250
|
+
show('email_send');
|
|
251
251
|
resetForm();
|
|
252
252
|
} catch (error) {
|
|
253
253
|
console.log('error send email', error);
|
package/pages/library/index.vue
CHANGED
|
@@ -323,7 +323,7 @@ const breadcrumbs = [
|
|
|
323
323
|
margin: 0;
|
|
324
324
|
padding: 0;
|
|
325
325
|
list-style: none;
|
|
326
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
326
|
+
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
|
327
327
|
|
|
328
328
|
@include from-br(sm) {
|
|
329
329
|
gap: 24px;
|
|
@@ -8,6 +8,31 @@
|
|
|
8
8
|
class="_breadcrumbs"
|
|
9
9
|
/>
|
|
10
10
|
|
|
11
|
+
<header class="playlists__header">
|
|
12
|
+
<UiSectionTitle
|
|
13
|
+
v-if="h1"
|
|
14
|
+
:title="h1"
|
|
15
|
+
:count="publicData.items.length"
|
|
16
|
+
icon="list"
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
<UiLink
|
|
20
|
+
:to="generateLink('/library')"
|
|
21
|
+
theme="secondary"
|
|
22
|
+
class="playlists__my-link"
|
|
23
|
+
>
|
|
24
|
+
<UiIcon
|
|
25
|
+
name="playlist"
|
|
26
|
+
size="16"
|
|
27
|
+
/>
|
|
28
|
+
{{ $t('my_playlists_count', { count: myPlaylistsCount }) }}
|
|
29
|
+
<UiIcon
|
|
30
|
+
name="arrow-right"
|
|
31
|
+
size="16"
|
|
32
|
+
/>
|
|
33
|
+
</UiLink>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
11
36
|
<ClientOnly>
|
|
12
37
|
<template v-if="isAuthorized && filteredUserItems && filteredUserItems.length > 0">
|
|
13
38
|
<UiSectionTitle
|
|
@@ -15,6 +40,7 @@
|
|
|
15
40
|
:count="filteredUserItems.length"
|
|
16
41
|
class="playlists__title"
|
|
17
42
|
title-tag="h2"
|
|
43
|
+
size="xl"
|
|
18
44
|
icon="clapperboard-play"
|
|
19
45
|
/>
|
|
20
46
|
|
|
@@ -27,12 +53,13 @@
|
|
|
27
53
|
</template>
|
|
28
54
|
</ClientOnly>
|
|
29
55
|
|
|
56
|
+
<SectionCollections :data-items="publicData.items"/>
|
|
57
|
+
|
|
30
58
|
<UiSectionTitle
|
|
31
|
-
|
|
32
|
-
:title="h1"
|
|
33
|
-
:count="publicData.items.length"
|
|
59
|
+
:title="$t('user_playlists')"
|
|
34
60
|
class="playlists__title"
|
|
35
|
-
|
|
61
|
+
title-tag="h2"
|
|
62
|
+
size="xl"
|
|
36
63
|
/>
|
|
37
64
|
|
|
38
65
|
<SectionNavLinks
|
|
@@ -119,6 +146,11 @@ watch(
|
|
|
119
146
|
|
|
120
147
|
const { t } = useI18n();
|
|
121
148
|
|
|
149
|
+
const { generateLink } = useGenerateLink();
|
|
150
|
+
|
|
151
|
+
/** До загрузки своих плейлистов (и у гостя) в скобках 0 */
|
|
152
|
+
const myPlaylistsCount = computed(() => filteredUserItems.value?.length ?? 0);
|
|
153
|
+
|
|
122
154
|
const pagePureName = useGetPureRouteName();
|
|
123
155
|
const { meta, h1 } = useMeta({
|
|
124
156
|
t,
|
|
@@ -143,6 +175,25 @@ const breadcrumbs = [
|
|
|
143
175
|
</script>
|
|
144
176
|
|
|
145
177
|
<style lang="scss">
|
|
178
|
+
.playlists__header {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
align-items: flex-start;
|
|
182
|
+
row-gap: 12px;
|
|
183
|
+
margin-bottom: 24px;
|
|
184
|
+
|
|
185
|
+
@include from-br(xm) {
|
|
186
|
+
flex-direction: row;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: space-between;
|
|
189
|
+
column-gap: 16px;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.playlists__my-link {
|
|
194
|
+
flex-shrink: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
146
197
|
.playlists__title {
|
|
147
198
|
margin-bottom: 24px;
|
|
148
199
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v8m-4-4h8"/></svg>
|