refactor: design critique fixes for account pages

- Admin accounts: replace dense card grid with compact DataTable
- Account settings: remove redundant hero card, stat grid, header pills
- Fix bg-accent (orange) misuse on decorative elements across 7 files
- Add ConfirmButton for destructive actions (deactivate, remove member)
- Replace single-field modals with inline editing (plan, trial)
- Add contextual help: display code tooltip, improved empty states
- Non-owner aside explanation for hidden owner-only sections
- Admin sidebar: group 11 items into 5 labeled sections
- Rename UsersPage.tsx → AccountsPage.tsx to match route
- Fix border radius consistency, hide zero-count badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-02 06:24:26 +00:00
parent 8c28f48ce0
commit a2749104f4
11 changed files with 1102 additions and 1337 deletions

View File

@@ -54,7 +54,7 @@ export function ActionMenu({ items }: ActionMenuProps) {
onClick={() => setOpen(!open)}
className={cn(
'rounded-md p-1.5 text-muted-foreground transition-colors',
'hover:bg-accent hover:text-foreground'
'hover:bg-elevated hover:text-foreground'
)}
>
<MoreHorizontal className="h-4 w-4" />
@@ -81,7 +81,7 @@ export function ActionMenu({ items }: ActionMenuProps) {
'disabled:opacity-50 disabled:pointer-events-none',
item.destructive
? 'text-red-400 hover:bg-red-400/10'
: 'text-muted-foreground hover:bg-accent'
: 'text-muted-foreground hover:bg-elevated'
)}
>
{item.icon}

View File

@@ -15,18 +15,54 @@ import {
} from 'lucide-react'
import { cn } from '@/lib/utils'
const navItems = [
{ path: '/admin', label: 'Dashboard', icon: LayoutDashboard, end: true },
{ path: '/admin/accounts', label: 'Accounts', icon: Building2 },
{ path: '/admin/invite-codes', label: 'Invite Codes', icon: Ticket },
{ path: '/admin/audit-logs', label: 'Audit Logs', icon: FileText },
{ path: '/admin/plan-limits', label: 'Plan Limits', icon: Gauge },
{ path: '/admin/feature-flags', label: 'Feature Flags', icon: ToggleLeft },
{ path: '/admin/settings', label: 'Settings', icon: Settings },
{ path: '/admin/categories', label: 'Categories', icon: FolderTree },
{ path: '/admin/survey-invites', label: 'Survey Invites', icon: ClipboardList },
{ path: '/admin/survey-responses', label: 'Survey Responses', icon: MessageSquareText },
{ path: '/admin/gallery', label: 'Gallery', icon: LayoutGrid },
interface NavItem {
path: string
label: string
icon: typeof LayoutDashboard
end?: boolean
}
interface NavSection {
label?: string
items: NavItem[]
}
const navSections: NavSection[] = [
{
items: [
{ path: '/admin', label: 'Dashboard', icon: LayoutDashboard, end: true },
{ path: '/admin/accounts', label: 'Accounts', icon: Building2 },
{ path: '/admin/invite-codes', label: 'Invite Codes', icon: Ticket },
],
},
{
label: 'Platform',
items: [
{ path: '/admin/plan-limits', label: 'Plan Limits', icon: Gauge },
{ path: '/admin/feature-flags', label: 'Feature Flags', icon: ToggleLeft },
{ path: '/admin/settings', label: 'Settings', icon: Settings },
],
},
{
label: 'Content',
items: [
{ path: '/admin/categories', label: 'Categories', icon: FolderTree },
{ path: '/admin/gallery', label: 'Gallery', icon: LayoutGrid },
],
},
{
label: 'Feedback',
items: [
{ path: '/admin/survey-invites', label: 'Survey Invites', icon: ClipboardList },
{ path: '/admin/survey-responses', label: 'Survey Responses', icon: MessageSquareText },
],
},
{
label: 'Audit',
items: [
{ path: '/admin/audit-logs', label: 'Audit Logs', icon: FileText },
],
},
]
interface AdminSidebarProps {
@@ -47,22 +83,33 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
<div className="p-4">
<h2 className="text-lg font-bold text-foreground">Admin Panel</h2>
</div>
<nav className="flex-1 space-y-1 px-3">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={onNavigate}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive(item.path, item.end)
? 'bg-accent text-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
<nav className="flex-1 space-y-4 overflow-y-auto px-3">
{navSections.map((section, i) => (
<div key={i}>
{section.label && (
<p className="mb-1 px-3 text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
{section.label}
</p>
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
<div className="space-y-0.5">
{section.items.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={onNavigate}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive(item.path, item.end)
? 'bg-elevated text-foreground'
: 'text-muted-foreground hover:bg-elevated hover:text-foreground'
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
))}
</div>
</div>
))}
</nav>
<div className="border-t border-border p-3">
@@ -71,7 +118,7 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
onClick={onNavigate}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium',
'text-muted-foreground hover:bg-accent hover:text-foreground'
'text-muted-foreground hover:bg-elevated hover:text-foreground'
)}
>
<ArrowLeft className="h-4 w-4" />

