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 Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { alpha as hexAlpha } from '@mui/material/styles';
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
|
||||||
import { paths } from 'src/routes/paths';
|
|
||||||
|
|
||||||
import { CONFIG } from 'src/config-global';
|
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 { 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 }) {
|
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 (
|
return (
|
||||||
<Stack sx={{ px: 2, py: 5, textAlign: 'center', ...sx }} {...other}>
|
<Stack
|
||||||
<Stack alignItems="center">
|
sx={{
|
||||||
<Box sx={{ position: 'relative' }}>
|
px: 2,
|
||||||
<Avatar src={user?.photoURL} alt={user?.displayName} sx={{ width: 48, height: 48 }}>
|
py: 2.5,
|
||||||
{user?.displayName?.charAt(0).toUpperCase()}
|
borderTop: '1px solid',
|
||||||
</Avatar>
|
borderColor: 'var(--layout-nav-border-color)',
|
||||||
|
...sx,
|
||||||
<Label
|
}}
|
||||||
color="success"
|
{...other}
|
||||||
variant="filled"
|
>
|
||||||
sx={{
|
{/* Avatar + name + email */}
|
||||||
top: -6,
|
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 1.5 }}>
|
||||||
px: 0.5,
|
<Box sx={{ position: 'relative', flexShrink: 0 }}>
|
||||||
left: 40,
|
<Avatar
|
||||||
height: 20,
|
src={avatarSrc}
|
||||||
position: 'absolute',
|
alt={displayName}
|
||||||
borderBottomLeftRadius: 2,
|
sx={{ width: 40, height: 40, fontSize: 15, bgcolor: 'primary.main' }}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Free
|
{initials}
|
||||||
</Label>
|
</Avatar>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack spacing={0.5} sx={{ mb: 2, mt: 1.5, width: 1 }}>
|
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
noWrap
|
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>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="caption"
|
||||||
noWrap
|
noWrap
|
||||||
sx={{ color: 'var(--layout-nav-text-disabled-color)' }}
|
sx={{ color: 'var(--layout-nav-text-disabled-color)', display: 'block' }}
|
||||||
>
|
>
|
||||||
{user?.email}
|
{user?.email || ''}
|
||||||
</Typography>
|
</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>
|
</Stack>
|
||||||
|
|
||||||
<Button variant="contained" href={paths.minimalStore} target="_blank" rel="noopener">
|
{daysProgress !== null && (
|
||||||
Upgrade to Pro
|
<Tooltip title={`Осталось ${sub.days_left} дн.`} placement="top">
|
||||||
</Button>
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={daysProgress}
|
||||||
|
color={daysProgress < 20 ? 'error' : daysProgress < 50 ? 'warning' : 'success'}
|
||||||
|
sx={{ height: 4, borderRadius: 2 }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
// UpgradeBlock — оставляем для совместимости, больше не используется в платформе
|
||||||
export function UpgradeBlock({ sx, ...other }) {
|
export function UpgradeBlock({ sx, ...other }) {
|
||||||
return (
|
return <Box sx={sx} {...other} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue