itube-specs 0.0.1064 → 0.0.1067

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/SITE_UPDATES.md CHANGED
@@ -11,6 +11,25 @@
11
11
  коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
12
12
  кому именно (всем / сайтам с N), что будет, если не сделать.
13
13
 
14
+ ## 0.0.1067 — 2026-11-03
15
+
16
+ - **hdsex-remaster, videosection-redesign — удалить оверрайды FapHouse, иначе блок под плеером будет ДВАЖДЫ:**
17
+ карусель FapHouse переехала в слой (`components/adv/faphouse/{index,promo}.vue`, `lib/faphouse.ts`,
18
+ `composables/fetch/use-fetch-faphouse.ts`, `server/api/faphouse/videos.get.ts`, `types/faphouse.d.ts`, ключи
19
+ `faphouse.promo_text/promo_action`, `advertising`, `ad_short`) и рендерится самим `view/video.vue` сразу после
20
+ `ViewVideoAfterPlayerSlot`, с отступом под боковую колонку с 1024px. Удалить у себя эти файлы (и
21
+ `composables/use-fetch-faphouse.ts`) и убрать `<LazyAdvFaphouse>` с его стилем из своего
22
+ `components/view/video-after-player-slot.vue`. Клик по блоку шлёт цель Метрики `adv.faphouse_click_<ниша>`, как
23
+ было в videosection. Свои `faphouse.promo_text` с `%niche%` в i18n сайта оставить — в слое текст без ниши.
24
+ - **Всем — решить флаг:** FapHouse включается флагом `AdsFaphouseEnabled` и конфигом
25
+ `runtimeConfig.public.faphouseConfig` (образец — `config/faphouse.config.ts` hdsex-remaster: `utm.campaign`,
26
+ `utm.content`, `utm.term` = домен, `utm.feedContent`, `itemsLimit`). Без флага блока нет, делать ничего не надо.
27
+
28
+ ## 0.0.1065 — 2026-11-02
29
+
30
+ - **Всем — к сведению:** шапка на десктопе как на gay.cool-рефе: высота 65px (было 71), строка поиска 40px, поиск
31
+ растягивается до 964px (было 672, до 1350px — 500). Делать ничего не надо.
32
+
14
33
  ## 0.0.1062 — 2026-11-02
15
34
 
16
35
  - **Всем — к сведению:** за тем же флагом `FeatureUploadEnabled` — страница «My videos» `/library/uploads` (загруженные
@@ -136,7 +136,7 @@ $search-border--active: $color-indigo-500;
136
136
  $search-button-bg: $color-indigo-600;
137
137
  $search-button-bg--hover: $color-indigo-500;
138
138
  $search-max-width: 500px;
139
- $search-max-width-md: 672px;
139
+ $search-max-width-md: 964px;
140
140
  $search-mobile-bg: $color-neutral-940-95;
141
141
  $search-mobile-border: $color-zinc-800;
142
142
  $search-mobile-decorate: $mono-black-50;
@@ -175,7 +175,7 @@ $search-border--active: $color-indigo-600;
175
175
  $search-button-bg: $color-indigo-600;
176
176
  $search-button-bg--hover: $color-indigo-500;
177
177
  $search-max-width: 500px;
178
- $search-max-width-md: 672px;
178
+ $search-max-width-md: 964px;
179
179
  $search-mobile-bg: $color-slate-875;
180
180
  $search-mobile-border: $color-slate-700;
181
181
  $search-mobile-decorate: $mono-black-50;
@@ -45,6 +45,6 @@
45
45
 
46
46
  // Высота шапки меняется вместе с её раскладкой, а не на общем тире sm.
47
47
  @include from-br($brkpnt-burger) {
48
- --header-height: 71px;
48
+ --header-height: 65px;
49
49
  }
50
50
  }