View File

@@ -53,7 +53,7 @@ export function DataTable<T>({
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-accent">
<tr className="border-b border-border bg-elevated">
{columns.map((col) => (
<th
key={col.key}
@@ -90,7 +90,7 @@ export function DataTable<T>({
<tr key={i} className="border-b border-border last:border-0">
{columns.map((col) => (
<td key={col.key} className="px-4 py-3">
<div className="h-4 w-3/4 animate-pulse rounded bg-accent" />
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
</td>
))}
</tr>
@@ -107,7 +107,7 @@ export function DataTable<T>({
data.map((item) => (
<tr
key={keyExtractor(item)}
className="border-b border-border last:border-0 hover:bg-accent transition-colors"
className="border-b border-border last:border-0 hover:bg-elevated transition-colors"
>
{columns.map((col) => (
<td key={col.key} className={cn('px-4 py-3', col.className)}>

View File

@@ -43,7 +43,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
<button
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
className={cn(btnBase, 'px-2 text-muted-foreground hover:bg-accent hover:text-foreground')}
className={cn(btnBase, 'px-2 text-muted-foreground hover:bg-elevated hover:text-foreground')}
>
<ChevronLeft className="h-4 w-4" />
</button>
@@ -59,7 +59,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
'px-2',
p === page
? 'bg-primary text-white'
: 'text-muted-foreground hover:bg-accent hover:text-foreground'
: 'text-muted-foreground hover:bg-elevated hover:text-foreground'
)}
>
{p}
@@ -69,7 +69,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
<button
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages}
className={cn(btnBase, 'px-2 text-muted-foreground hover:bg-accent hover:text-foreground')}
className={cn(btnBase, 'px-2 text-muted-foreground hover:bg-elevated hover:text-foreground')}
>
<ChevronRight className="h-4 w-4" />
</button>

View File

@@ -6,22 +6,26 @@ interface StatusBadgeProps {
variant?: BadgeVariant
children: React.ReactNode
className?: string
title?: string
}
const variantClasses: Record<BadgeVariant, string> = {
success: 'bg-emerald-400/10 text-emerald-400',
destructive: 'bg-red-400/10 text-red-400',
warning: 'bg-yellow-400/10 text-yellow-400',
default: 'bg-accent text-muted-foreground',
default: 'bg-muted text-muted-foreground',
}
export function StatusBadge({ variant = 'default', children, className }: StatusBadgeProps) {
export function StatusBadge({ variant = 'default', children, className, title }: StatusBadgeProps) {
return (
<span className={cn(
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
variantClasses[variant],
className
)}>
<span
className={cn(
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
variantClasses[variant],
className
)}
title={title}
>
{children}
</span>
)

View File

@@ -0,0 +1,69 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface ConfirmButtonProps {
onConfirm: () => void
children: React.ReactNode
confirmLabel?: string
className?: string
confirmClassName?: string
timeoutMs?: number
'aria-label'?: string
}
/**
* Two-click inline confirm button.
* First click arms the button (shows confirm state).
* Second click executes the action.
* Auto-resets after timeoutMs (default 3000ms).
*/
export function ConfirmButton({
onConfirm,
children,
confirmLabel = 'Confirm?',
className,
confirmClassName,
timeoutMs = 3000,
'aria-label': ariaLabel,
}: ConfirmButtonProps) {
const [armed, setArmed] = useState(false)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const reset = useCallback(() => {
setArmed(false)
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}, [])
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [])
const handleClick = () => {
if (armed) {
reset()
onConfirm()
} else {
setArmed(true)
timerRef.current = setTimeout(reset, timeoutMs)
}
}
return (
<button
type="button"
onClick={handleClick}
onBlur={reset}
aria-label={ariaLabel}
className={cn(armed ? confirmClassName : className)}
>
{armed ? confirmLabel : children}
</button>
)
}
export default ConfirmButton