mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 01:23:47 +00:00
fix(dashboard): приветствие без пустого имени для email-пользователей
This commit is contained in:
@@ -250,6 +250,7 @@
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"welcome": "Welcome, {{name}}!",
|
||||
"welcomeNoName": "Welcome!",
|
||||
"yourSubscription": "Your Subscription",
|
||||
"quickActions": "Quick Actions",
|
||||
"viewSubscription": "Manage Subscription",
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"dashboard": {
|
||||
"title": "داشبورد",
|
||||
"welcome": "خوش آمدید، {{name}}!",
|
||||
"welcomeNoName": "خوش آمدید!",
|
||||
"yourSubscription": "اشتراک شما",
|
||||
"quickActions": "دسترسی سریع",
|
||||
"viewSubscription": "مدیریت اشتراک",
|
||||
|
||||
@@ -253,6 +253,7 @@
|
||||
"dashboard": {
|
||||
"title": "Личный кабинет",
|
||||
"welcome": "Добро пожаловать, {{name}}!",
|
||||
"welcomeNoName": "Добро пожаловать!",
|
||||
"yourSubscription": "Ваша подписка",
|
||||
"quickActions": "Быстрые действия",
|
||||
"viewSubscription": "Управление подпиской",
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"dashboard": {
|
||||
"title": "个人中心",
|
||||
"welcome": "欢迎,{{name}}!",
|
||||
"welcomeNoName": "欢迎!",
|
||||
"yourSubscription": "您的订阅",
|
||||
"quickActions": "快捷操作",
|
||||
"viewSubscription": "管理订阅",
|
||||
|
||||
@@ -251,12 +251,14 @@ export default function Dashboard() {
|
||||
setShowOnboarding(false);
|
||||
};
|
||||
|
||||
const userName = displayName(user);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div data-onboarding="welcome">
|
||||
<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>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<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) */}
|
||||
{!isMultiTariff && (
|
||||
<>
|
||||
{subLoading ? (
|
||||
<div className="bento-card">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="skeleton h-5 w-20" />
|
||||
<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>
|
||||
{!isMultiTariff &&
|
||||
(subLoading ? (
|
||||
<div className="bento-card">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="skeleton h-5 w-20" />
|
||||
<div className="skeleton h-6 w-16 rounded-full" />
|
||||
</div>
|
||||
) : subscription?.is_expired ||
|
||||
subscription?.status === 'disabled' ||
|
||||
subscription?.is_limited ? (
|
||||
<SubscriptionCardExpired
|
||||
subscription={subscription}
|
||||
balanceKopeks={balanceData?.balance_kopeks ?? 0}
|
||||
balanceRubles={balanceData?.balance_rubles ?? 0}
|
||||
/>
|
||||
) : subscription ? (
|
||||
<SubscriptionCardActive
|
||||
subscription={subscription}
|
||||
trafficData={trafficData}
|
||||
refreshTrafficMutation={refreshTrafficMutation}
|
||||
trafficRefreshCooldown={trafficRefreshCooldown}
|
||||
connectedDevices={devicesData?.total ?? 0}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
) : subscription?.is_expired ||
|
||||
subscription?.status === 'disabled' ||
|
||||
subscription?.is_limited ? (
|
||||
<SubscriptionCardExpired
|
||||
subscription={subscription}
|
||||
balanceKopeks={balanceData?.balance_kopeks ?? 0}
|
||||
balanceRubles={balanceData?.balance_rubles ?? 0}
|
||||
/>
|
||||
) : subscription ? (
|
||||
<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 ("Добро пожаловать, О!").
|
||||
* 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 {
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
username?: string | null;
|
||||
telegram_id?: number | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export function displayName(user?: NameSource | null): string {
|
||||
if (!user) return '';
|
||||
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(' ');
|
||||
if (fullName) return fullName;
|
||||
if (user.username) return user.username;
|
||||
if (user.telegram_id) return `#${user.telegram_id}`;
|
||||
const emailLocalPart = user.email?.trim().split('@')[0];
|
||||
if (emailLocalPart) return emailLocalPart;
|
||||
return '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user