adata-ui 2.1.41-beta.15 → 2.1.41-beta.17
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/components/elements/empty-box/empty-box-V2.vue +2 -2
- package/components/features/lang-switcher/lang-switcher.vue +12 -10
- package/components/modals/id/IdLoginModal.vue +2 -0
- package/components/modals/id/IdNewPasswordModal.vue +2 -0
- package/components/modals/id/IdRecoveryModal.vue +1 -0
- package/components/modals/id/IdRegistrationModal.vue +3 -0
- package/components/navigation/bottom-navigation/ABottomNavigation.vue +10 -4
- package/components/navigation/bottom-navigation/types.ts +6 -0
- package/components/navigation/mobile-navigation/AMobileNavigation.vue +31 -14
- package/lang/en.ts +1 -0
- package/lang/kk.ts +1 -0
- package/lang/ru.ts +1 -0
- package/nuxt.config.ts +25 -1
- package/package.json +1 -1
- package/server/middleware/color-mode-cookie.ts +58 -0
|
@@ -105,22 +105,24 @@ function onSelect(loc: LocaleItem) {
|
|
|
105
105
|
role="option"
|
|
106
106
|
:aria-selected="localeCode(loc) === locale"
|
|
107
107
|
>
|
|
108
|
+
<!-- Текущий язык — не ссылка: якорь на саму страницу краулеры считают циклической ссылкой -->
|
|
109
|
+
<span
|
|
110
|
+
v-if="localeCode(loc) === locale"
|
|
111
|
+
:data-test-id="`header-switch-${localeCode(loc)}-language-button${testIdSuffix}`"
|
|
112
|
+
class="flex w-full items-center justify-between gap-3 rounded-md bg-brand-subtle px-3 py-2 text-body-md font-medium text-brand"
|
|
113
|
+
@click="isOpen = false"
|
|
114
|
+
>
|
|
115
|
+
<span>{{ t(`lang.${localeCode(loc)}.long`) }}</span>
|
|
116
|
+
<check class="size-4 shrink-0 text-brand" />
|
|
117
|
+
</span>
|
|
108
118
|
<nuxt-link
|
|
119
|
+
v-else
|
|
109
120
|
:data-test-id="`header-switch-${localeCode(loc)}-language-button${testIdSuffix}`"
|
|
110
121
|
:to="switchLocalePath(localeCode(loc))"
|
|
111
|
-
class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-body-md transition-colors duration-100"
|
|
112
|
-
:class="[
|
|
113
|
-
localeCode(loc) === locale
|
|
114
|
-
? 'bg-brand-subtle font-medium text-brand'
|
|
115
|
-
: 'text-quaternary hover:bg-action-secondary-hover',
|
|
116
|
-
]"
|
|
122
|
+
class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-body-md text-quaternary transition-colors duration-100 hover:bg-action-secondary-hover"
|
|
117
123
|
@click.prevent="onSelect(loc)"
|
|
118
124
|
>
|
|
119
125
|
<span>{{ t(`lang.${localeCode(loc)}.long`) }}</span>
|
|
120
|
-
<check
|
|
121
|
-
v-if="localeCode(loc) === locale"
|
|
122
|
-
class="size-4 shrink-0 text-brand"
|
|
123
|
-
/>
|
|
124
126
|
</nuxt-link>
|
|
125
127
|
</li>
|
|
126
128
|
</ul>
|
|
@@ -253,6 +253,7 @@ function onForgotPassword() {
|
|
|
253
253
|
size="lg"
|
|
254
254
|
autocomplete="email"
|
|
255
255
|
:start-icon="AIconMail"
|
|
256
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
256
257
|
:placeholder="t('modals.id.login.placeholders.email')"
|
|
257
258
|
:error="getError('username')"
|
|
258
259
|
data-test-id="login-form-email-input"
|
|
@@ -268,6 +269,7 @@ function onForgotPassword() {
|
|
|
268
269
|
:input-id="passwordFieldId"
|
|
269
270
|
size="lg"
|
|
270
271
|
:start-icon="AIconLock"
|
|
272
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
271
273
|
:placeholder="t('modals.id.login.placeholders.password')"
|
|
272
274
|
:error="getError('password')"
|
|
273
275
|
data-test-id="login-form-password-input"
|
|
@@ -116,6 +116,7 @@ async function onSubmit() {
|
|
|
116
116
|
size="lg"
|
|
117
117
|
autocomplete="new-password"
|
|
118
118
|
:start-icon="AIconLock"
|
|
119
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
119
120
|
:placeholder="t('modals.id.newPassword.placeholder.1')"
|
|
120
121
|
:error="getError('password')"
|
|
121
122
|
/>
|
|
@@ -133,6 +134,7 @@ async function onSubmit() {
|
|
|
133
134
|
size="lg"
|
|
134
135
|
autocomplete="new-password"
|
|
135
136
|
:start-icon="AIconLock"
|
|
137
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
136
138
|
:placeholder="t('modals.id.newPassword.placeholder.2')"
|
|
137
139
|
:error="getError('password_confirmation')"
|
|
138
140
|
/>
|
|
@@ -168,6 +168,7 @@ watch(registrationModal, (value) => {
|
|
|
168
168
|
size="lg"
|
|
169
169
|
autocomplete="email"
|
|
170
170
|
:start-icon="AIconMail"
|
|
171
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
171
172
|
:placeholder="t('modals.id.register.placeholders.email')"
|
|
172
173
|
:error="getError('email')"
|
|
173
174
|
/>
|
|
@@ -183,6 +184,7 @@ watch(registrationModal, (value) => {
|
|
|
183
184
|
size="lg"
|
|
184
185
|
autocomplete="new-password"
|
|
185
186
|
:start-icon="AIconLock"
|
|
187
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
186
188
|
:placeholder="t('modals.id.register.placeholders.password')"
|
|
187
189
|
:error="getError('password')"
|
|
188
190
|
/>
|
|
@@ -200,6 +202,7 @@ watch(registrationModal, (value) => {
|
|
|
200
202
|
size="lg"
|
|
201
203
|
autocomplete="new-password"
|
|
202
204
|
:start-icon="AIconLock"
|
|
205
|
+
start-icon-class="size-4 text-icon-secondary"
|
|
203
206
|
:placeholder="t('modals.id.register.placeholders.password_confirmation')"
|
|
204
207
|
:error="getError('password_confirmation')"
|
|
205
208
|
/>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends NavigationItem">
|
|
2
|
+
import type { NavigationItem } from './types'
|
|
2
3
|
import Avatar from '#adata-ui/icons/avatar.vue'
|
|
3
4
|
import IconLogout from '#adata-ui/icons/logout.vue'
|
|
4
5
|
import { useUrls } from '#adata-ui/composables/useUrls'
|
|
@@ -9,6 +10,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
9
10
|
balance: 0,
|
|
10
11
|
rate: 'Базовый',
|
|
11
12
|
showLogIn: true,
|
|
13
|
+
columns: 3,
|
|
12
14
|
})
|
|
13
15
|
const emit = defineEmits(['update:modelValue', 'logout'])
|
|
14
16
|
const { t } = useI18n()
|
|
@@ -17,6 +19,7 @@ const urls = useUrls()
|
|
|
17
19
|
|
|
18
20
|
interface Props {
|
|
19
21
|
grow?: boolean
|
|
22
|
+
columns?: 3 | 4
|
|
20
23
|
items: T[]
|
|
21
24
|
modelValue: string | null
|
|
22
25
|
replenish?: string
|
|
@@ -57,13 +60,16 @@ const profile: string = 'modals.mobile_navigation.type_profile'
|
|
|
57
60
|
|
|
58
61
|
<template>
|
|
59
62
|
<header class="mx-3 mb-3 overflow-hidden rounded-full border border-default bg-main/10 shadow-lg backdrop-blur-lg backdrop-saturate-140">
|
|
60
|
-
<div
|
|
63
|
+
<div
|
|
64
|
+
class="grid w-full justify-items-center"
|
|
65
|
+
:class="columns === 4 ? 'grid-cols-4' : 'grid-cols-3'"
|
|
66
|
+
>
|
|
61
67
|
<slot>
|
|
62
68
|
<button
|
|
63
|
-
v-for="
|
|
64
|
-
:key="
|
|
69
|
+
v-for="item in items"
|
|
70
|
+
:key="item.value"
|
|
65
71
|
class="h-16 w-auto min-w-16 max-w-[168px] lg:hover:bg-brand-subtle lg:hover:text-link-hover"
|
|
66
|
-
:class="{ grow, 'text-brand': modelValue === item.value }"
|
|
72
|
+
:class="{ grow, 'text-brand': modelValue === item.value || item.active }"
|
|
67
73
|
@click="onClick(item.value)"
|
|
68
74
|
>
|
|
69
75
|
<span class="flex flex-col items-center justify-center text-body-sm lg:text-body-md">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { NavigationItem } from '#adata-ui/components/navigation/bottom-navigation/types'
|
|
2
3
|
import type { ProjectKeys } from '#adata-ui/components/navigation/header/types'
|
|
3
4
|
import ContactsMobileModel from '#adata-ui/components/modals/ContactsMobileModel.vue'
|
|
4
5
|
import ContentNavigationModal from '#adata-ui/components/modals/ContentNavigationModal.vue'
|
|
@@ -21,6 +22,7 @@ interface Props {
|
|
|
21
22
|
showLogIn?: boolean
|
|
22
23
|
module?: ProjectKeys
|
|
23
24
|
oldVersion?: string
|
|
25
|
+
extraItem?: NavigationItem
|
|
24
26
|
}
|
|
25
27
|
const props = withDefaults(defineProps<Props>(), {
|
|
26
28
|
limitRemaining: 0,
|
|
@@ -29,8 +31,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
29
31
|
rate: 'Базовый',
|
|
30
32
|
showLogIn: true,
|
|
31
33
|
module: 'pk',
|
|
34
|
+
extraItem: undefined,
|
|
32
35
|
})
|
|
33
|
-
const emit = defineEmits
|
|
36
|
+
const emit = defineEmits<{
|
|
37
|
+
logout: []
|
|
38
|
+
select: [value: string]
|
|
39
|
+
}>()
|
|
34
40
|
useCurrentModule().value = props.module
|
|
35
41
|
const { landing } = useUrls()
|
|
36
42
|
const activeModule = ref<string | null>(null)
|
|
@@ -38,24 +44,34 @@ const isMain = ref(false)
|
|
|
38
44
|
const isContacts = ref(false)
|
|
39
45
|
const isProfile = ref(false)
|
|
40
46
|
const { t } = useI18n()
|
|
41
|
-
const items = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
const items = computed<NavigationItem[]>(() => {
|
|
48
|
+
const baseItems: NavigationItem[] = [
|
|
49
|
+
{
|
|
50
|
+
label: 'modals.mobile_navigation.type_tariff',
|
|
51
|
+
icon: IconInvoice,
|
|
52
|
+
value: 'tariff',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
// с четвёртым пунктом «Продукты и решения» не влезает
|
|
56
|
+
label: props.extraItem
|
|
57
|
+
? 'modals.mobile_navigation.type_main_short'
|
|
58
|
+
: 'modals.mobile_navigation.type_main',
|
|
59
|
+
icon: IconMenu,
|
|
60
|
+
value: 'main',
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
return props.extraItem ? [props.extraItem, ...baseItems] : baseItems
|
|
64
|
+
})
|
|
53
65
|
function onLogout() {
|
|
54
66
|
emit('logout')
|
|
55
67
|
isProfile.value = false
|
|
56
68
|
}
|
|
57
69
|
watch(activeModule, (e) => {
|
|
58
|
-
if (e
|
|
70
|
+
if (!e) return
|
|
71
|
+
if (e === props.extraItem?.value) {
|
|
72
|
+
emit('select', e)
|
|
73
|
+
}
|
|
74
|
+
else if (e === 'main') {
|
|
59
75
|
isMain.value = true
|
|
60
76
|
}
|
|
61
77
|
else if (e === 'contacts') {
|
|
@@ -103,6 +119,7 @@ watch(activeModule, (e) => {
|
|
|
103
119
|
:rate="rate"
|
|
104
120
|
:balance="balance"
|
|
105
121
|
:items="items"
|
|
122
|
+
:columns="extraItem ? 4 : 3"
|
|
106
123
|
@logout="onLogout"
|
|
107
124
|
/>
|
|
108
125
|
</div>
|
package/lang/en.ts
CHANGED
package/lang/kk.ts
CHANGED
package/lang/ru.ts
CHANGED
package/nuxt.config.ts
CHANGED
|
@@ -3,6 +3,30 @@ import { createResolver } from '@nuxt/kit'
|
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite'
|
|
4
4
|
|
|
5
5
|
const { resolve } = createResolver(import.meta.url)
|
|
6
|
+
|
|
7
|
+
// Стенд: `adtdev` | `adada` | `adata`. То же значение, из которого `useUrls()`
|
|
8
|
+
// собирает адреса сервисов. `NUXT_PUBLIC_MODE` — предпочтительное имя (по нему
|
|
9
|
+
// же Nuxt переопределяет `runtimeConfig.public.mode`); `MODE` оставлен для
|
|
10
|
+
// консьюмеров слоя, которые ещё не переименовали переменную.
|
|
11
|
+
const mode = process.env.NUXT_PUBLIC_MODE ?? process.env.MODE
|
|
12
|
+
|
|
13
|
+
// Тема живёт в куке `colorMode` и должна быть одна на все поддомены
|
|
14
|
+
// (`<mode>.kz`, `pk.<mode>.kz`, `avto.<mode>.kz`, `id.<mode>.kz` …), поэтому
|
|
15
|
+
// домен куки считаем из `mode`, а не держим отдельной переменной окружения:
|
|
16
|
+
// `COLOR_MODE_COOKIE_DOMAIN` жила своей жизнью и её просто забывали — из 12
|
|
17
|
+
// консьюмеров слоя она была прописана только в pk-frontend, в
|
|
18
|
+
// adata-main-front-v2 не во всех `.env`, в avto-front её не было вообще. Чем
|
|
19
|
+
// именно это ломало синхронизацию — см. `server/middleware/color-mode-cookie.ts`,
|
|
20
|
+
// он же разово вычищает host-only куки, выставленные до этого фикса.
|
|
21
|
+
//
|
|
22
|
+
// В dev домен не ставим: на localhost браузер выбросит куку с
|
|
23
|
+
// `Domain=.adtdev.kz`, и переключатель темы перестанет переживать перезагрузку.
|
|
24
|
+
// `nuxt build` идёт с `NODE_ENV=production`, `nuxt dev` — с `development`, так
|
|
25
|
+
// что сборка для любого стенда (dev/staging/master) домен получает.
|
|
26
|
+
const colorModeCookieDomain = process.env.NODE_ENV === 'production' && mode
|
|
27
|
+
? `.${mode}.kz`
|
|
28
|
+
: undefined
|
|
29
|
+
|
|
6
30
|
export default defineNuxtConfig({
|
|
7
31
|
|
|
8
32
|
app: {
|
|
@@ -43,7 +67,7 @@ export default defineNuxtConfig({
|
|
|
43
67
|
cookieAttrs: {
|
|
44
68
|
path: '/',
|
|
45
69
|
maxAge: 60 * 60 * 24 * 365,
|
|
46
|
-
domain:
|
|
70
|
+
domain: colorModeCookieDomain,
|
|
47
71
|
},
|
|
48
72
|
},
|
|
49
73
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Разовая миграция куки `colorMode`: убираем host-only дубль.
|
|
3
|
+
*
|
|
4
|
+
* Пока домен куки приезжал отдельной переменной `COLOR_MODE_COOKIE_DOMAIN`
|
|
5
|
+
* (её прописали далеко не везде — см. комментарий к `colorMode` в
|
|
6
|
+
* `nuxt.config.ts`), часть консьюмеров слоя писала `colorMode` без `Domain`, то
|
|
7
|
+
* есть host-only на свой поддомен. Причём писала на КАЖДОЙ загрузке страницы:
|
|
8
|
+
* `setPreferenceToStorage()` в клиентском плагине @nuxtjs/color-mode вызывается
|
|
9
|
+
* из `watch(preference, { immediate: true })`, а не только по клику. Поэтому
|
|
10
|
+
* рядом с доменной кукой `.<mode>.kz` от соседнего сервиса в браузере
|
|
11
|
+
* оказываются ДВЕ куки с именем `colorMode`, и браузер отдаёт обе.
|
|
12
|
+
*
|
|
13
|
+
* Дальше ломается сам модуль:
|
|
14
|
+
* 1. Его inline-скрипт в `<head>` читает куку как `split('; colorMode=')` и
|
|
15
|
+
* возвращает значение только при одном вхождении. При двух — `undefined`,
|
|
16
|
+
* то есть откат на `preference`, у нас это `light`.
|
|
17
|
+
* 2. Состояние с SSR помечено `unknown: true`, и из-за этого флага хук
|
|
18
|
+
* `app:mounted` затирает `colorMode.preference` значением из
|
|
19
|
+
* inline-скрипта — тем самым `light` — и сохраняет его в куку.
|
|
20
|
+
*
|
|
21
|
+
* Итог: заход на «сломанный» поддомен сбрасывал тему в светлую сразу для всех
|
|
22
|
+
* остальных, потому что доменная кука общая. Новая сборка сама это не вылечит:
|
|
23
|
+
* host-only кука уже стоит в браузере и живёт год с последнего до-фиксного
|
|
24
|
+
* визита.
|
|
25
|
+
*
|
|
26
|
+
* Чистим здесь, а не в клиентском плагине, по двум причинам. Во-первых,
|
|
27
|
+
* `Set-Cookie` из ответа браузер применяет до того, как выполнит inline-скрипт
|
|
28
|
+
* в `<head>`, поэтому корректной оказывается уже первая загрузка — без вспышки
|
|
29
|
+
* светлой темы и без перезаписи общей куки. Во-вторых, здесь не нужно трогать
|
|
30
|
+
* внутренности модуля (`window.__NUXT_COLOR_MODE__`).
|
|
31
|
+
*
|
|
32
|
+
* `Set-Cookie` без `Domain` адресует ровно host-only запись — доменную такой
|
|
33
|
+
* набор атрибутов не задевает.
|
|
34
|
+
*
|
|
35
|
+
* Мидлвар можно снести, когда истекут куки, выставленные до фикса.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const KEY = 'colorMode'
|
|
39
|
+
|
|
40
|
+
export default defineEventHandler((event) => {
|
|
41
|
+
const cookieHeader = getRequestHeader(event, 'cookie')
|
|
42
|
+
|
|
43
|
+
if (!cookieHeader) return
|
|
44
|
+
|
|
45
|
+
// Только документы: `Set-Cookie` на ответе статики или API нам не нужен, а на
|
|
46
|
+
// ответах с `cache-control: public` (такие route rules есть у консьюмеров) он
|
|
47
|
+
// ещё и попал бы в общий кеш.
|
|
48
|
+
if (!getRequestHeader(event, 'accept')?.includes('text/html')) return
|
|
49
|
+
|
|
50
|
+
const duplicated = cookieHeader
|
|
51
|
+
.split(';')
|
|
52
|
+
.filter(pair => pair.trimStart().startsWith(`${KEY}=`))
|
|
53
|
+
.length > 1
|
|
54
|
+
|
|
55
|
+
if (!duplicated) return
|
|
56
|
+
|
|
57
|
+
setCookie(event, KEY, '', { path: '/', maxAge: 0 })
|
|
58
|
+
})
|