@@ -0,0 +1,243 @@
1
+ <!-- Партнёрский блок FapHouse под плеером — по макету редизайна (v0-dark-eta, /video/1).
2
+
3
+ Заголовка у блока нет: рекламу помечает плашка «AD» на каждой тумбе плюс
4
+ `aria-label` секции. Под лентой — широкая промо-кнопка на тур (AdvFaphousePromo).
5
+
6
+ На десктопе (от 1024px, как в макете) пять тумб в ряд; ниже — горизонтальная
7
+ лента: карточка занимает 42% ширины, поэтому третья обрезается краем и видно,
8
+ что лента прокручивается. Лента вылезает за поля контейнера до краёв экрана —
9
+ так в макете (`-mx-4 px-4`).
10
+
11
+ Поведение тумбы повторяет боевое: постер по умолчанию, при наведении на его
12
+ место кладётся mp4-превью (`previewUrl`) — только на десктопе, на тач-устройствах
13
+ превью не грузится вообще.
14
+
15
+ Данные — `useFetchFaphouse()` через BFF-прокси фида партнёрки. Блок целиком
16
+ гейтится флагом `AdsFaphouseEnabled` и не рендерится при пустом списке. -->
17
+ <template>
18
+ <section
19
+ v-if="isEnabled && items.length"
20
+ :aria-label="t('advertising')"
21
+ class="adv-faphouse"
22
+ >
23
+ <ul class="adv-faphouse__items">
24
+ <li
25
+ v-for="(item, index) in items"
26
+ :key="`faphouse-${index}`"
27
+ class="adv-faphouse__item"
28
+ >
29
+ <a
30
+ :href="item.url"
31
+ class="adv-faphouse__link"
32
+ target="_blank"
33
+ rel="nofollow noopener sponsored"
34
+ @click="onClick"
35
+ @mouseenter="onPreviewShow(index)"
36
+ @mouseleave="onPreviewHide"
37
+ >
38
+ <span class="adv-faphouse__poster">
39
+ <UiImg
40
+ :src="item.imageUrl"
41
+ :alt="item.title"
42
+ class="adv-faphouse__img"
43
+ width="335"
44
+ height="188"
45
+ sizes="240px"
46
+ loading="lazy"
47
+ />
48
+ <video
49
+ v-if="previewIndex === index && item.previewUrl"
50
+ :src="item.previewUrl"
51
+ class="adv-faphouse__video"
52
+ muted
53
+ loop
54
+ autoplay
55
+ playsinline
56
+ />
57
+ <span class="adv-faphouse__ad">{{ t('ad_short') }}</span>
58
+ <span
59
+ v-if="item.duration"
60
+ class="adv-faphouse__duration"
61
+ >{{ formatDuration(item.duration) }}</span>
62
+ </span>
63
+ <span class="adv-faphouse__item-title _truncate">{{ item.title }}</span>
64
+ </a>
65
+ </li>
66
+ </ul>
67
+
68
+ <AdvFaphousePromo class="adv-faphouse__promo" />
69
+ </section>
70
+ </template>
71
+
72
+ <script setup lang="ts">
73
+ import { faphouseClickGoal } from '../../../lib/faphouse';
74
+
75
+ const { t } = useI18n();
76
+ const reachGoal = useMetrikaGoal();
77
+
78
+ const onClick = () => reachGoal(...faphouseClickGoal(NicheHelper.getCurrentNiche()));
79
+
80
+ const isEnabled = computed(() => !!useRuntimeConfig().public.featureFlags.AdsFaphouseEnabled);
81
+
82
+ const { items } = isEnabled.value ? await useFetchFaphouse() : { items: computed(() => []) };
83
+
84
+ const isMobile = useState<boolean>('isMobile');
85
+ const previewIndex = ref(-1);
86
+
87
+ const onPreviewShow = (index: number) => {
88
+ if (isMobile.value) return;
89
+
90
+ previewIndex.value = index;
91
+ };
92
+
93
+ const onPreviewHide = () => {
94
+ previewIndex.value = -1;
95
+ };
96
+
97
+ /** Фид отдаёт длительность секундами; часы показываем только если они есть. */
98
+ function formatDuration(duration: string | number): string {
99
+ if (typeof duration === 'string') return duration;
100
+
101
+ const total = Math.max(0, Math.round(duration));
102
+ const hours = Math.floor(total / 3600);
103
+ const minutes = Math.floor((total % 3600) / 60);
104
+ const seconds = String(total % 60).padStart(2, '0');
105
+
106
+ return hours
107
+ ? `${hours}:${String(minutes).padStart(2, '0')}:${seconds}`
108
+ : `${minutes}:${seconds}`;
109
+ }
110
+ </script>
111
+
112
+ <style lang="scss">
113
+ .adv-faphouse {
114
+ display: grid;
115
+ row-gap: 16px;
116
+ margin-bottom: 24px;
117
+ }
118
+
119
+ .adv-faphouse__items {
120
+ display: grid;
121
+ grid-auto-flow: column;
122
+
123
+ // лента: карточка в 42% ширины, третья обрезается краем — видно, что она листается
124
+ grid-auto-columns: 42%;
125
+ gap: 12px;
126
+ overflow-x: auto;
127
+ scrollbar-width: none;
128
+ scroll-snap-type: x mandatory;
129
+
130
+ // лента идёт до краёв экрана, за поля контейнера
131
+ margin-inline: calc(-1 * var(--container-padding-inline));
132
+ padding-inline: var(--container-padding-inline);
133
+
134
+ &::-webkit-scrollbar {
135
+ display: none;
136
+ }
137
+
138
+ @include from-br(sd) {
139
+ grid-auto-flow: row;
140
+ grid-template-columns: repeat(5, minmax(0, 1fr));
141
+ gap: 16px;
142
+ overflow-x: visible;
143
+ margin-inline: 0;
144
+ padding-inline: 0;
145
+ }
146
+ }
147
+
148
+ .adv-faphouse__item {
149
+ min-width: 0;
150
+ scroll-snap-align: start;
151
+ }
152
+
153
+ .adv-faphouse__link {
154
+ display: grid;
155
+ row-gap: 10px;
156
+ color: $faphouse-item-text;
157
+ }
158
+
159
+ .adv-faphouse__poster {
160
+ position: relative;
161
+ display: block;
162
+ overflow: hidden;
163
+ aspect-ratio: 16 / 9;
164
+ background-color: $faphouse-poster-bg;
165
+
166
+ // в макете rounded-lg = 8px; наш $radius-lg — это 10px, поэтому $radius-md
167
+ border-radius: $radius-md;
168
+ }
169
+
170
+ .adv-faphouse__img,
171
+ .adv-faphouse__video {
172
+ width: 100%;
173
+ height: 100%;
174
+ object-fit: cover;
175
+ }
176
+
177
+ /* Наезд постера — как у видео-тумб макета */
178
+ @media (hover: hover) {
179
+ .adv-faphouse__img {
180
+ transition: .3s var(--ease-in-out) transform;
181
+ }
182
+
183
+ .adv-faphouse__link:is(:hover, :focus-visible) .adv-faphouse__img {
184
+ transform: scale(1.05);
185
+ }
186
+
187
+ .adv-faphouse__link:is(:hover, :focus-visible) .adv-faphouse__item-title {
188
+ color: $faphouse-item-text--hover;
189
+ }
190
+
191
+ .adv-faphouse__item-title {
192
+ transition: var(--transition) color;
193
+ }
194
+ }
195
+
196
+ .adv-faphouse__video {
197
+ position: absolute;
198
+ inset: 0;
199
+ z-index: $z-index-plus;
200
+ }
201
+
202
+ /* Плашки выше превью: пометка «реклама» обязана быть видна и когда тумба проигрывает
203
+ ролик. Порядка в разметке тут мало — у превью свой z-index, он бы их накрыл. */
204
+ .adv-faphouse__ad,
205
+ .adv-faphouse__duration {
206
+ position: absolute;
207
+ z-index: $z-index-loader;
208
+ border-radius: $radius-small;
209
+ }
210
+
211
+ .adv-faphouse__ad {
212
+ /* 9px в шкале микро-размеров слоя нет (ближайшие 10 и 11), а в макете плашка
213
+ именно 9px с плотной строкой — как и наша собственная пометка «реклама» на
214
+ нативных слотах (adv/spot.vue). Держим их одинаковыми. */
215
+ font-size: 9px;
216
+ line-height: 1.25;
217
+ top: 6px;
218
+ right: 6px;
219
+ padding: 1px 4px;
220
+ font-weight: 700;
221
+ text-transform: uppercase;
222
+ color: $faphouse-ad-text;
223
+ background-color: $faphouse-ad-bg;
224
+ }
225
+
226
+ .adv-faphouse__duration {
227
+ @include font-xs;
228
+
229
+ bottom: 8px;
230
+ left: 8px;
231
+ padding: 2px 6px;
232
+ font-weight: 500;
233
+ color: $faphouse-duration-text;
234
+ background-color: $faphouse-duration-bg;
235
+ }
236
+
237
+ .adv-faphouse__item-title {
238
+ @include font-sm;
239
+
240
+ // обрезка — общим модификатором `_truncate`, число строк через его переменную
241
+ --line-clamp: 2;
242
+ }
243
+ </style>
@@ -0,0 +1,73 @@
1
+ <!-- Промо-кнопка FapHouse («…GET ACCESS») — широкая плашка под каруселью.
2
+ Текст — общий ключ `faphouse.promo_text`; сайт с нишевой формулировкой перекрывает его своим (с `%niche%`-токеном
3
+ при `nichePostTranslation`). Ссылка на тур строится из ниши и utm-меток сайта (`runtimeConfig.public.faphouseConfig`). -->
4
+ <template>
5
+ <a
6
+ :href="link"
7
+ :data-niche="niche"
8
+ class="adv-faphouse-promo"
9
+ target="_blank"
10
+ rel="nofollow noopener sponsored"
11
+ @click="reachGoal(...faphouseClickGoal(niche))"
12
+ >
13
+ <span class="adv-faphouse-promo__text">{{ t('faphouse.promo_text') }}</span>
14
+ <span class="adv-faphouse-promo__action">{{ t('faphouse.promo_action') }}</span>
15
+ </a>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { faphouseClickGoal, faphouseTourLink } from '../../../lib/faphouse';
20
+
21
+ const { t } = useI18n();
22
+ const reachGoal = useMetrikaGoal();
23
+
24
+ const niche = computed(() => NicheHelper.getCurrentNiche());
25
+ const link = computed(() => faphouseTourLink(niche.value, useRuntimeConfig().public.faphouseConfig));
26
+ </script>
27
+
28
+ <style lang="scss">
29
+ .adv-faphouse-promo {
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ gap: 8px;
33
+ align-items: center;
34
+ justify-content: center;
35
+ padding: 10px 12px;
36
+ background-image: linear-gradient(to bottom, $faphouse-promo-bg-from, $faphouse-promo-bg-to);
37
+ color: $faphouse-promo-text;
38
+
39
+ // в макете rounded-lg = 8px; наш $radius-lg — это 10px
40
+ border-radius: $radius-md;
41
+ text-align: center;
42
+
43
+ @include hover(box-shadow) {
44
+ box-shadow: $faphouse-promo-shadow--hover;
45
+ }
46
+ }
47
+
48
+ .adv-faphouse-promo__text {
49
+ @include font-2xs;
50
+
51
+ font-weight: 800;
52
+
53
+ @include from-br(xm) {
54
+ @include font-sm;
55
+ }
56
+ }
57
+
58
+ .adv-faphouse-promo__action {
59
+ @include font-xs;
60
+
61
+ padding: 6px 12px;
62
+ font-weight: 700;
63
+ text-transform: uppercase;
64
+ color: $faphouse-promo-action-text;
65
+ background-color: $faphouse-promo-action-bg;
66
+ border-radius: $radius-small;
67
+ white-space: nowrap;
68
+
69
+ @include from-br(xm) {
70
+ @include font-sm;
71
+ }
72
+ }
73
+ </style>
@@ -22,21 +22,6 @@
22
22
  </div>
