mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix(dashboard): show full name on welcome, fix gender mismatch on trial-expired card
User reported single-letter welcome "Добро пожаловать, О!" (short first_name "Олег"-style users) and a confused "Истекла" feminine label on the masculine "Пробный период истёк" trial-expired card. * Add `src/utils/displayName.ts` helper that composes `first_name + last_name` with `username` and `#telegram_id` fallbacks. Single source of truth for user-facing name rendering across the app. * Apply `displayName(user)` in Dashboard welcome, AppHeader mobile drawer, and DesktopSidebar profile chip. Now a user with `first_name="О"` and `last_name="Иванов"` sees "О Иванов" instead of just "О". * `SubscriptionCardExpired` — context-aware Russian label: for trial subscriptions render masculine "Истёк" (agrees with "пробный период"), for paid subscriptions keep feminine "Истекла" (agrees with "подписка"). Uses i18next `context: subscription.is_trial ? 'trial' : ''` — falls back to base key for EN/ZH/FA which are grammatically neutral. * Add `expiredDate_trial: "Истёк"` only to `ru.json` (no changes needed for other locales — i18next context falls back to `expiredDate`).
This commit is contained in:
@@ -228,7 +228,11 @@ export default function SubscriptionCardExpired({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
<div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
||||||
{isLimited ? t('dashboard.expired.activeUntil') : t('dashboard.expired.expiredDate')}
|
{isLimited
|
||||||
|
? t('dashboard.expired.activeUntil')
|
||||||
|
: t('dashboard.expired.expiredDate', {
|
||||||
|
context: subscription.is_trial ? 'trial' : '',
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-base font-bold tracking-tight text-dark-50/50">
|
<div className="ml-3 text-base font-bold tracking-tight text-dark-50/50">
|
||||||
{formattedDate}
|
{formattedDate}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { initDataUser } from '@telegram-apps/sdk-react';
|
import { initDataUser } from '@telegram-apps/sdk-react';
|
||||||
|
|
||||||
import { useAuthStore } from '@/store/auth';
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
import { displayName } from '@/utils/displayName';
|
||||||
import { useShallow } from 'zustand/shallow';
|
import { useShallow } from 'zustand/shallow';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { usePlatform } from '@/platform';
|
import { usePlatform } from '@/platform';
|
||||||
@@ -340,9 +341,7 @@ export function AppHeader({
|
|||||||
<UserIcon className="h-5 w-5" />
|
<UserIcon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-dark-100">
|
<div className="text-sm font-medium text-dark-100">{displayName(user)}</div>
|
||||||
{user?.first_name || user?.username}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-dark-500">
|
<div className="text-xs text-dark-500">
|
||||||
@{user?.username || `ID: ${user?.telegram_id}`}
|
@{user?.username || `ID: ${user?.telegram_id}`}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useAuthStore } from '@/store/auth';
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
import { displayName } from '@/utils/displayName';
|
||||||
import {
|
import {
|
||||||
brandingApi,
|
brandingApi,
|
||||||
getCachedBranding,
|
getCachedBranding,
|
||||||
@@ -188,9 +189,7 @@ export function DesktopSidebar({
|
|||||||
<UserIcon className="h-4 w-4 text-dark-400" />
|
<UserIcon className="h-4 w-4 text-dark-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-dark-100">
|
<p className="truncate text-sm font-medium text-dark-100">{displayName(user)}</p>
|
||||||
{user?.first_name || user?.username || `#${user?.telegram_id}`}
|
|
||||||
</p>
|
|
||||||
<p className="truncate text-xs text-dark-500">
|
<p className="truncate text-xs text-dark-500">
|
||||||
@{user?.username || `ID: ${user?.telegram_id}`}
|
@{user?.username || `ID: ${user?.telegram_id}`}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"traffic": "Трафик",
|
"traffic": "Трафик",
|
||||||
"devices": "Устройства",
|
"devices": "Устройства",
|
||||||
"expiredDate": "Истекла",
|
"expiredDate": "Истекла",
|
||||||
|
"expiredDate_trial": "Истёк",
|
||||||
"activeUntil": "Активна до"
|
"activeUntil": "Активна до"
|
||||||
},
|
},
|
||||||
"suspended": {
|
"suspended": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { Link, useNavigate } from 'react-router';
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuthStore } from '../store/auth';
|
import { useAuthStore } from '../store/auth';
|
||||||
|
import { displayName } from '../utils/displayName';
|
||||||
import { useBlockingStore } from '../store/blocking';
|
import { useBlockingStore } from '../store/blocking';
|
||||||
import { subscriptionApi } from '../api/subscription';
|
import { subscriptionApi } from '../api/subscription';
|
||||||
import { referralApi } from '../api/referral';
|
import { referralApi } from '../api/referral';
|
||||||
@@ -254,7 +255,7 @@ export default function Dashboard() {
|
|||||||
{/* 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: user?.first_name || user?.username || '' })}
|
{t('dashboard.welcome', { name: displayName(user) })}
|
||||||
</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>
|
||||||
|
|||||||
23
src/utils/displayName.ts
Normal file
23
src/utils/displayName.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Composes a user-facing display name from first_name + last_name with sensible fallbacks.
|
||||||
|
*
|
||||||
|
* Why: single-letter first_name (e.g., "О") looked confusing alone ("Добро пожаловать, О!").
|
||||||
|
* Combining with last_name makes truncated/short first names readable.
|
||||||
|
*/
|
||||||
|
export interface NameSource {
|
||||||
|
first_name?: string | null;
|
||||||
|
last_name?: string | null;
|
||||||
|
username?: string | null;
|
||||||
|
telegram_id?: number | 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()))
|
||||||
|
.join(' ');
|
||||||
|
if (fullName) return fullName;
|
||||||
|
if (user.username) return user.username;
|
||||||
|
if (user.telegram_id) return `#${user.telegram_id}`;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user