mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Enhance Dashboard and Layout Styles
- Updated opacity and blur effects for background elements in DashboardPage. - Adjusted radial gradient color mixes for visual consistency. - Increased font sizes and modified letter spacing for improved readability in hero sections. - Implemented a new glassmorphism effect with backdrop filters in various components. - Refined sidebar styles, including width adjustments and hover effects. - Enhanced header styles for better responsiveness and visual hierarchy. - Introduced new gradient backgrounds and box shadows for a modern look. - Improved mobile responsiveness with adjusted dimensions and layout properties.
This commit is contained in:
@@ -207,8 +207,6 @@ export function MainLayout() {
|
||||
const { showNotification } = useNotificationStore();
|
||||
const location = useLocation();
|
||||
|
||||
const apiBase = useAuthStore((state) => state.apiBase);
|
||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
|
||||
const config = useConfigStore((state) => state.config);
|
||||
@@ -224,18 +222,17 @@ export function MainLayout() {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [languageMenuOpen, setLanguageMenuOpen] = useState(false);
|
||||
const [themeMenuOpen, setThemeMenuOpen] = useState(false);
|
||||
const [brandExpanded, setBrandExpanded] = useState(true);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const languageMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const themeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const headerRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const fullBrandName = 'CLI Proxy API Management Center';
|
||||
const abbrBrandName = t('title.abbr');
|
||||
const isLogsPage = location.pathname.startsWith('/logs');
|
||||
const showSidebarLabels = !sidebarCollapsed || sidebarOpen;
|
||||
|
||||
// 将顶栏高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动
|
||||
// 将顶部悬浮控制区高度写入 CSS 变量,供移动端粘性元素和浮层避让。
|
||||
useLayoutEffect(() => {
|
||||
const updateHeaderHeight = () => {
|
||||
const height = headerRef.current?.offsetHeight;
|
||||
@@ -296,19 +293,6 @@ export function MainLayout() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 5秒后自动收起品牌名称
|
||||
useEffect(() => {
|
||||
brandCollapseTimer.current = setTimeout(() => {
|
||||
setBrandExpanded(false);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
if (brandCollapseTimer.current) {
|
||||
clearTimeout(brandCollapseTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!languageMenuOpen) {
|
||||
return;
|
||||
@@ -361,19 +345,6 @@ export function MainLayout() {
|
||||
};
|
||||
}, [themeMenuOpen]);
|
||||
|
||||
const handleBrandClick = useCallback(() => {
|
||||
if (!brandExpanded) {
|
||||
setBrandExpanded(true);
|
||||
// 点击展开后,5秒后再次收起
|
||||
if (brandCollapseTimer.current) {
|
||||
clearTimeout(brandCollapseTimer.current);
|
||||
}
|
||||
brandCollapseTimer.current = setTimeout(() => {
|
||||
setBrandExpanded(false);
|
||||
}, 5000);
|
||||
}
|
||||
}, [brandExpanded]);
|
||||
|
||||
const toggleLanguageMenu = useCallback(() => {
|
||||
setLanguageMenuOpen((prev) => !prev);
|
||||
setThemeMenuOpen(false);
|
||||
@@ -409,15 +380,6 @@ export function MainLayout() {
|
||||
});
|
||||
}, [fetchConfig]);
|
||||
|
||||
const statusClass =
|
||||
connectionStatus === 'connected'
|
||||
? 'success'
|
||||
: connectionStatus === 'connecting'
|
||||
? 'warning'
|
||||
: connectionStatus === 'error'
|
||||
? 'error'
|
||||
: 'muted';
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: t('nav.dashboard'), icon: sidebarIcons.dashboard },
|
||||
{ path: '/config', label: t('nav.config_management'), icon: sidebarIcons.config },
|
||||
@@ -508,176 +470,157 @@ export function MainLayout() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div className={`app-shell ${sidebarCollapsed ? 'sidebar-is-collapsed' : ''}`}>
|
||||
<div className="top-gradient-blur" aria-hidden="true" />
|
||||
|
||||
<header className="main-header" ref={headerRef}>
|
||||
<div className="left">
|
||||
<button
|
||||
className="sidebar-toggle-header"
|
||||
onClick={() => setSidebarCollapsed((prev) => !prev)}
|
||||
title={
|
||||
sidebarCollapsed
|
||||
? t('sidebar.expand', { defaultValue: '展开' })
|
||||
: t('sidebar.collapse', { defaultValue: '收起' })
|
||||
}
|
||||
>
|
||||
{sidebarCollapsed ? headerIcons.chevronRight : headerIcons.chevronLeft}
|
||||
</button>
|
||||
<img src={INLINE_LOGO_JPEG} alt="CPAMC logo" className="brand-logo" />
|
||||
<div
|
||||
className={`brand-header ${brandExpanded ? 'expanded' : 'collapsed'}`}
|
||||
onClick={handleBrandClick}
|
||||
title={brandExpanded ? undefined : fullBrandName}
|
||||
>
|
||||
<span className="brand-full">{fullBrandName}</span>
|
||||
<span className="brand-abbr">{abbrBrandName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-toggle-floating"
|
||||
onClick={() => setSidebarCollapsed((prev) => !prev)}
|
||||
title={
|
||||
sidebarCollapsed
|
||||
? t('sidebar.expand', { defaultValue: '展开' })
|
||||
: t('sidebar.collapse', { defaultValue: '收起' })
|
||||
}
|
||||
aria-label={
|
||||
sidebarCollapsed
|
||||
? t('sidebar.expand', { defaultValue: '展开' })
|
||||
: t('sidebar.collapse', { defaultValue: '收起' })
|
||||
}
|
||||
>
|
||||
{sidebarCollapsed ? headerIcons.chevronRight : headerIcons.chevronLeft}
|
||||
</button>
|
||||
|
||||
<div className="right">
|
||||
<div className="connection">
|
||||
<span className={`status-badge ${statusClass}`}>
|
||||
{t(
|
||||
connectionStatus === 'connected'
|
||||
? 'common.connected_status'
|
||||
: connectionStatus === 'connecting'
|
||||
? 'common.connecting_status'
|
||||
: 'common.disconnected_status'
|
||||
)}
|
||||
</span>
|
||||
<span className="base">{apiBase || '-'}</span>
|
||||
</div>
|
||||
|
||||
<div className="header-actions">
|
||||
<Button
|
||||
className="mobile-menu-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSidebarOpen((prev) => !prev)}
|
||||
>
|
||||
{headerIcons.menu}
|
||||
</Button>
|
||||
<div className="header-actions floating-actions">
|
||||
<Button
|
||||
className="mobile-menu-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSidebarOpen((prev) => !prev)}
|
||||
title={t('sidebar.toggle_expand', { defaultValue: 'Open navigation' })}
|
||||
aria-label={t('sidebar.toggle_expand', { defaultValue: 'Open navigation' })}
|
||||
>
|
||||
{headerIcons.menu}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefreshAll}
|
||||
title={t('header.refresh_all')}
|
||||
>
|
||||
{headerIcons.refresh}
|
||||
</Button>
|
||||
<div className={`language-menu ${languageMenuOpen ? 'open' : ''}`} ref={languageMenuRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefreshAll}
|
||||
title={t('header.refresh_all')}
|
||||
onClick={toggleLanguageMenu}
|
||||
title={t('language.switch')}
|
||||
aria-label={t('language.switch')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={languageMenuOpen}
|
||||
>
|
||||
{headerIcons.refresh}
|
||||
{headerIcons.language}
|
||||
</Button>
|
||||
<div
|
||||
className={`language-menu ${languageMenuOpen ? 'open' : ''}`}
|
||||
ref={languageMenuRef}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleLanguageMenu}
|
||||
title={t('language.switch')}
|
||||
{languageMenuOpen && (
|
||||
<div
|
||||
className="notification entering language-menu-popover"
|
||||
role="menu"
|
||||
aria-label={t('language.switch')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={languageMenuOpen}
|
||||
>
|
||||
{headerIcons.language}
|
||||
</Button>
|
||||
{languageMenuOpen && (
|
||||
<div
|
||||
className="notification entering language-menu-popover"
|
||||
role="menu"
|
||||
aria-label={t('language.switch')}
|
||||
>
|
||||
{LANGUAGE_ORDER.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
className={`language-menu-option ${language === lang ? 'active' : ''}`}
|
||||
onClick={() => handleLanguageSelect(lang)}
|
||||
role="menuitemradio"
|
||||
aria-checked={language === lang}
|
||||
>
|
||||
<span>{t(LANGUAGE_LABEL_KEYS[lang])}</span>
|
||||
{language === lang ? <span className="language-menu-check">✓</span> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`theme-menu ${themeMenuOpen ? 'open' : ''}`} ref={themeMenuRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleThemeMenu}
|
||||
title={t('theme.switch')}
|
||||
{LANGUAGE_ORDER.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
className={`language-menu-option ${language === lang ? 'active' : ''}`}
|
||||
onClick={() => handleLanguageSelect(lang)}
|
||||
role="menuitemradio"
|
||||
aria-checked={language === lang}
|
||||
>
|
||||
<span>{t(LANGUAGE_LABEL_KEYS[lang])}</span>
|
||||
{language === lang ? <span className="language-menu-check">✓</span> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`theme-menu ${themeMenuOpen ? 'open' : ''}`} ref={themeMenuRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleThemeMenu}
|
||||
title={t('theme.switch')}
|
||||
aria-label={t('theme.switch')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={themeMenuOpen}
|
||||
>
|
||||
{theme === 'auto'
|
||||
? headerIcons.autoTheme
|
||||
: theme === 'dark'
|
||||
? headerIcons.moon
|
||||
: theme === 'white'
|
||||
? headerIcons.whiteTheme
|
||||
: headerIcons.sun}
|
||||
</Button>
|
||||
{themeMenuOpen && (
|
||||
<div
|
||||
className="notification entering theme-menu-popover"
|
||||
role="menu"
|
||||
aria-label={t('theme.switch')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={themeMenuOpen}
|
||||
>
|
||||
{theme === 'auto'
|
||||
? headerIcons.autoTheme
|
||||
: theme === 'dark'
|
||||
? headerIcons.moon
|
||||
: theme === 'white'
|
||||
? headerIcons.whiteTheme
|
||||
: headerIcons.sun}
|
||||
</Button>
|
||||
{themeMenuOpen && (
|
||||
<div
|
||||
className="notification entering theme-menu-popover"
|
||||
role="menu"
|
||||
aria-label={t('theme.switch')}
|
||||
>
|
||||
{THEME_CARDS.map((tc) => (
|
||||
<button
|
||||
key={tc.key}
|
||||
type="button"
|
||||
className={`theme-card ${theme === tc.key ? 'active' : ''}`}
|
||||
onClick={() => handleThemeSelect(tc.key)}
|
||||
role="menuitemradio"
|
||||
aria-checked={theme === tc.key}
|
||||
{THEME_CARDS.map((tc) => (
|
||||
<button
|
||||
key={tc.key}
|
||||
type="button"
|
||||
className={`theme-card ${theme === tc.key ? 'active' : ''}`}
|
||||
onClick={() => handleThemeSelect(tc.key)}
|
||||
role="menuitemradio"
|
||||
aria-checked={theme === tc.key}
|
||||
>
|
||||
<div
|
||||
className="theme-card-preview"
|
||||
style={{
|
||||
background: tc.colors.bg,
|
||||
border: `1px solid ${tc.colors.border}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="theme-card-preview"
|
||||
className="theme-card-header"
|
||||
style={{
|
||||
background: tc.colors.bg,
|
||||
border: `1px solid ${tc.colors.border}`,
|
||||
background: tc.colors.card,
|
||||
borderBottom: `1px solid ${tc.colors.border}`,
|
||||
}}
|
||||
>
|
||||
/>
|
||||
<div className="theme-card-body">
|
||||
<div
|
||||
className="theme-card-header"
|
||||
className="theme-card-sidebar"
|
||||
style={{
|
||||
background: tc.colors.card,
|
||||
borderBottom: `1px solid ${tc.colors.border}`,
|
||||
borderRight: `1px solid ${tc.colors.border}`,
|
||||
}}
|
||||
/>
|
||||
<div className="theme-card-body">
|
||||
<div className="theme-card-content" style={{ background: tc.colors.bg }}>
|
||||
<div
|
||||
className="theme-card-sidebar"
|
||||
style={{
|
||||
background: tc.colors.card,
|
||||
borderRight: `1px solid ${tc.colors.border}`,
|
||||
}}
|
||||
className="theme-card-line"
|
||||
style={{ background: tc.colors.textMuted }}
|
||||
/>
|
||||
<div
|
||||
className="theme-card-line short"
|
||||
style={{ background: tc.colors.textMuted }}
|
||||
/>
|
||||
<div className="theme-card-content" style={{ background: tc.colors.bg }}>
|
||||
<div
|
||||
className="theme-card-line"
|
||||
style={{ background: tc.colors.textMuted }}
|
||||
/>
|
||||
<div
|
||||
className="theme-card-line short"
|
||||
style={{ background: tc.colors.textMuted }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="theme-card-label">{t(tc.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={logout} title={t('header.logout')}>
|
||||
{headerIcons.logout}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="theme-card-label">{t(tc.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={logout} title={t('header.logout')}>
|
||||
{headerIcons.logout}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -694,6 +637,11 @@ export function MainLayout() {
|
||||
<aside
|
||||
className={`sidebar ${sidebarOpen ? 'open' : ''} ${sidebarCollapsed ? 'collapsed' : ''}`}
|
||||
>
|
||||
<div className="sidebar-brand" title={fullBrandName}>
|
||||
<img src={INLINE_LOGO_JPEG} alt="CPAMC logo" className="sidebar-brand-logo" />
|
||||
{showSidebarLabels && <span className="sidebar-brand-title">{abbrBrandName}</span>}
|
||||
</div>
|
||||
|
||||
<div className="nav-section">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
@@ -701,10 +649,10 @@ export function MainLayout() {
|
||||
to={item.path}
|
||||
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
title={sidebarCollapsed ? item.label : undefined}
|
||||
title={showSidebarLabels ? undefined : item.label}
|
||||
>
|
||||
<span className="nav-icon">{item.icon}</span>
|
||||
{!sidebarCollapsed && <span className="nav-label">{item.label}</span>}
|
||||
{showSidebarLabels && <span className="nav-label">{item.label}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0.42;
|
||||
filter: blur(16px);
|
||||
}
|
||||
|
||||
.orb1 {
|
||||
@@ -29,7 +31,7 @@
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
color-mix(in srgb, var(--primary-color) 8%, transparent),
|
||||
color-mix(in srgb, var(--primary-color) 6%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
top: -140px;
|
||||
@@ -44,7 +46,7 @@
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
color-mix(in srgb, var(--success-color) 6%, transparent),
|
||||
color-mix(in srgb, var(--success-color) 4%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
bottom: 18%;
|
||||
@@ -97,10 +99,10 @@
|
||||
top: 50%;
|
||||
left: $spacing-xl;
|
||||
transform: translateY(-50%);
|
||||
font-size: clamp(64px, 12vw, 120px);
|
||||
font-size: 104px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04em;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-primary);
|
||||
opacity: 0.04;
|
||||
@@ -110,7 +112,7 @@
|
||||
animation: watermarkEnter 0.8s ease-out 0.1s both;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
font-size: clamp(48px, 14vw, 80px);
|
||||
font-size: 58px;
|
||||
left: $spacing-lg;
|
||||
}
|
||||
}
|
||||
@@ -137,7 +139,7 @@
|
||||
.heroGreeting {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary-color);
|
||||
animation: fadeSlideUp 0.5s ease-out 0.1s both;
|
||||
@@ -145,12 +147,16 @@
|
||||
|
||||
.heroTitle {
|
||||
margin: 0;
|
||||
font-size: clamp(32px, 5vw, 48px);
|
||||
font-size: 44px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-primary);
|
||||
animation: fadeSlideUp 0.5s ease-out 0.2s both;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.heroCaring {
|
||||
@@ -269,7 +275,7 @@
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 0 $spacing-md;
|
||||
}
|
||||
@@ -294,9 +300,19 @@
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-lg;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, var(--bg-primary) 86%, transparent),
|
||||
color-mix(in srgb, var(--bg-secondary) 72%, transparent)
|
||||
);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 66%, transparent);
|
||||
border-radius: $radius-lg;
|
||||
--glass-blur: 12px;
|
||||
backdrop-filter: var(--glass-backdrop-filter);
|
||||
-webkit-backdrop-filter: var(--glass-backdrop-filter);
|
||||
box-shadow:
|
||||
0 18px 42px rgb(0 0 0 / 0.12),
|
||||
inset 0 1px 0 rgb(255 255 255 / 0.03);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color $transition-fast,
|
||||
@@ -305,9 +321,11 @@
|
||||
animation: cardEnter 0.4s ease-out both;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-3px);
|
||||
border-color: color-mix(in srgb, var(--border-hover) 82%, transparent);
|
||||
box-shadow:
|
||||
0 22px 48px rgb(0 0 0 / 0.16),
|
||||
inset 0 1px 0 rgb(255 255 255 / 0.04);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,8 +335,8 @@
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
160deg,
|
||||
color-mix(in srgb, var(--primary-color) 6%, var(--bg-primary)),
|
||||
var(--bg-primary)
|
||||
color-mix(in srgb, var(--primary-color) 8%, var(--bg-primary)),
|
||||
color-mix(in srgb, var(--bg-primary) 84%, transparent)
|
||||
);
|
||||
|
||||
.bentoValue {
|
||||
@@ -408,14 +426,17 @@
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
padding: 6px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--bg-primary) 68%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent);
|
||||
border-radius: $radius-full;
|
||||
--glass-blur: 10px;
|
||||
backdrop-filter: var(--glass-backdrop-filter);
|
||||
-webkit-backdrop-filter: var(--glass-backdrop-filter);
|
||||
font-size: 13px;
|
||||
transition: border-color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-hover);
|
||||
border-color: color-mix(in srgb, var(--border-hover) 82%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+525
-440
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user