23
23
 
24
24
  <div class="layout-header__right">
25
- <NuxtLink
26
- v-if="featureFlags.FeatureUploadEnabled"
27
- :to="uploadLink"
28
- :aria-current="isUploadPage ? 'page' : undefined"
29
- :tabindex="isUploadPage ? -1 : undefined"
30
- :class="{'--current': isUploadPage}"
31
- class="layout-header__upload _from-sd"
32
- >
33
- <UiIcon
34
- name="upload-arrow"
35
- size="18"
36
- aria-hidden="true"
37
- />
38
- {{ $t('upload') }}
39
- </NuxtLink>
40
25
  <div
41
26
  ref="languageRef"
42
27
  class="layout-header__language _from-sd"
@@ -71,6 +56,21 @@
71
56
  </KeepAlive>
72
57
  </transition>
73
58
  </div>
59
+ <NuxtLink
60
+ v-if="featureFlags.FeatureUploadEnabled"
61
+ :to="uploadLink"
62
+ :aria-current="isUploadPage ? 'page' : undefined"
63
+ :tabindex="isUploadPage ? -1 : undefined"
64
+ :class="{'--current': isUploadPage}"
65
+ class="layout-header__upload _from-sd"
66
+ >
67
+ <UiIcon
68
+ name="upload-arrow"
69
+ size="18"
70
+ aria-hidden="true"
71
+ />
72
+ {{ $t('upload') }}
73
+ </NuxtLink>
74
74
 
