itube-specs 0.0.1066 → 0.0.1068
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 +21 -0
- package/components/adv/faphouse/index.vue +243 -0
- package/components/adv/faphouse/promo.vue +73 -0
- package/components/view/video.vue +12 -0
- package/composables/fetch/use-fetch-faphouse.ts +25 -0
- package/i18n/locales/de-DE.json +6 -0
- package/i18n/locales/en-US.json +6 -0
- package/i18n/locales/es-ES.json +6 -0
- package/i18n/locales/fr-FR.json +6 -0
- package/i18n/locales/it-IT.json +6 -0
- package/i18n/locales/ja-JP.json +6 -0
- package/i18n/locales/ko-KR.json +6 -0
- package/i18n/locales/ru-RU.json +6 -0
- package/i18n/locales/zh-CN.json +6 -0
- package/lib/faphouse.ts +42 -0
- package/lib/index.ts +1 -0
- package/middleware/age.global.ts +19 -23
- package/nuxt.config.ts +1 -0
- package/package.json +1 -1
- package/runtime/utils/server/age.ts +3 -3
- package/server/api/faphouse/videos.get.ts +54 -0
- package/types/faphouse.d.ts +14 -0
- package/types/index.d.ts +1 -0
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,27 @@
|
|
|
11
11
|
коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
|
|
12
12
|
кому именно (всем / сайтам с N), что будет, если не сделать.
|
|
13
13
|
|
|
14
|
+
## 0.0.1068 — 2026-11-03
|
|
15
|
+
|
|
16
|
+
- **Всем — решить флаг:** верификация возраста (редирект на agerify, пока не пройдена) снова работает, за флагом
|
|
17
|
+
`FeatureAgeVerificationEnabled`. Список стран общий, в слое (`runtime/utils/server/age.ts`); сейчас для проверки
|
|
18
|
+
только `NL` (GB и штат KS временно сняты). Хотите проверку — добавьте `FeatureAgeVerificationEnabled: true` в
|
|
19
|
+
`config/feature-flags.config.ts` (gay-cool-new включил). Без флага ничего не меняется.
|
|
20
|
+
|
|
21
|
+
## 0.0.1067 — 2026-11-03
|
|
22
|
+
|
|
23
|
+
- **hdsex-remaster, videosection-redesign — удалить оверрайды FapHouse, иначе блок под плеером будет ДВАЖДЫ:**
|
|
24
|
+
карусель FapHouse переехала в слой (`components/adv/faphouse/{index,promo}.vue`, `lib/faphouse.ts`,
|
|
25
|
+
`composables/fetch/use-fetch-faphouse.ts`, `server/api/faphouse/videos.get.ts`, `types/faphouse.d.ts`, ключи
|
|
26
|
+
`faphouse.promo_text/promo_action`, `advertising`, `ad_short`) и рендерится самим `view/video.vue` сразу после
|
|
27
|
+
`ViewVideoAfterPlayerSlot`, с отступом под боковую колонку с 1024px. Удалить у себя эти файлы (и
|
|
28
|
+
`composables/use-fetch-faphouse.ts`) и убрать `<LazyAdvFaphouse>` с его стилем из своего
|
|
29
|
+
`components/view/video-after-player-slot.vue`. Клик по блоку шлёт цель Метрики `adv.faphouse_click_<ниша>`, как
|
|
30
|
+
было в videosection. Свои `faphouse.promo_text` с `%niche%` в i18n сайта оставить — в слое текст без ниши.
|
|
31
|
+
- **Всем — решить флаг:** FapHouse включается флагом `AdsFaphouseEnabled` и конфигом
|
|
32
|
+
`runtimeConfig.public.faphouseConfig` (образец — `config/faphouse.config.ts` hdsex-remaster: `utm.campaign`,
|
|
33
|
+
`utm.content`, `utm.term` = домен, `utm.feedContent`, `itemsLimit`). Без флага блока нет, делать ничего не надо.
|
|
34
|
+
|
|
14
35
|
## 0.0.1065 — 2026-11-02
|
|
15
36
|
|
|
16
37
|
- **Всем — к сведению:** шапка на десктопе как на gay.cool-рефе: высота 65px (было 71), строка поиска 40px, поиск
|
|
@@ -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>
|
|
@@ -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
|
+
};
|
package/i18n/locales/de-DE.json
CHANGED
package/i18n/locales/en-US.json
CHANGED
package/i18n/locales/es-ES.json
CHANGED
|
@@ -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",
|
package/i18n/locales/fr-FR.json
CHANGED
|
@@ -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",
|
package/i18n/locales/it-IT.json
CHANGED
package/i18n/locales/ja-JP.json
CHANGED
package/i18n/locales/ko-KR.json
CHANGED
package/i18n/locales/ru-RU.json
CHANGED
|
@@ -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} онлайн",
|
package/i18n/locales/zh-CN.json
CHANGED
package/lib/faphouse.ts
ADDED
|
@@ -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
package/middleware/age.global.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
// Логика проверки гео + статуса верификации с редиректом на форму сохранена ниже
|
|
3
|
-
// в закомментированном виде — раскомментировать при включении.
|
|
4
|
-
// import { isGeoMatch, getAgeValidationUrl, isAgeVerified } from '../runtime';
|
|
5
|
-
// import { navigateTo } from '#app';
|
|
1
|
+
import { isGeoMatch, getAgeValidationUrl, isAgeVerified } from '../runtime';
|
|
6
2
|
|
|
7
|
-
export default defineNuxtRouteMiddleware(async (
|
|
8
|
-
return;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
3
|
+
export default defineNuxtRouteMiddleware(async (to) => {
|
|
4
|
+
if (!import.meta.server) return;
|
|
5
|
+
if (to.path.startsWith('/bff/age/verify')) return;
|
|
6
|
+
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
if (!config.public.featureFlags?.FeatureAgeVerificationEnabled) return;
|
|
9
|
+
|
|
10
|
+
const event = useRequestEvent();
|
|
11
|
+
if (!(await isGeoMatch(event, config))) return;
|
|
12
|
+
|
|
13
|
+
const verified = await isAgeVerified(event, config);
|
|
14
|
+
|
|
15
|
+
if (!verified) {
|
|
16
|
+
const { url } = await getAgeValidationUrl(event, to.path, config);
|
|
17
|
+
if (url) {
|
|
18
|
+
return navigateTo(url, { external: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
25
21
|
});
|
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getCookie } from 'h3';
|
|
2
2
|
import { getClientHeaders } from './get-client-headers';
|
|
3
3
|
|
|
4
|
-
/** Возвращает true если пользователь находится в регионе где требуется верификация
|
|
4
|
+
/** Возвращает true если пользователь находится в регионе где требуется верификация возраста. */
|
|
5
5
|
export async function isGeoMatch(event, _config) {
|
|
6
|
-
const AGE_VERIFY_REGIONS = [
|
|
7
|
-
const AGE_VERIFY_COUNTRIES = ['
|
|
6
|
+
const AGE_VERIFY_REGIONS: string[] = [];
|
|
7
|
+
const AGE_VERIFY_COUNTRIES = ['NL'];
|
|
8
8
|
|
|
9
9
|
const headers = getClientHeaders(event);
|
|
10
10
|
const countryCode = headers[ 'countryCode' ];
|
|
@@ -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