feat: nav sidebar user card — real data + subscription info

- Show real avatar, full name, email from useAuthContext
- Fetch GET /subscriptions/subscriptions/active/ on mount
- Display subscription plan name (label: success=active, warning=trial, default=none)
- Show end date + days left text
- LinearProgress bar showing days remaining (red <20%, yellow <50%, green otherwise)
- Trial subscription displayed as 'Пробный: <plan>'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dev Server 2026-03-09 17:48:54 +03:00
parent a62f53fd96
commit da3736e131
1 changed files with 129 additions and 103 deletions

View File

@ -1,141 +1,167 @@
import { m } from 'framer-motion';
import { useState, useEffect } from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import { alpha as hexAlpha } from '@mui/material/styles';
import { paths } from 'src/routes/paths';
import LinearProgress from '@mui/material/LinearProgress';
import { CONFIG } from 'src/config-global';
import { varAlpha, bgGradient } from 'src/theme/styles';
import { useAuthContext } from 'src/auth/hooks';
import { Label } from 'src/components/label';
import { useMockedUser } from 'src/auth/hooks';
// ----------------------------------------------------------------------
async function fetchActiveSubscription() {
try {
const token =
localStorage.getItem('jwt_access_token') || localStorage.getItem('access_token') || '';
const base = (CONFIG.site.serverUrl || '').replace(/\/+$/, '');
const res = await fetch(`${base}/subscriptions/subscriptions/active/`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
// ----------------------------------------------------------------------
export function NavUpgrade({ sx, ...other }) {
const { user } = useMockedUser();
const { user } = useAuthContext();
const [sub, setSub] = useState(undefined); // undefined = loading, null = no sub
useEffect(() => {
if (!user) return;
fetchActiveSubscription().then(setSub);
}, [user]);
const displayName = user
? `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email
: '';
const initials = user
? [user.first_name?.[0], user.last_name?.[0]].filter(Boolean).join('').toUpperCase() ||
(user.email?.[0] || '?').toUpperCase()
: '?';
const avatarSrc = user?.avatar
? user.avatar.startsWith('http')
? user.avatar
: `${(CONFIG.site.serverUrl || '').replace(/\/api\/?$/, '')}${user.avatar}`
: null;
// Subscription label
let labelColor = 'default';
let labelText = 'Нет подписки';
if (sub === undefined) {
labelText = '…';
} else if (sub && sub.is_active_now) {
const planName = sub.plan?.name || 'Подписка';
const status = sub.status;
labelText = status === 'trial' ? `Пробный: ${planName}` : planName;
labelColor = status === 'trial' ? 'warning' : 'success';
}
// End date display
let endDateText = null;
if (sub && sub.is_active_now) {
const endField = sub.status === 'trial' ? sub.trial_end_date : sub.end_date;
if (endField) {
const date = new Date(endField);
endDateText = `до ${date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' })}`;
}
if (sub.days_left !== undefined && sub.days_left !== null) {
endDateText = endDateText ? `${endDateText} (${sub.days_left} дн.)` : `${sub.days_left} дн.`;
}
}
// Progress bar for days left (out of 30)
const daysProgress =
sub?.days_left != null && sub?.is_active_now
? Math.min(100, Math.round((sub.days_left / 30) * 100))
: null;
return (
<Stack sx={{ px: 2, py: 5, textAlign: 'center', ...sx }} {...other}>
<Stack alignItems="center">
<Box sx={{ position: 'relative' }}>
<Avatar src={user?.photoURL} alt={user?.displayName} sx={{ width: 48, height: 48 }}>
{user?.displayName?.charAt(0).toUpperCase()}
</Avatar>
<Label
color="success"
variant="filled"
sx={{
top: -6,
px: 0.5,
left: 40,
height: 20,
position: 'absolute',
borderBottomLeftRadius: 2,
}}
<Stack
sx={{
px: 2,
py: 2.5,
borderTop: '1px solid',
borderColor: 'var(--layout-nav-border-color)',
...sx,
}}
{...other}
>
{/* Avatar + name + email */}
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1.5 }}>
<Box sx={{ position: 'relative', flexShrink: 0 }}>
<Avatar
src={avatarSrc}
alt={displayName}
sx={{ width: 40, height: 40, fontSize: 15, bgcolor: 'primary.main' }}
>
Free
</Label>
{initials}
</Avatar>
</Box>
<Stack spacing={0.5} sx={{ mb: 2, mt: 1.5, width: 1 }}>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography
variant="subtitle2"
noWrap
sx={{ color: 'var(--layout-nav-text-primary-color)' }}
sx={{ color: 'var(--layout-nav-text-primary-color)', lineHeight: 1.3 }}
>
{user?.displayName}
{displayName || '—'}
</Typography>
<Typography
variant="body2"
variant="caption"
noWrap
sx={{ color: 'var(--layout-nav-text-disabled-color)' }}
sx={{ color: 'var(--layout-nav-text-disabled-color)', display: 'block' }}
>
{user?.email}
{user?.email || ''}
</Typography>
</Box>
</Stack>
{/* Subscription badge */}
<Stack spacing={0.75}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Label color={labelColor} variant="soft" sx={{ fontSize: 11 }}>
{labelText}
</Label>
{endDateText && (
<Typography
variant="caption"
noWrap
sx={{ color: 'var(--layout-nav-text-disabled-color)', fontSize: 10 }}
>
{endDateText}
</Typography>
)}
</Stack>
<Button variant="contained" href={paths.minimalStore} target="_blank" rel="noopener">
Upgrade to Pro
</Button>
{daysProgress !== null && (
<Tooltip title={`Осталось ${sub.days_left} дн.`} placement="top">
<LinearProgress
variant="determinate"
value={daysProgress}
color={daysProgress < 20 ? 'error' : daysProgress < 50 ? 'warning' : 'success'}
sx={{ height: 4, borderRadius: 2 }}
/>
</Tooltip>
)}
</Stack>
</Stack>
);
}
// ----------------------------------------------------------------------
// UpgradeBlock оставляем для совместимости, больше не используется в платформе
export function UpgradeBlock({ sx, ...other }) {
return (
<Stack
sx={{
...bgGradient({
color: `135deg, ${hexAlpha('#F7BB95', 0.92)}, ${hexAlpha('#5B2FF3', 0.92)}`,
imgUrl: `${CONFIG.site.basePath}/assets/background/background-7.webp`,
}),
px: 3,
py: 4,
borderRadius: 2,
position: 'relative',
...sx,
}}
{...other}
>
<Box
sx={{
top: 0,
left: 0,
width: 1,
height: 1,
borderRadius: 2,
position: 'absolute',
border: (theme) => `solid 3px ${varAlpha(theme.vars.palette.common.whiteChannel, 0.16)}`,
}}
/>
<Box
component={m.img}
animate={{ y: [12, -12, 12] }}
transition={{
duration: 8,
ease: 'linear',
repeat: Infinity,
repeatDelay: 0,
}}
alt="Small Rocket"
src={`${CONFIG.site.basePath}/assets/illustrations/illustration-rocket-small.webp`}
sx={{ right: 0, width: 112, height: 112, position: 'absolute' }}
/>
<Stack alignItems="flex-start" sx={{ position: 'relative' }}>
<Box component="span" sx={{ typography: 'h5', color: 'common.white' }}>
35% OFF
</Box>
<Box
component="span"
sx={{
mb: 2,
mt: 0.5,
color: 'common.white',
typography: 'subtitle2',
}}
>
Power up Productivity!
</Box>
<Button variant="contained" size="small" color="warning">
Upgrade to Pro
</Button>
</Stack>
</Stack>
);
return <Box sx={sx} {...other} />;
}