Fix dashboard mobile layout: header overflow, welcome banner, and stats grid
Hide nav icon bar and volunteer button on mobile (accessible via drawer), stack welcome banner vertically with proper nowrap, replace status bar flex row with 3-column CSS grid using MobileQuickStat component. Bunker Admin
This commit is contained in:
parent
6db44eadc6
commit
610f547dbf
@ -638,7 +638,7 @@ export default function AppLayout() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{pageHeader?.actions}
|
{pageHeader?.actions}
|
||||||
{(() => {
|
{!isMobile && (() => {
|
||||||
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
|
const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS);
|
||||||
const withOverrides = applyAdminOverrides(merged);
|
const withOverrides = applyAdminOverrides(merged);
|
||||||
const flags = buildFeatureFlags(settings);
|
const flags = buildFeatureFlags(settings);
|
||||||
@ -676,7 +676,7 @@ export default function AppLayout() {
|
|||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
>
|
>
|
||||||
<Button type="text" size="small" icon={getIcon(item.icon)}>
|
<Button type="text" size="small" icon={getIcon(item.icon)}>
|
||||||
{!isMobile && !collapsed && item.label}
|
{!collapsed && item.label}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
@ -689,23 +689,25 @@ export default function AppLayout() {
|
|||||||
icon={getIcon(item.icon)}
|
icon={getIcon(item.icon)}
|
||||||
onClick={() => handleItemClick(item)}
|
onClick={() => handleItemClick(item)}
|
||||||
>
|
>
|
||||||
{!isMobile && !collapsed && item.label}
|
{!collapsed && item.label}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
})()}
|
})()}
|
||||||
{/* Volunteer Portal button — always visible for quick switching */}
|
{/* Volunteer Portal button — always visible for quick switching */}
|
||||||
<Tooltip title="Switch to Volunteer Portal">
|
{!isMobile && (
|
||||||
<Button
|
<Tooltip title="Switch to Volunteer Portal">
|
||||||
type="text"
|
<Button
|
||||||
size="small"
|
type="text"
|
||||||
icon={<TeamOutlined />}
|
size="small"
|
||||||
onClick={() => navigate('/volunteer')}
|
icon={<TeamOutlined />}
|
||||||
>
|
onClick={() => navigate('/volunteer')}
|
||||||
{!isMobile && !collapsed && 'Volunteer'}
|
>
|
||||||
</Button>
|
{!collapsed && 'Volunteer'}
|
||||||
</Tooltip>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||||
<Button type="text" icon={<UserOutlined />} data-tour="user-menu">
|
<Button type="text" icon={<UserOutlined />} data-tour="user-menu">
|
||||||
{!isMobile && !collapsed && (
|
{!isMobile && !collapsed && (
|
||||||
|
|||||||
@ -314,17 +314,19 @@ export default function DashboardPage() {
|
|||||||
{/* === Welcome Banner === */}
|
{/* === Welcome Banner === */}
|
||||||
<Card
|
<Card
|
||||||
style={{ marginBottom: 16, background: 'linear-gradient(135deg, #1890ff 0%, #722ed1 100%)', border: 'none' }}
|
style={{ marginBottom: 16, background: 'linear-gradient(135deg, #1890ff 0%, #722ed1 100%)', border: 'none' }}
|
||||||
styles={{ body: { padding: '10px 16px' } }}
|
styles={{ body: { padding: isMobile ? '10px 12px' : '10px 16px' } }}
|
||||||
>
|
>
|
||||||
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
|
<Flex vertical={isMobile} gap={8}>
|
||||||
<Flex align="center" gap={12}>
|
<Flex justify="space-between" align="center" gap={8} style={{ minWidth: 0 }}>
|
||||||
<Text strong style={{ color: '#fff', fontSize: 16, whiteSpace: 'nowrap' }}>
|
<Flex align="center" gap={8} style={{ minWidth: 0 }}>
|
||||||
Welcome{user?.name ? `, ${user.name}` : ''}
|
<Text strong style={{ color: '#fff', fontSize: 16, whiteSpace: 'nowrap' }}>
|
||||||
</Text>
|
Welcome{user?.name ? `, ${user.name}` : ''}
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.55)', fontSize: 12 }}>
|
</Text>
|
||||||
{lastRefresh && `Updated ${lastRefresh.toLocaleTimeString()}`}
|
<Text style={{ color: 'rgba(255,255,255,0.55)', fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||||
</Text>
|
{lastRefresh && `Updated ${lastRefresh.toLocaleTimeString()}`}
|
||||||
{isSuperAdmin && homepageUrl && (
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
{!isMobile && isSuperAdmin && homepageUrl && (
|
||||||
<Segmented
|
<Segmented
|
||||||
size="small"
|
size="small"
|
||||||
value={activeView}
|
value={activeView}
|
||||||
@ -333,12 +335,24 @@ export default function DashboardPage() {
|
|||||||
{ label: 'Dashboard', value: 'dashboard', icon: <DashboardOutlined /> },
|
{ label: 'Dashboard', value: 'dashboard', icon: <DashboardOutlined /> },
|
||||||
{ label: 'Homepage', value: 'homepage', icon: <HomeOutlined /> },
|
{ label: 'Homepage', value: 'homepage', icon: <HomeOutlined /> },
|
||||||
]}
|
]}
|
||||||
style={{ background: 'rgba(255,255,255,0.2)', borderRadius: 6 }}
|
style={{ background: 'rgba(255,255,255,0.2)', borderRadius: 6, flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{isMobile && isSuperAdmin && homepageUrl && (
|
||||||
|
<Segmented
|
||||||
|
size="small"
|
||||||
|
value={activeView}
|
||||||
|
onChange={(val) => setActiveView(val as 'dashboard' | 'homepage')}
|
||||||
|
options={[
|
||||||
|
{ label: 'Dashboard', value: 'dashboard', icon: <DashboardOutlined /> },
|
||||||
|
{ label: 'Homepage', value: 'homepage', icon: <HomeOutlined /> },
|
||||||
|
]}
|
||||||
|
style={{ background: 'rgba(255,255,255,0.2)', borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{activeView === 'dashboard' && (
|
{activeView === 'dashboard' && (
|
||||||
<Flex gap={4} wrap="wrap" justify="flex-end" data-tour-dashboard-actions>
|
<Flex gap={4} wrap="wrap" justify={isMobile ? 'flex-start' : 'flex-end'} data-tour-dashboard-actions>
|
||||||
{showInfluence && <Tooltip title="New Campaign"><Button type="text" icon={<PlusOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/campaigns')} /></Tooltip>}
|
{showInfluence && <Tooltip title="New Campaign"><Button type="text" icon={<PlusOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/campaigns')} /></Tooltip>}
|
||||||
{showMap && <Tooltip title="Locations"><Button type="text" icon={<EnvironmentOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/map')} /></Tooltip>}
|
{showMap && <Tooltip title="Locations"><Button type="text" icon={<EnvironmentOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/map')} /></Tooltip>}
|
||||||
{showMedia && <Tooltip title="Videos"><Button type="text" icon={<UploadOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/media/library')} /></Tooltip>}
|
{showMedia && <Tooltip title="Videos"><Button type="text" icon={<UploadOutlined style={{ color: '#fff', fontSize: 18 }} />} onClick={() => navigate('/app/media/library')} /></Tooltip>}
|
||||||
@ -433,56 +447,99 @@ export default function DashboardPage() {
|
|||||||
{/* === Status Bar (weather + stats + pending actions + connectivity) === */}
|
{/* === Status Bar (weather + stats + pending actions + connectivity) === */}
|
||||||
{summary && (
|
{summary && (
|
||||||
<Card size="small" style={{ marginBottom: 16 }} styles={{ body: { padding: isMobile ? '8px 10px' : '10px 16px' } }} data-tour-dashboard-stats>
|
<Card size="small" style={{ marginBottom: 16 }} styles={{ body: { padding: isMobile ? '8px 10px' : '10px 16px' } }} data-tour-dashboard-stats>
|
||||||
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
|
{isMobile ? (
|
||||||
<Flex gap={0} align="center" style={isMobile ? { overflowX: 'auto', maxWidth: '100%', WebkitOverflowScrolling: 'touch' } : { flexWrap: 'wrap' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
|
||||||
{weather && (
|
{weather && (
|
||||||
<Flex align="center" gap={6} style={{ padding: '0 14px 0 0', borderRight: '1px solid rgba(255,255,255,0.08)' }}>
|
<div style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 6, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
<span style={{ fontSize: 24 }}>{getWeatherIcon(weather.weatherCode, weather.isDay)}</span>
|
<span style={{ fontSize: 22 }}>{getWeatherIcon(weather.weatherCode, weather.isDay)}</span>
|
||||||
<Text strong style={{ fontSize: 16 }}>{Math.round(weather.temperature)}°C</Text>
|
<Text strong style={{ fontSize: 15 }}>{Math.round(weather.temperature)}°C</Text>
|
||||||
<Text type="secondary" style={{ fontSize: 13 }}>{weather.weatherDescription}</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>{weather.weatherDescription}</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<MobileQuickStat icon={<TeamOutlined />} color="#1890ff" value={summary.users.total} label="Users" onClick={() => navigate('/app/users')} />
|
||||||
|
{showInfluence && <MobileQuickStat icon={<SendOutlined />} color="#52c41a" value={summary.campaigns.active} label={`of ${summary.campaigns.total}`} onClick={() => navigate('/app/campaigns')} />}
|
||||||
|
{showMap && <MobileQuickStat icon={<EnvironmentOutlined />} color="#722ed1" value={summary.locations.total.toLocaleString()} label={`${geocodePct}%`} onClick={() => navigate('/app/map')} />}
|
||||||
|
{showInfluence && <MobileQuickStat icon={<MailOutlined />} color="#faad14" value={summary.emails.sent} label="sent" onClick={() => navigate('/app/email-queue')} />}
|
||||||
|
{showMedia && <MobileQuickStat icon={<VideoCameraOutlined />} color="#13c2c2" value={summary.videos.published} label={`of ${summary.videos.total}`} onClick={() => navigate('/app/media/library')} />}
|
||||||
|
{showMap && <MobileQuickStat icon={<ScheduleOutlined />} color="#eb2f96" value={summary.shifts.upcoming} label={`${summary.shifts.open} open`} onClick={() => navigate('/app/map/shifts')} />}
|
||||||
|
{/* Pending action tags */}
|
||||||
|
{(summary.responses.pending > 0 || (summary.locations.total > 0 && summary.locations.total - summary.locations.geocoded > 0) || summary.emails.queued > 0) && (
|
||||||
|
<div style={{ gridColumn: '1 / -1', display: 'flex', flexWrap: 'wrap', gap: 4, paddingTop: 4, borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
|
{summary.responses.pending > 0 && (
|
||||||
|
<Tag color="orange" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/responses')}>
|
||||||
|
{summary.responses.pending} pending
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{summary.locations.total > 0 && summary.locations.total - summary.locations.geocoded > 0 && (
|
||||||
|
<Tag color="purple" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/map')}>
|
||||||
|
{summary.locations.total - summary.locations.geocoded} ungeocoded
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{summary.emails.queued > 0 && (
|
||||||
|
<Tag color="blue" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/email-queue')}>
|
||||||
|
{summary.emails.queued} queued
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{summary.campaignModeration.pendingReview > 0 && (
|
||||||
|
<Tag color="gold" style={{ cursor: 'pointer', margin: 0 }} onClick={() => navigate('/app/campaign-moderation')}>
|
||||||
|
{summary.campaignModeration.pendingReview} review
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Flex justify="space-between" align="center" wrap="wrap" gap={8}>
|
||||||
|
<Flex gap={0} align="center" style={{ flexWrap: 'wrap' }}>
|
||||||
|
{weather && (
|
||||||
|
<Flex align="center" gap={6} style={{ padding: '0 14px 0 0', borderRight: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
|
<span style={{ fontSize: 24 }}>{getWeatherIcon(weather.weatherCode, weather.isDay)}</span>
|
||||||
|
<Text strong style={{ fontSize: 16 }}>{Math.round(weather.temperature)}°C</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>{weather.weatherDescription}</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{/* Quick stat chips */}
|
||||||
|
<QuickStat icon={<TeamOutlined />} color="#1890ff" value={summary.users.total} label="Users" onClick={() => navigate('/app/users')} />
|
||||||
|
{showInfluence && <QuickStat icon={<SendOutlined />} color="#52c41a" value={summary.campaigns.active} label={`of ${summary.campaigns.total}`} onClick={() => navigate('/app/campaigns')} />}
|
||||||
|
{showMap && <QuickStat icon={<EnvironmentOutlined />} color="#722ed1" value={summary.locations.total.toLocaleString()} label={`${geocodePct}% geo`} onClick={() => navigate('/app/map')} />}
|
||||||
|
{showInfluence && <QuickStat icon={<MailOutlined />} color="#faad14" value={summary.emails.sent} label="sent" onClick={() => navigate('/app/email-queue')} />}
|
||||||
|
{showMedia && <QuickStat icon={<VideoCameraOutlined />} color="#13c2c2" value={summary.videos.published} label={`of ${summary.videos.total}`} onClick={() => navigate('/app/media/library')} />}
|
||||||
|
{showMap && <QuickStat icon={<ScheduleOutlined />} color="#eb2f96" value={summary.shifts.upcoming} label={`${summary.shifts.open} open`} onClick={() => navigate('/app/map/shifts')} />}
|
||||||
|
{isSuperAdmin && grafanaUrl && (
|
||||||
|
<QuickStat icon={<BarChartOutlined />} color="#f5222d" value="Metrics" label="" onClick={() => window.open(`${grafanaUrl}/d/changemaker-overview?from=now-24h&to=now`, '_blank')} />
|
||||||
|
)}
|
||||||
|
{/* Pending action tags */}
|
||||||
|
{summary.responses.pending > 0 && (
|
||||||
|
<Tag color="orange" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/responses')}>
|
||||||
|
{summary.responses.pending} pending
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{summary.locations.total > 0 && summary.locations.total - summary.locations.geocoded > 0 && (
|
||||||
|
<Tag color="purple" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/map')}>
|
||||||
|
{summary.locations.total - summary.locations.geocoded} ungeocoded
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{summary.emails.queued > 0 && (
|
||||||
|
<Tag color="blue" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/email-queue')}>
|
||||||
|
{summary.emails.queued} queued
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{summary.campaignModeration.pendingReview > 0 && (
|
||||||
|
<Tag color="gold" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/campaign-moderation')}>
|
||||||
|
{summary.campaignModeration.pendingReview} review
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{isSuperAdmin && connectivity && (
|
||||||
|
<Flex gap={6} align="center" style={{ flexShrink: 0 }}>
|
||||||
|
<ConnectivityDot label="SMTP" online={connectivity.smtp} />
|
||||||
|
<ConnectivityDot label="Listmonk" online={connectivity.listmonk} />
|
||||||
|
<ConnectivityDot label="Chat" online={connectivity.rocketchat} />
|
||||||
|
<ConnectivityDot label="Events" online={connectivity.gancio} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{/* Quick stat chips */}
|
|
||||||
<QuickStat icon={<TeamOutlined />} color="#1890ff" value={summary.users.total} label="Users" onClick={() => navigate('/app/users')} />
|
|
||||||
{showInfluence && <QuickStat icon={<SendOutlined />} color="#52c41a" value={summary.campaigns.active} label={`of ${summary.campaigns.total}`} onClick={() => navigate('/app/campaigns')} />}
|
|
||||||
{showMap && <QuickStat icon={<EnvironmentOutlined />} color="#722ed1" value={summary.locations.total.toLocaleString()} label={`${geocodePct}% geo`} onClick={() => navigate('/app/map')} />}
|
|
||||||
{showInfluence && <QuickStat icon={<MailOutlined />} color="#faad14" value={summary.emails.sent} label="sent" onClick={() => navigate('/app/email-queue')} />}
|
|
||||||
{showMedia && <QuickStat icon={<VideoCameraOutlined />} color="#13c2c2" value={summary.videos.published} label={`of ${summary.videos.total}`} onClick={() => navigate('/app/media/library')} />}
|
|
||||||
{showMap && <QuickStat icon={<ScheduleOutlined />} color="#eb2f96" value={summary.shifts.upcoming} label={`${summary.shifts.open} open`} onClick={() => navigate('/app/map/shifts')} />}
|
|
||||||
{isSuperAdmin && grafanaUrl && (
|
|
||||||
<QuickStat icon={<BarChartOutlined />} color="#f5222d" value="Metrics" label="" onClick={() => window.open(`${grafanaUrl}/d/changemaker-overview?from=now-24h&to=now`, '_blank')} />
|
|
||||||
)}
|
|
||||||
{/* Pending action tags */}
|
|
||||||
{summary.responses.pending > 0 && (
|
|
||||||
<Tag color="orange" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/responses')}>
|
|
||||||
{summary.responses.pending} pending
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{summary.locations.total > 0 && summary.locations.total - summary.locations.geocoded > 0 && (
|
|
||||||
<Tag color="purple" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/map')}>
|
|
||||||
{summary.locations.total - summary.locations.geocoded} ungeocoded
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{summary.emails.queued > 0 && (
|
|
||||||
<Tag color="blue" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/email-queue')}>
|
|
||||||
{summary.emails.queued} queued
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{summary.campaignModeration.pendingReview > 0 && (
|
|
||||||
<Tag color="gold" style={{ cursor: 'pointer', margin: '0 0 0 4px' }} onClick={() => navigate('/app/campaign-moderation')}>
|
|
||||||
{summary.campaignModeration.pendingReview} review
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
{isSuperAdmin && connectivity && !isMobile && (
|
)}
|
||||||
<Flex gap={6} align="center" style={{ flexShrink: 0 }}>
|
|
||||||
<ConnectivityDot label="SMTP" online={connectivity.smtp} />
|
|
||||||
<ConnectivityDot label="Listmonk" online={connectivity.listmonk} />
|
|
||||||
<ConnectivityDot label="Chat" online={connectivity.rocketchat} />
|
|
||||||
<ConnectivityDot label="Events" online={connectivity.gancio} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -1097,6 +1154,33 @@ function QuickStat({ icon, color, value, label, onClick }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MobileQuickStat({ icon, color, value, label, onClick }: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
value: string | number;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '6px 8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 6,
|
||||||
|
background: 'rgba(255,255,255,0.04)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color, fontSize: 15 }}>{icon}</span>
|
||||||
|
<Text strong style={{ fontSize: 15 }}>{value}</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 11 }}>{label}</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ConnectivityDot({ label, online }: { label: string; online: boolean }) {
|
function ConnectivityDot({ label, online }: { label: string; online: boolean }) {
|
||||||
return (
|
return (
|
||||||
<Tooltip title={`${label}: ${online ? 'Connected' : 'Offline'}`}>
|
<Tooltip title={`${label}: ${online ? 'Connected' : 'Offline'}`}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user