75
75
  <LayoutNotifications
76
76
  v-if="featureFlags.FeatureFollowEnabled"
@@ -155,12 +155,9 @@ function closeLanguage() {
155
155
  width: 100%;
156
156
 
157
157
  @include from-br($brkpnt-burger) {
158
- grid-template-columns: minmax(max-content, 1fr) minmax(0, $search-max-width) minmax(max-content, 1fr);
159
- justify-content: stretch;
160
- }
161
-
162
- @include from-br(md) {
163
158
  grid-template-columns: minmax(max-content, 1fr) minmax(0, $search-max-width-md) minmax(max-content, 1fr);
159
+ justify-content: stretch;
160
+ column-gap: 16px;
164
161
  }
165
162
 
166
163
  /* поиск | лого | колокольчик | бургер — 1fr держит лого по центру */
@@ -120,6 +120,10 @@ $icon-size: 16px;
120
120
  padding-right: 80px;
121
121
  padding-block: 12px;
122
122
 
123
+ @include from-br($brkpnt-burger) {
124
+ padding-block: 9px;
125
+ }
126
+
123
127
  @include to-br(sm) {
124
128
  --input-autofill-text: #{$search-text--mobile};
125
129
 
@@ -171,6 +175,10 @@ $icon-size: 16px;
171
175
  cursor: pointer;
172
176
  }
173
177
 
178
+ @include from-br($brkpnt-burger) {
179
+ right: 2px;
180
+ }
181
+
174
182
  @include hover(background-color) {
175
183
  background-color: $search-button-bg--hover;
176
184
  }
@@ -207,8 +207,8 @@ const latestSearches = computed(() =>
207
207
  padding-bottom: 8px;
208
208
  }
209
209
 
210
- @include from-br(md) {
211
- width: $search-max-width-md;
210
+ @include from-br($brkpnt-burger) {
211
+ width: 100%;
212
212
  }
213
213
  }
214
214
 
@@ -41,8 +41,8 @@
41
41
  <UploadSpinner v-if="isChecking"/>
42
42
  <UiIcon
43
43
  v-else
44
- name="arrow-up"
45
- size="20"
44
+ name="upload-arrow"
45
+ size="18"
46
46
  aria-hidden="true"
47
47
  />
48
48
  {{ mobileLabel }}
@@ -69,8 +69,8 @@
69
69
  <UploadSpinner v-if="isChecking"/>
70
70
  <UiIcon
71
71
  v-else
72
- name="arrow-up"
73
- size="20"
72
+ name="upload-arrow"
73
+ size="18"
74
74
  />
75
75
  </span>
76
76
  <p class="upload-picker__zone-text">{{ zoneLabel }}</p>
@@ -82,8 +82,8 @@
82
82
  @click="openDialog"
83
83
  >
84
84
  <UiIcon
85
- name="arrow-up"
86
- size="20"
85
+ name="upload-arrow"
86
+ size="18"
87
87
  aria-hidden="true"
88
88
  />
89
89
  {{ t(hasFailed ? 'upload_page.choose_another_file' : 'upload_page.choose_file') }}
@@ -52,6 +52,11 @@
52
52
 
53
53
  <ViewVideoAfterPlayerSlot :data="data"/>
54
54
 
55
+ <LazyAdvFaphouse
56
+ class="view-video__faphouse"
57
+ hydrate-on-idle
58
+ />
59
+
55
60
  <section
56
61
  v-if="related"
57
62
  :class="{'_loading': status === 'pending'}"
@@ -343,6 +348,13 @@ function getModelChips(model: IVideoData['models'][number]): IChipsItem {
343
348
  margin-top: 32px;
344
349
  }
345
350
 
351
+ /* С 1024px справа от плеера колонка (288px + 16px отступа) — блок партнёрки живёт в ширине плеера. */
352
+ .view-video__faphouse {
353
+ @include from-br(sd) {
354
+ padding-right: 304px;
355
+ }
356
+ }
357
+
346
358
  .view-video__video-aside {
347
359
  @include from-br(md) {
348
360
  display: flex;
@@ -0,0 +1,25 @@
1
+ import type { IFaphouseVideo } from '../../types';
2
+
3
+ /**
4
+ * Тумбы промо-карусели FapHouse.
5
+ *
6
+ * Данные — ручка партнёрки через BFF-прокси (`server/api/faphouse/videos.get.ts`).
7
+ * Ниша и язык уходят query-параметрами: прокси перекладывает их в заголовки
8
+ * `X-Niche` / `X-Language`, которых ручка требует обязательно.
9
+ */
10
+ export const useFetchFaphouse = async (niche?: string) => {
11
+ const config = useRuntimeConfig().public.faphouseConfig;
12
+ const quantity = config?.itemsLimit || 5;
13
+ const currentNiche = niche || NicheHelper.getCurrentNiche();
14
+ const { locale } = useI18n();
15
+
16
+ const { data } = await useFetch<{ items: IFaphouseVideo[] }>('/bff/faphouse/videos', {
17
+ query: { niche: currentNiche, lang: locale, quantity },
18
+ key: `faphouse-${currentNiche}-${locale.value}`,
19
+ default: () => ({ items: [] }),
20
+ });
21
+
22
+ const items = computed<IFaphouseVideo[]>(() => data.value?.items || []);
23
+
24
+ return { items };
25
+ };
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ Schau und lade VOLLE Videos in HD & 4K 🔥",
4
+ "promo_action": "ZUGANG ERHALTEN"
5
+ },
6
+ "advertising": "Werbung",
7
+ "ad_short": "Ad",
2
8
  "webcams": {
3
9
  "live_cams": "Live Cams",
4
10
  "online_now": "{count} online",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ Watch and download FULL videos in HD & 4K quality 🔥",
4
+ "promo_action": "GET ACCESS"
5
+ },
6
+ "advertising": "Advertising",
7
+ "ad_short": "Ad",
2
8
  "my_uploads": {
3
9
  "title": "My videos",
4
10
  "filter_label": "Filter by status",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ Mira y descarga vídeos COMPLETOS en HD y 4K 🔥",
4
+ "promo_action": "OBTENER ACCESO"
5
+ },
6
+ "advertising": "Publicidad",
7
+ "ad_short": "Ad",
2
8
  "webcams": {
3
9
  "live_cams": "Cams en directo",
4
10
  "online_now": "{count} en línea",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ Regardez et téléchargez des vidéos COMPLÈTES en HD et 4K 🔥",
4
+ "promo_action": "OBTENIR L’ACCÈS"
5
+ },
6
+ "advertising": "Publicité",
7
+ "ad_short": "Ad",
2
8
  "webcams": {
3
9
  "live_cams": "Cams en direct",
4
10
  "online_now": "{count} en ligne",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ Guarda e scarica video COMPLETI in HD e 4K 🔥",
4
+ "promo_action": "OTTIENI ACCESSO"
5
+ },
6
+ "advertising": "Pubblicità",
7
+ "ad_short": "Ad",
2
8
  "webcams": {
3
9
  "live_cams": "Cam in diretta",
4
10
  "online_now": "{count} online",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ 動画のフル版をHD・4K画質で視聴・ダウンロード 🔥",
4
+ "promo_action": "アクセスする"
5
+ },
6
+ "advertising": "広告",
7
+ "ad_short": "PR",
2
8
  "webcams": {
3
9
  "live_cams": "ライブチャット",
4
10
  "online_now": "{count}人がオンライン",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ 영상 전체를 HD·4K 화질로 감상하고 다운로드 🔥",
4
+ "promo_action": "액세스 받기"
5
+ },
6
+ "advertising": "광고",
7
+ "ad_short": "AD",
2
8
  "webcams": {
3
9
  "live_cams": "라이브 캠",
4
10
  "online_now": "{count}명 접속 중",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ Смотрите и скачивайте ПОЛНЫЕ видео в качестве HD и 4K 🔥",
4
+ "promo_action": "ПОЛУЧИТЬ ДОСТУП"
5
+ },
6
+ "advertising": "Реклама",
7
+ "ad_short": "Ad",
2
8
  "webcams": {
3
9
  "live_cams": "Трансляции",
4
10
  "online_now": "{count} онлайн",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "faphouse": {
3
+ "promo_text": "❤️ 观看并下载完整视频,支持 HD 和 4K 🔥",
4
+ "promo_action": "获取访问权限"
5
+ },
6
+ "advertising": "广告",
7
+ "ad_short": "Ad",
2
8
  "webcams": {
3
9
  "live_cams": "真人视讯",
4
10
  "online_now": "{count} 人在线",
@@ -0,0 +1,42 @@
1
+ import type { Niche } from '../runtime';
2
+
3
+ /**
4
+ * Промо-карусель FapHouse («Suggested to you») под плеером.
5
+ *
6
+ * Ссылка на тур: `https://faphouse.com/{нишевый префикс}tour?utm_*` — префикс
7
+ * задаёт раздел FapHouse (у shemale он исторически `transgender/`), utm-метки
8
+ * приходят из конфига сайта (`runtimeConfig.public.faphouseConfig`), потому
9
+ * что `utm_term` — это домен сайта.
10
+ */
11
+ export const FAPHOUSE_NICHE_PREFIX: Record<string, string> = {
12
+ straight: '',
13
+ gay: 'gay/',
14
+ shemale: 'transgender/',
15
+ };
16
+
17
+ export type FaphouseConfigType = {
18
+ baseUrl?: string;
19
+ itemsLimit?: number;
20
+ utm?: { campaign?: string, content?: string, term?: string, feedContent?: string };
21
+ };
22
+
23
+ /** Ссылка на тур FapHouse для ниши. */
24
+ export function faphouseTourLink(niche: Niche | string, config?: FaphouseConfigType): string {
25
+ const prefix = FAPHOUSE_NICHE_PREFIX[niche] ?? '';
26
+ const base = config?.baseUrl || 'https://faphouse.com';
27
+ const utm = config?.utm || {};
28
+ const params = new URLSearchParams();
29
+
30
+ if (utm.campaign) params.set('utm_campaign', utm.campaign);
31
+ if (utm.content) params.set('utm_content', utm.content);
32
+ if (utm.term) params.set('utm_term', utm.term);
33
+
34
+ const query = params.toString();
35
+
36
+ return `${base}/${prefix}tour${query ? `?${query}` : ''}`;
37
+ }
38
+
39
+ /** Цель Метрики на клик по FapHouse — имена и параметры боевого videosection (`js/enums/metrika-events.enum.js`). */
40
+ export function faphouseClickGoal(niche: string): [string, Record<string, unknown>] {
41
+ return [`adv.faphouse_click_${niche}`, { adv_faphouse_click: { niche } }];
42
+ }
package/lib/index.ts CHANGED
@@ -19,3 +19,4 @@ export * from './scheme/categories-navigation';
19
19
  export * from './scheme/navigation-items';
20
20
  export * from './scheme/profile-navigation-items';
21
21
  export * from './scheme/additional-links-items';
22
+ export * from './faphouse';
package/nuxt.config.ts CHANGED
@@ -88,6 +88,7 @@ export default defineNuxtConfig({
88
88
  // runtimeConfig: по ней выбирается вариант портрета из `portraits`.
89
89
  theme: '',
90
90
  webcamsConfig: {},
91
+ faphouseConfig: {},
91
92
  locales: {
92
93
  codes: [],
93
94
  default: 'en',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1064",
4
+ "version": "0.0.1067",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -0,0 +1,54 @@
1
+ import type { IFaphouseVideo } from '../../../types';
2
+
3
+ type RawFaphouseItem = {
4
+ url?: string;
5
+ original_url?: string;
6
+ title?: string;
7
+ image_url?: string;
8
+ image2x_url?: string;
9
+ preview_url?: string;
10
+ duration?: number;
11
+ quality?: string;
12
+ };
13
+
14
+ /**
15
+ * BFF-прокси фида FapHouse (`GET /v1/faphouse/videos`).
16
+ *
17
+ * Ниша и язык уходят заголовками `X-Niche` / `X-Language` — так требует ручка;
18
+ * клиент передаёт их query-параметрами `niche` и `lang`, потому что до BFF свои
19
+ * заголовки он не проставляет. utm-метки партнёрки (`utm-campaign`,
20
+ * `utm-content`) берём из конфига сайта: их API подставляет в ссылки роликов, а
21
+ * значения per-site.
22
+ */
23
+ export default defineEventHandler(async (event): Promise<{ items: IFaphouseVideo[], total: number }> => {
24
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
25
+ const query = getQuery(event);
26
+ const config = useRuntimeConfig(event).public.faphouseConfig;
27
+
28
+ const url = new URL(`${apiUrl}/v1/faphouse/videos`);
29
+ url.searchParams.set('quantity', String(query.quantity || config?.itemsLimit || 5));
30
+ if (config?.utm?.campaign) url.searchParams.set('utm-campaign', config.utm.campaign);
31
+ if (config?.utm?.feedContent) url.searchParams.set('utm-content', config.utm.feedContent);
32
+
33
+ const raw = await apiFetch(url.toString(), {
34
+ headers: {
35
+ ...event.node.req.headers,
36
+ 'x-niche': String(query.niche || ''),
37
+ 'x-language': String(query.lang || event.node.req.headers['x-language'] || 'en'),
38
+ } as HeadersInit,
39
+ });
40
+
41
+ const items: RawFaphouseItem[] = raw?.data?.items || [];
42
+
43
+ return {
44
+ total: raw?.data?.total || 0,
45
+ items: items.map(item => ({
46
+ url: item.url || item.original_url || '',
47
+ title: item.title || '',
48
+ imageUrl: item.image_url || item.image2x_url || '',
49
+ previewUrl: item.preview_url || '',
50
+ duration: item.duration || 0,
51
+ quality: item.quality || '',
52
+ })).filter(item => item.url && item.imageUrl),
53
+ };
54
+ });
@@ -0,0 +1,14 @@
1
+ /** Тумба промо-карусели FapHouse (партнёрский фид). */
2
+ export interface IFaphouseVideo {
3
+ /** Ссылка на видео на faphouse.com (уже с utm-метками партнёрки). */
4
+ url: string;
5
+ /** Постер тумбы. */
6
+ imageUrl: string;
7
+ /** Видео-превью для ховера (mp4). */
8
+ previewUrl?: string;
9
+ title: string;
10
+ /** Длительность: строка вида `20:26` или секунды. */
11
+ duration?: string | number;
12
+ /** `hd` | `sd` — бейдж качества. */
13
+ quality?: string;
14
+ }
package/types/index.d.ts CHANGED
@@ -84,3 +84,4 @@ export * from './switcher-item';
84
84
  export * from './webcam-facet';
85
85
  export * from './webcam-models-response';
86
86
  export * from './upload.d.ts';
87
+ export * from './faphouse.d.ts';
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" viewBox="0 0 24 24"><path d="m5 12 7-7 7 7M12 19V5"/></svg>