itube-specs 0.0.812 → 0.0.815
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/README.md +22 -11
- package/package.json +2 -1
- package/pages/2257.vue +71 -0
- package/pages/categories/[slug].vue +246 -0
- package/pages/categories/all.vue +100 -0
- package/pages/categories/index.vue +180 -0
- package/pages/categories/letter/[slug].vue +92 -0
- package/pages/categories.vue +44 -0
- package/pages/channels/[channel].vue +216 -0
- package/pages/channels/index.vue +148 -0
- package/pages/channels.vue +6 -0
- package/pages/confirm/registration/[token].vue +88 -0
- package/pages/contacts.vue +368 -0
- package/pages/dmca.vue +129 -0
- package/pages/embed/[spot].vue +23 -0
- package/pages/favorites.vue +30 -0
- package/pages/index.vue +91 -0
- package/pages/models/[slug]/channel/[channel].vue +98 -0
- package/pages/models/[slug]/index.vue +100 -0
- package/pages/models/[slug]/tag/[tag].vue +95 -0
- package/pages/models/index.vue +87 -0
- package/pages/models/letter/[slug].vue +61 -0
- package/pages/models.vue +6 -0
- package/pages/playlists/[playlistId]/[slug].vue +136 -0
- package/pages/playlists/index.vue +154 -0
- package/pages/playlists.vue +6 -0
- package/pages/policy.vue +190 -0
- package/pages/search/[slug]/index.vue +266 -0
- package/pages/search/[slug]/models.vue +63 -0
- package/pages/search/[slug]/videos.vue +65 -0
- package/pages/tag/[tag].vue +80 -0
- package/pages/terms.vue +473 -0
- package/pages/user/profile.vue +252 -0
- package/pages/user/recovery-password.vue +82 -0
- package/pages/user/settings.vue +36 -0
- package/pages/user.vue +36 -0
- package/pages/videos/[slug].vue +161 -0
- package/services/api/authorization.service.ts +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# itube-specs
|
|
2
2
|
|
|
3
3
|
Общий **Nuxt Layer** для сайтов itube. Держит единую базу: компоненты, composables, сервисы, утилиты,
|
|
4
|
-
runtime-хелперы,
|
|
5
|
-
|
|
4
|
+
runtime-хелперы, типы, lib-данные, а также middleware, SSR-плагины и BFF-хендлеры (`server/api`).
|
|
5
|
+
Приложения расширяют слой (`extends: ['itube-specs']`) и форкаются под разные сайты одной ниши —
|
|
6
|
+
поэтому слой должен оставаться **самодостаточным и site-agnostic**.
|
|
6
7
|
|
|
7
8
|
> Bitbucket: https://bitbucket.org/luckytube/specs/src/master/
|
|
8
9
|
|
|
@@ -76,9 +77,15 @@ runtime/ # runtime-значения и хелперы (barrel index.ts)
|
|
|
76
77
|
constants/ # доменные `as const` (Niche, PlaylistType, AdSpotType, …)
|
|
77
78
|
utils/ # чистые хелперы, cleaners/, converters/
|
|
78
79
|
types/ # .d.ts типы (barrel index.d.ts — с расширением .d.ts в реэкспортах)
|
|
79
|
-
lib/ # статические scheme/config-данные (barrel index.ts)
|
|
80
|
+
lib/ # статические scheme/config-данные (barrel index.ts): scheme/*, css-breakpoints
|
|
81
|
+
middleware/ # глобальные: auth.global, normalize.global, age.global
|
|
82
|
+
plugins/ # SSR/клиент: country/countries/mobile (SSR), services.client, sentry.client, adv
|
|
83
|
+
server/ # BFF: api/** (тонкие прокси к внешнему API), utils/metrics, plugins/*, tasks
|
|
80
84
|
```
|
|
81
85
|
|
|
86
|
+
> `middleware`/`plugins`/`server` обязаны быть в `files` (whitelist) — иначе не попадут в паблиш
|
|
87
|
+
> (см. раздел `files`/`exports`).
|
|
88
|
+
|
|
82
89
|
---
|
|
83
90
|
|
|
84
91
|
## Импорты
|
|
@@ -87,8 +94,9 @@ lib/ # статические scheme/config-данные (barrel index
|
|
|
87
94
|
через имя пакета. Приложения импортируют как `itube-specs/runtime`, `itube-specs/services/...`,
|
|
88
95
|
`itube-specs/utils/...`, а типы — `itube-specs/types`.
|
|
89
96
|
|
|
90
|
-
Алиасы `~/…`/`@/…` в файле слоя резолвятся на **приложение**, не на слой —
|
|
91
|
-
|
|
97
|
+
Алиасы `~/…`/`@/…` в файле слоя резолвятся на **приложение**, не на слой — в коде слоя их не используем.
|
|
98
|
+
Сейчас таких app-зависимостей в слое не осталось: то, что раньше приходило из приложения (фича-флаги,
|
|
99
|
+
ad-конфиг), теперь читается из `runtimeConfig.public` (значения задаёт приложение).
|
|
92
100
|
|
|
93
101
|
### `files` / `exports`
|
|
94
102
|
|
|
@@ -105,10 +113,12 @@ lib/ # статические scheme/config-данные (barrel index
|
|
|
105
113
|
|
|
106
114
|
## Что живёт в слое, а что в приложении
|
|
107
115
|
|
|
108
|
-
- **Слой:**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
- **Слой:** компоненты, composables, API-сервисы, доменные утилиты, runtime-константы/хелперы, типы,
|
|
117
|
+
`lib/` (scheme, css-breakpoints), middleware (`auth`/`normalize`/`age`), SSR-плагины
|
|
118
|
+
(`country`/`mobile`/`services`/`sentry`/`adv`), BFF-хендлеры `server/api/**` — всё site-agnostic.
|
|
119
|
+
- **Приложение:** только per-site — **значения** config (флаги/реклама/локали) через `runtimeConfig.public`
|
|
120
|
+
(слой держит пустые контейнеры-заглушки), `assets/scss` (токены), страницы, i18n-локали и остаток
|
|
121
|
+
`server/`, завязанный на локали (`locale-guard`, `site/languages`, `is-locale-blocked`).
|
|
112
122
|
|
|
113
123
|
---
|
|
114
124
|
|
|
@@ -130,5 +140,6 @@ lib/ # статические scheme/config-данные (barrel index
|
|
|
130
140
|
## Тесты
|
|
131
141
|
|
|
132
142
|
Vitest через `@nuxt/test-utils`. В слое — **только тесты без app-специфичного окружения** (чистая логика
|
|
133
|
-
composables/utils/runtime). Компонентные тесты с монтированием SFC
|
|
134
|
-
|
|
143
|
+
composables/utils/runtime). Компонентные тесты с монтированием SFC (нужна SCSS-инъекция токенов, которой в слое
|
|
144
|
+
нет) живут в **приложении**. Значения `runtimeConfig` (`featureFlags`/`adsConfig`) в слое — пустые контейнеры,
|
|
145
|
+
поэтому его код при бутстрапе в CI не падает, но реальные значения приходят только из приложения.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "itube-specs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.815",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"middleware/",
|
|
56
56
|
"plugins/",
|
|
57
57
|
"server/",
|
|
58
|
+
"pages/",
|
|
58
59
|
"nuxt.config.ts"
|
|
59
60
|
],
|
|
60
61
|
"devDependencies": {
|
package/pages/2257.vue
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="document__container _container">
|
|
3
|
+
<div class="document">
|
|
4
|
+
<section>
|
|
5
|
+
<h1>{{ h1 }}</h1>
|
|
6
|
+
<p>
|
|
7
|
+
For the purposes of 18 U.S. Code § 2257, the operator of the {{ brandName }} website is not a “Producer,”
|
|
8
|
+
either primary or secondary, of any of the content found on {{ brandName }}. The operator’s activities,
|
|
9
|
+
with respect to such content, are limited to the transmission, storage, retrieval, hosting and/or formatting of
|
|
10
|
+
depictions posted by third party users, on areas of the website under the user's control.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p>
|
|
14
|
+
For § 2257 records for any content found on the {{ brandName }} website, please direct your request to the
|
|
15
|
+
respective uploader, whether a website, a user, a studio or a producer which provided the content (“Uploader”).
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
{{ brandName }} abides by the following procedures to ensure compliance:
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<ul>
|
|
23
|
+
<li>
|
|
24
|
+
All Uploaders must be over eighteen (18) years of age to upload,
|
|
25
|
+
share and sell content on {{ brandName }};
|
|
26
|
+
</li>
|
|
27
|
+
|
|
28
|
+
<li>
|
|
29
|
+
When uploading, Uploader must verify the content; assure
|
|
30
|
+
he/she/they are 18 years of age; certify that he/she/they keep records of the models in the content and that
|
|
31
|
+
they are over 18 years of age;
|
|
32
|
+
</li>
|
|
33
|
+
|
|
34
|
+
<li>
|
|
35
|
+
the uploaded content does not violate {{ brandName }}’s
|
|
36
|
+
<a href="/terms">Terms of Service
|
|
37
|
+
</a>
|
|
38
|
+
, and any of its related policies.
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
|
|
42
|
+
<p>
|
|
43
|
+
For further assistance and/or information in finding the content’s Uploader, please contact {{ brandName }}’s
|
|
44
|
+
compliance department at
|
|
45
|
+
<a
|
|
46
|
+
:href="`mailto:${emails.support}`"
|
|
47
|
+
class="email"
|
|
48
|
+
>{{ emails.support }}</a>
|
|
49
|
+
.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<p>
|
|
53
|
+
{{ brandName }} allows content to be reported as inappropriate. Should any content be reported as illegal,
|
|
54
|
+
unlawful,
|
|
55
|
+
harassing, harmful, offensive or various other reasons, {{ brandName }} shall remove it without delay.
|
|
56
|
+
</p>
|
|
57
|
+
</section>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
const { t } = useI18n();
|
|
64
|
+
const pagePureName = useGetPureRouteName();
|
|
65
|
+
const { brandName, emails } = useAppConfig();
|
|
66
|
+
const { meta, h1 } = useMeta(t, pagePureName, brandName);
|
|
67
|
+
|
|
68
|
+
watchEffect(() => {
|
|
69
|
+
useHead(meta.value);
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="category _page _container">
|
|
3
|
+
<UiBreadcrumbs
|
|
4
|
+
:items="breadcrumbs"
|
|
5
|
+
class="_breadcrumbs-info"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<InfoMain
|
|
9
|
+
v-if="categoryInfoResult && isFirstPage"
|
|
10
|
+
:key="`info-${categoryInfo?.header}`"
|
|
11
|
+
:card="categoryInfoResult"
|
|
12
|
+
type="category"
|
|
13
|
+
class="category__info"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<UiExpandRow
|
|
17
|
+
v-if="relatedCategories && relatedCategories.length && relatedCategories.length > 0"
|
|
18
|
+
:items="relatedCategories"
|
|
19
|
+
:pre-text="`${$t('related_categories')}:`"
|
|
20
|
+
class="category__related-categories"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<LazySectionToolbar
|
|
24
|
+
class="category__options"
|
|
25
|
+
hydrate-on-idle
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<GridVideos
|
|
29
|
+
v-if="data && data.items && data.items?.length > 0"
|
|
30
|
+
id="anchor"
|
|
31
|
+
:items="data.items"
|
|
32
|
+
priority
|
|
33
|
+
:class="{'_loading': status === 'pending'}"
|
|
34
|
+
>
|
|
35
|
+
<template #grid-end>
|
|
36
|
+
<LazyAdvNativeBottom
|
|
37
|
+
v-if="featureFlags.AdsNativeEnabled"
|
|
38
|
+
hydrate-on-visible
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
41
|
+
<template #default>
|
|
42
|
+
<UiExpandRow
|
|
43
|
+
v-if="isChipsShow && relatedSearch && relatedSearch.length && relatedSearch.length > 0 && statusRelated !== 'pending'"
|
|
44
|
+
:items="relatedSearch"
|
|
45
|
+
:pre-text="`${$t('related_searches')}: `"
|
|
46
|
+
class="category__related-searches"
|
|
47
|
+
/>
|
|
48
|
+
<div
|
|
49
|
+
v-else-if="statusRelated === 'pending'"
|
|
50
|
+
class="category__related-loading _loading"
|
|
51
|
+
/>
|
|
52
|
+
<LazySectionSwiperCards
|
|
53
|
+
hydrate-on-visible
|
|
54
|
+
v-if="modelsByCategory && modelsByCategory.items && modelsByCategory.items.length > 0"
|
|
55
|
+
:items="modelsRoundCards"
|
|
56
|
+
:title="$t('category_top_models_title', {name: title})"
|
|
57
|
+
class="category__models"
|
|
58
|
+
placeholder-avatar="/img/model-placeholder.webp"
|
|
59
|
+
/>
|
|
60
|
+
</template>
|
|
61
|
+
</GridVideos>
|
|
62
|
+
<div
|
|
63
|
+
v-else-if="status === 'pending'"
|
|
64
|
+
class="category__loading _loading"
|
|
65
|
+
/>
|
|
66
|
+
<UiEmptyState v-else/>
|
|
67
|
+
|
|
68
|
+
<UiPagination
|
|
69
|
+
v-if="data"
|
|
70
|
+
arrow-icon
|
|
71
|
+
:key="`pagination-${data.allPages}`"
|
|
72
|
+
:total="data.allPages"
|
|
73
|
+
class="category__pagination _pagination"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<LazyUiFullDescr
|
|
77
|
+
v-if="isFirstPage && categoryInfo?.fullDescription"
|
|
78
|
+
:text="categoryInfo.fullDescription"
|
|
79
|
+
hydrate-on-visible
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { CategoriesApiService } from '../../services/api/categories.service';
|
|
86
|
+
import { VideosApiService } from '../../services/api/videos.service';
|
|
87
|
+
import { ModelsApiService } from '../../services/api/models.service';
|
|
88
|
+
import { convertModelToRoundCard } from '../../runtime';
|
|
89
|
+
import type { ICardInfo } from '../../types';
|
|
90
|
+
import { computed, unref } from 'vue';
|
|
91
|
+
|
|
92
|
+
const featureFlags = useRuntimeConfig().public.featureFlags;
|
|
93
|
+
|
|
94
|
+
const { t } = useI18n();
|
|
95
|
+
|
|
96
|
+
const route = useRoute();
|
|
97
|
+
const disallowedSlugs = ['18', '19'];
|
|
98
|
+
|
|
99
|
+
const slug = useSlug();
|
|
100
|
+
|
|
101
|
+
const [
|
|
102
|
+
{ data: categoryInfo },
|
|
103
|
+
{ data, status },
|
|
104
|
+
{ data: relatedData, status: statusRelated },
|
|
105
|
+
{ data: modelsByCategory },
|
|
106
|
+
] = await Promise.all([
|
|
107
|
+
useFetchCategoriesByName(CategoriesApiService.getCategoriesByName),
|
|
108
|
+
useFetchVideosByCategories(VideosApiService.getListByNicheAndCategory),
|
|
109
|
+
useFetchCategoriesRelated(CategoriesApiService.getRelatedCategories),
|
|
110
|
+
useFetchModelsByCategory(ModelsApiService.getListByCategory, 100),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const isChipsShow = computed(() => !disallowedSlugs.some((item) => slug.value.includes(item)));
|
|
114
|
+
|
|
115
|
+
const relatedSearchRaw = computed(() => {
|
|
116
|
+
if (!isChipsShow.value) return null;
|
|
117
|
+
return relatedData.value || null;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const modelsRoundCards = computed(() =>
|
|
121
|
+
modelsByCategory.value?.items.map(convertModelToRoundCard) || []
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const categoryInfoResult = computed(() => ({
|
|
125
|
+
title: categoryInfo.value?.header,
|
|
126
|
+
description: categoryInfo.value?.description,
|
|
127
|
+
videosCount: categoryInfo.value?.videosCount,
|
|
128
|
+
guid: categoryInfo.value?.guid,
|
|
129
|
+
miniCardTitle: categoryInfo.value?.title,
|
|
130
|
+
} as ICardInfo));
|
|
131
|
+
|
|
132
|
+
const relatedSearch = computed(() => relatedSearchRaw.value?.items?.map(item => ({
|
|
133
|
+
title: item.query,
|
|
134
|
+
value: item.query,
|
|
135
|
+
prefix: 'search',
|
|
136
|
+
})) || []);
|
|
137
|
+
|
|
138
|
+
const relatedCategories = computed(() => categoryInfo.value?.related);
|
|
139
|
+
|
|
140
|
+
const title = computed(() => categoryInfo.value?.title || '');
|
|
141
|
+
const metaTitle = computed(() => categoryInfo.value?.metaTitle || '');
|
|
142
|
+
const brandName = computed(() => useAppConfig().brandName);
|
|
143
|
+
const pageNumber = computed(() => route.query?.[ 'page' ] ? Number(route.query[ 'page' ]) : 1);
|
|
144
|
+
const pageText = computed(() => unref(pageNumber) === 1 ? '' : ` ${t('page')} ${unref(pageNumber)}`);
|
|
145
|
+
|
|
146
|
+
watchEffect(() => {
|
|
147
|
+
const categoryTitle = `${metaTitle.value}${pageText.value} | ${brandName.value}`;
|
|
148
|
+
const categoryDescription = `${categoryInfo.value?.metaDescription || ''}${pageText.value}`;
|
|
149
|
+
|
|
150
|
+
useHead({
|
|
151
|
+
title: categoryTitle,
|
|
152
|
+
meta: [
|
|
153
|
+
{ name: 'description', content: categoryDescription },
|
|
154
|
+
{ property: 'og:title', content: categoryTitle },
|
|
155
|
+
{ property: 'og:description', content: categoryDescription },
|
|
156
|
+
{ name: 'twitter:title', content: categoryTitle },
|
|
157
|
+
{ name: 'twitter:description', content: categoryDescription },
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
status.value === 'success' &&
|
|
163
|
+
(!data.value || !categoryInfo.value)
|
|
164
|
+
) {
|
|
165
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const isFirstPage = computed(() => !route.query[ 'page' ] || route.query[ 'page' ] === '1');
|
|
170
|
+
|
|
171
|
+
const breadcrumbs = [
|
|
172
|
+
{
|
|
173
|
+
name: `${t('navigation.main')}`,
|
|
174
|
+
to: '/',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: `${t('navigation.categories')}`,
|
|
178
|
+
to: '/categories',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: title.value,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style lang="scss">
|
|
187
|
+
.category__options {
|
|
188
|
+
margin-bottom: 24px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.category__options:has(.ui-expand-row) {
|
|
192
|
+
margin-bottom: 16px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.category__loading {
|
|
196
|
+
height: 100%;
|
|
197
|
+
min-height: 30vh;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.category__models {
|
|
201
|
+
margin-top: 40px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.category__info {
|
|
205
|
+
margin-bottom: 24px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.category__related-loading {
|
|
209
|
+
--size: 20px;
|
|
210
|
+
|
|
211
|
+
height: 24px;
|
|
212
|
+
|
|
213
|
+
@include from-br(sm) {
|
|
214
|
+
height: 32px;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.category__models,
|
|
219
|
+
.category__related-searches {
|
|
220
|
+
grid-column: 1/-1;
|
|
221
|
+
grid-row: 2;
|
|
222
|
+
margin-block: 8px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.category__related-searches + .category__models {
|
|
226
|
+
grid-row: 3;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.category__top {
|
|
230
|
+
display: grid;
|
|
231
|
+
row-gap: 8px;
|
|
232
|
+
margin-top: auto;
|
|
233
|
+
|
|
234
|
+
@include from-br(sm) {
|
|
235
|
+
row-gap: 16px;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.category__related-categories {
|
|
240
|
+
margin-bottom: 24px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.category__pagination {
|
|
244
|
+
margin-top: 32px;
|
|
245
|
+
}
|
|
246
|
+
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="all _page _container">
|
|
3
|
+
<UiBreadcrumbs
|
|
4
|
+
:items="breadcrumbs"
|
|
5
|
+
class="all__breadcrumbs _breadcrumbs"
|
|
6
|
+
/>
|
|
7
|
+
<UiSectionTitle
|
|
8
|
+
:title="h1"
|
|
9
|
+
:count="totalCount"
|
|
10
|
+
class="_title"
|
|
11
|
+
icon="grid"
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<div class="_nav-filter">
|
|
15
|
+
<CategoryNav
|
|
16
|
+
v-if="categoriesGroupByLetterChips"
|
|
17
|
+
:items="categoriesGroupByLetterChips as IChipsItem[]"
|
|
18
|
+
/>
|
|
19
|
+
<div
|
|
20
|
+
v-else
|
|
21
|
+
class="categories__loading _loading"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<LazyCategoryByLetter
|
|
26
|
+
v-if="categoriesGroupByLetter"
|
|
27
|
+
:categories="categoriesGroupByLetter as Record<string,ICategoryCard[]>"
|
|
28
|
+
hydrate-on-visible
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
<LazySectionSeoText
|
|
32
|
+
:text="t('all_footer_text')"
|
|
33
|
+
links-link="all_footer_links"
|
|
34
|
+
hydrate-on-visible
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
<section
|
|
38
|
+
v-if="dataModels"
|
|
39
|
+
class="all__popular-models"
|
|
40
|
+
>
|
|
41
|
+
<LazySectionModels
|
|
42
|
+
:data-items="dataModels.items"
|
|
43
|
+
:title="t('categories_popular_models')"
|
|
44
|
+
hydrate-on-visible
|
|
45
|
+
/>
|
|
46
|
+
</section>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import type { IBreadcrumbItem, ICategoryCard, IChipsItem } from '../../types';
|
|
52
|
+
import { ModelsApiService } from '../../services/api/models.service';
|
|
53
|
+
import { CategoriesApiService } from '../../services/api/categories.service';
|
|
54
|
+
import { AsyncData } from '../../runtime';
|
|
55
|
+
|
|
56
|
+
const pageName = useRoute().name;
|
|
57
|
+
|
|
58
|
+
const { t } = useI18n();
|
|
59
|
+
|
|
60
|
+
const [
|
|
61
|
+
{ data: dataModels },
|
|
62
|
+
{ data: categoriesGroupByLetterChips },
|
|
63
|
+
{ data: categoriesGroupByLetter },
|
|
64
|
+
] = await Promise.all([
|
|
65
|
+
useFetchTopModels(ModelsApiService.getTop, 25, AsyncData.TopModels, String(pageName)),
|
|
66
|
+
useFetchCategoriesGroupByLetter(AsyncData.GroupsCategoriesByLetterChips, CategoriesApiService.getListGroupedByLetterChips),
|
|
67
|
+
useFetchCategoriesGroupByLetter(AsyncData.GroupsCategoriesByLetter, CategoriesApiService.getListGroupedByLetter),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const pagePureName = useGetPureRouteName();
|
|
71
|
+
const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName);
|
|
72
|
+
|
|
73
|
+
watchEffect(() => {
|
|
74
|
+
useHead(meta.value);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const totalCount = computed(() =>
|
|
78
|
+
Object.values(categoriesGroupByLetter.value || {}).reduce((sum, arr) => sum + arr.length, 0)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const breadcrumbs = [
|
|
82
|
+
{
|
|
83
|
+
name: `${t('navigation.main')}`,
|
|
84
|
+
to: '/',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: `${t('navigation.categories')}`,
|
|
88
|
+
to: '/categories',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: `${t('all_categories')}`,
|
|
92
|
+
},
|
|
93
|
+
] as IBreadcrumbItem[];
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<style lang="scss">
|
|
97
|
+
.all__popular-models {
|
|
98
|
+
margin-top: 32px;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="grouped _page _container">
|
|
3
|
+
<UiBreadcrumbs
|
|
4
|
+
:items="breadcrumbs"
|
|
5
|
+
class="_breadcrumbs"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<UiSectionTitle
|
|
9
|
+
:title="h1"
|
|
10
|
+
:count="totalCount"
|
|
11
|
+
class="all__title _title"
|
|
12
|
+
icon="grid"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<div class="_nav-filter">
|
|
16
|
+
<CategoryNav
|
|
17
|
+
v-if="categoriesGroupByLetter"
|
|
18
|
+
:items="categoriesGroupByLetter as IChipsItem[]"
|
|
19
|
+
hide-on-mobile
|
|
20
|
+
/>
|
|
21
|
+
<div
|
|
22
|
+
v-else
|
|
23
|
+
class="categories__loading _loading"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div
|
|
28
|
+
v-if="groupsCategories && groupsCategories.length > 0"
|
|
29
|
+
class="grouped__category-wrapper"
|
|
30
|
+
>
|
|
31
|
+
<CategoryGroup
|
|
32
|
+
v-for="(item, index) in groupsCategories"
|
|
33
|
+
:key="`group-category-${index}`"
|
|
34
|
+
:items="item?.categories"
|
|
35
|
+
:title="item.title"
|
|
36
|
+
:icon="item.icon"
|
|
37
|
+
class="grouped__category-section"
|
|
38
|
+
/>
|
|
39
|
+
<UiLink
|
|
40
|
+
:to="generateLink('/categories/all')"
|
|
41
|
+
class="grouped__category-all"
|
|
42
|
+
theme="secondary"
|
|
43
|
+
>{{ t('show_all') }}
|
|
44
|
+
</UiLink>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<SectionSeoText
|
|
48
|
+
:text="t('grouped_footer_text')"
|
|
49
|
+
links-link="grouped_footer_links"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import type { IBreadcrumbItem, IChipsItem } from '../../types';
|
|
56
|
+
import { CategoriesApiService } from '../../services/api/categories.service';
|
|
57
|
+
import { AsyncData } from '../../runtime';
|
|
58
|
+
|
|
59
|
+
const [
|
|
60
|
+
{ data: categoriesGroupByLetter },
|
|
61
|
+
{ data: groupsCategories },
|
|
62
|
+
] = await Promise.all([
|
|
63
|
+
useFetchCategoriesGroupByLetter(AsyncData.GroupsCategoriesByLetterChips, CategoriesApiService.getListGroupedByLetterChips),
|
|
64
|
+
useFetchGroupsCategories(CategoriesApiService.getGroupsCategories),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const { t } = useI18n();
|
|
68
|
+
|
|
69
|
+
const { generateLink } = useGenerateLink();
|
|
70
|
+
|
|
71
|
+
const pagePureName = useGetPureRouteName();
|
|
72
|
+
const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName);
|
|
73
|
+
|
|
74
|
+
watchEffect(() => {
|
|
75
|
+
useHead(meta.value);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const totalCount = computed(() =>
|
|
79
|
+
groupsCategories.value?.reduce((sum, item) => sum + (item.categories?.length || 0), 0) || 0
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const breadcrumbs = [
|
|
83
|
+
{
|
|
84
|
+
name: `${t('navigation.main')}`,
|
|
85
|
+
to: '/',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: `${t('categories')}`,
|
|
89
|
+
},
|
|
90
|
+
] as IBreadcrumbItem[];
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<style lang="scss">
|
|
94
|
+
.grouped__popular {
|
|
95
|
+
display: grid;
|
|
96
|
+
row-gap: 8px;
|
|
97
|
+
|
|
98
|
+
@include from-br(sm) {
|
|
99
|
+
row-gap: 16px;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.grouped__popular-models {
|
|
104
|
+
display: flex;
|
|
105
|
+
column-gap: 4px;
|
|
106
|
+
align-items: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.grouped__popular-highlight {
|
|
110
|
+
@include from-br(sm) {
|
|
111
|
+
padding-right: 4px;
|
|
112
|
+
border-right: 1px solid $grouped-highlight-border;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.grouped__options-separate {
|
|
117
|
+
display: block;
|
|
118
|
+
width: 1px;
|
|
119
|
+
height: 24px;
|
|
120
|
+
background-color: $grouped-separator;
|
|
121
|
+
|
|
122
|
+
@include from-br(sm) {
|
|
123
|
+
height: 32px;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.grouped__loading {
|
|
128
|
+
--size: 24px;
|
|
129
|
+
|
|
130
|
+
min-height: 32px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.grouped__category-icon {
|
|
134
|
+
color: $grouped-category-icon;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.grouped__category-wrapper {
|
|
138
|
+
display: grid;
|
|
139
|
+
row-gap: 16px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.grouped__category-all {
|
|
143
|
+
justify-self: center;
|
|
144
|
+
margin-block: 16px;
|
|
145
|
+
|
|
146
|
+
@include from-br(sm) {
|
|
147
|
+
grid-column: 1/-1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.grouped__footer-text {
|
|
152
|
+
@include font-sm;
|
|
153
|
+
|
|
154
|
+
color: $grouped-footer-text;
|
|
155
|
+
margin-top: 32px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.grouped__category-section {
|
|
159
|
+
padding-bottom: 16px;
|
|
160
|
+
border-bottom: 1px solid $grouped-section-border;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.grouped__category-section:first-child {
|
|
164
|
+
padding-top: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.grouped__category-section:last-child {
|
|
168
|
+
border-bottom: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.grouped__footer-models {
|
|
172
|
+
display: grid;
|
|
173
|
+
row-gap: 8px;
|
|
174
|
+
padding-top: 8px;
|
|
175
|
+
|
|
176
|
+
@include from-br(sm) {
|
|
177
|
+
row-gap: 16px;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</style>
|