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:
parent
a62f53fd96
commit
da3736e131
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue