mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(dashboard): приветствие без пустого имени для email-пользователей
This commit is contained in:
@@ -250,6 +250,7 @@
|
|||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"welcome": "Welcome, {{name}}!",
|
"welcome": "Welcome, {{name}}!",
|
||||||
|
"welcomeNoName": "Welcome!",
|
||||||
"yourSubscription": "Your Subscription",
|
"yourSubscription": "Your Subscription",
|
||||||
"quickActions": "Quick Actions",
|
"quickActions": "Quick Actions",
|
||||||
"viewSubscription": "Manage Subscription",
|
"viewSubscription": "Manage Subscription",
|
||||||
|
|||||||
@@ -235,6 +235,7 @@
|
|||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "داشبورد",
|
"title": "داشبورد",
|
||||||
"welcome": "خوش آمدید، {{name}}!",
|
"welcome": "خوش آمدید، {{name}}!",
|
||||||
|
"welcomeNoName": "خوش آمدید!",
|
||||||
"yourSubscription": "اشتراک شما",
|
"yourSubscription": "اشتراک شما",
|
||||||
"quickActions": "دسترسی سریع",
|
"quickActions": "دسترسی سریع",
|
||||||
"viewSubscription": "مدیریت اشتراک",
|
"viewSubscription": "مدیریت اشتراک",
|
||||||
|
|||||||
@@ -253,6 +253,7 @@
|
|||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Личный кабинет",
|
"title": "Личный кабинет",
|
||||||
"welcome": "Добро пожаловать, {{name}}!",
|
"welcome": "Добро пожаловать, {{name}}!",
|
||||||
|
"welcomeNoName": "Добро пожаловать!",
|
||||||
"yourSubscription": "Ваша подписка",
|
"yourSubscription": "Ваша подписка",
|
||||||
"quickActions": "Быстрые действия",
|
"quickActions": "Быстрые действия",
|
||||||
"viewSubscription": "Управление подпиской",
|
"viewSubscription": "Управление подпиской",
|
||||||
|
|||||||
@@ -235,6 +235,7 @@
|
|||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "个人中心",
|
"title": "个人中心",
|
||||||
"welcome": "欢迎,{{name}}!",
|
"welcome": "欢迎,{{name}}!",
|
||||||
|
"welcomeNoName": "欢迎!",
|
||||||
"yourSubscription": "您的订阅",
|
"yourSubscription": "您的订阅",
|
||||||
"quickActions": "快捷操作",
|
"quickActions": "快捷操作",
|
||||||
"viewSubscription": "管理订阅",
|
"viewSubscription": "管理订阅",
|
||||||
|
|||||||
@@ -251,12 +251,14 @@ export default function Dashboard() {
|
|||||||
setShowOnboarding(false);
|
setShowOnboarding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userName = displayName(user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div data-onboarding="welcome">
|
<div data-onboarding="welcome">
|
||||||
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">
|
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">
|
||||||
{t('dashboard.welcome', { name: displayName(user) })}
|
{userName ? t('dashboard.welcome', { name: userName }) : t('dashboard.welcomeNoName')}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
<p className="text-dark-400">{t('dashboard.yourSubscription')}</p>
|
<p className="text-dark-400">{t('dashboard.yourSubscription')}</p>
|
||||||
@@ -328,40 +330,37 @@ export default function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Subscription Status Card — hidden in multi-tariff (managed via /subscriptions) */}
|
{/* Subscription Status Card — hidden in multi-tariff (managed via /subscriptions) */}
|
||||||
{!isMultiTariff && (
|
{!isMultiTariff &&
|
||||||
<>
|
(subLoading ? (
|
||||||
{subLoading ? (
|
<div className="bento-card">
|
||||||
<div className="bento-card">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="skeleton h-5 w-20" />
|
||||||
<div className="skeleton h-5 w-20" />
|
<div className="skeleton h-6 w-16 rounded-full" />
|
||||||
<div className="skeleton h-6 w-16 rounded-full" />
|
|
||||||
</div>
|
|
||||||
<div className="skeleton mb-3 h-10 w-32" />
|
|
||||||
<div className="skeleton mb-3 h-4 w-40" />
|
|
||||||
<div className="skeleton h-3 w-full rounded-full" />
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="skeleton h-12 w-full rounded-xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : subscription?.is_expired ||
|
<div className="skeleton mb-3 h-10 w-32" />
|
||||||
subscription?.status === 'disabled' ||
|
<div className="skeleton mb-3 h-4 w-40" />
|
||||||
subscription?.is_limited ? (
|
<div className="skeleton h-3 w-full rounded-full" />
|
||||||
<SubscriptionCardExpired
|
<div className="mt-5">
|
||||||
subscription={subscription}
|
<div className="skeleton h-12 w-full rounded-xl" />
|
||||||
balanceKopeks={balanceData?.balance_kopeks ?? 0}
|
</div>
|
||||||
balanceRubles={balanceData?.balance_rubles ?? 0}
|
</div>
|
||||||
/>
|
) : subscription?.is_expired ||
|
||||||
) : subscription ? (
|
subscription?.status === 'disabled' ||
|
||||||
<SubscriptionCardActive
|
subscription?.is_limited ? (
|
||||||
subscription={subscription}
|
<SubscriptionCardExpired
|
||||||
trafficData={trafficData}
|
subscription={subscription}
|
||||||
refreshTrafficMutation={refreshTrafficMutation}
|
balanceKopeks={balanceData?.balance_kopeks ?? 0}
|
||||||
trafficRefreshCooldown={trafficRefreshCooldown}
|
balanceRubles={balanceData?.balance_rubles ?? 0}
|
||||||
connectedDevices={devicesData?.total ?? 0}
|
/>
|
||||||
/>
|
) : subscription ? (
|
||||||
) : null}
|
<SubscriptionCardActive
|
||||||
</>
|
subscription={subscription}
|
||||||
)}
|
trafficData={trafficData}
|
||||||
|
refreshTrafficMutation={refreshTrafficMutation}
|
||||||
|
trafficRefreshCooldown={trafficRefreshCooldown}
|
||||||
|
connectedDevices={devicesData?.total ?? 0}
|
||||||
|
/>
|
||||||
|
) : null)}
|
||||||
|
|
||||||
{/* Нет подписок: показываем триал (если доступен) и ВСЕГДА одну явную
|
{/* Нет подписок: показываем триал (если доступен) и ВСЕГДА одну явную
|
||||||
кнопку покупки. Триал не обязателен, чтобы попасть в витрину — раньше
|
кнопку покупки. Триал не обязателен, чтобы попасть в витрину — раньше
|
||||||
|
|||||||
51
src/utils/displayName.test.ts
Normal file
51
src/utils/displayName.test.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { displayName } from './displayName';
|
||||||
|
|
||||||
|
describe('displayName', () => {
|
||||||
|
it('returns empty string for missing user', () => {
|
||||||
|
expect(displayName(undefined)).toBe('');
|
||||||
|
expect(displayName(null)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combines first_name and last_name', () => {
|
||||||
|
expect(displayName({ first_name: 'Иван', last_name: 'Петров' })).toBe('Иван Петров');
|
||||||
|
expect(displayName({ first_name: 'Иван', last_name: null })).toBe('Иван');
|
||||||
|
expect(displayName({ first_name: null, last_name: 'Петров' })).toBe('Петров');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to username when there is no name', () => {
|
||||||
|
expect(displayName({ first_name: null, last_name: null, username: 'ivan' })).toBe('ivan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to telegram_id when there is no name and no username', () => {
|
||||||
|
expect(displayName({ first_name: null, username: null, telegram_id: 123456 })).toBe('#123456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the email local part for email-only users', () => {
|
||||||
|
expect(
|
||||||
|
displayName({
|
||||||
|
first_name: null,
|
||||||
|
last_name: null,
|
||||||
|
username: null,
|
||||||
|
telegram_id: null,
|
||||||
|
email: 'vasya@gmail.com',
|
||||||
|
}),
|
||||||
|
).toBe('vasya');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers telegram_id over email', () => {
|
||||||
|
expect(displayName({ telegram_id: 123456, email: 'vasya@gmail.com' })).toBe('#123456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string when every source is empty', () => {
|
||||||
|
expect(
|
||||||
|
displayName({
|
||||||
|
first_name: ' ',
|
||||||
|
last_name: null,
|
||||||
|
username: null,
|
||||||
|
telegram_id: null,
|
||||||
|
email: null,
|
||||||
|
}),
|
||||||
|
).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,21 +3,28 @@
|
|||||||
*
|
*
|
||||||
* Why: single-letter first_name (e.g., "О") looked confusing alone ("Добро пожаловать, О!").
|
* Why: single-letter first_name (e.g., "О") looked confusing alone ("Добро пожаловать, О!").
|
||||||
* Combining with last_name makes truncated/short first names readable.
|
* Combining with last_name makes truncated/short first names readable.
|
||||||
|
*
|
||||||
|
* Email/OAuth registration does not collect a name, so for such users every
|
||||||
|
* Telegram-derived field is null — fall back to the email local part
|
||||||
|
* ("vasya@gmail.com" → "vasya") to avoid an empty name in greetings.
|
||||||
*/
|
*/
|
||||||
export interface NameSource {
|
export interface NameSource {
|
||||||
first_name?: string | null;
|
first_name?: string | null;
|
||||||
last_name?: string | null;
|
last_name?: string | null;
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
telegram_id?: number | null;
|
telegram_id?: number | null;
|
||||||
|
email?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayName(user?: NameSource | null): string {
|
export function displayName(user?: NameSource | null): string {
|
||||||
if (!user) return '';
|
if (!user) return '';
|
||||||
const fullName = [user.first_name, user.last_name]
|
const fullName = [user.first_name, user.last_name]
|
||||||
.filter((part): part is string => Boolean(part && part.trim()))
|
.filter((part): part is string => Boolean(part?.trim()))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
if (fullName) return fullName;
|
if (fullName) return fullName;
|
||||||
if (user.username) return user.username;
|
if (user.username) return user.username;
|
||||||
if (user.telegram_id) return `#${user.telegram_id}`;
|
if (user.telegram_id) return `#${user.telegram_id}`;
|
||||||
|
const emailLocalPart = user.email?.trim().split('@')[0];
|
||||||
|
if (emailLocalPart) return emailLocalPart;
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user