From 7549159204c02ade259ac8ac791659c86ad69928 Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Wed, 10 Dec 2025 10:14:34 +0100 Subject: [PATCH] feat: Professional redesign of user and admin backend - Redesigned Sidebar with pounce puma logo and elegant premium styling - Updated CommandCenterLayout with improved top bar styling - Integrated Admin page into CommandCenterLayout for consistent experience - Created reusable DataTable component with elegant styling - Enhanced Dashboard with premium card designs and visual effects - Improved Watchlist with modern gradient styling - Added case-insensitive email handling in auth (from previous fix) All tables now have consistent, elegant styling with: - Gradient backgrounds - Subtle borders and shadows - Hover effects with accent color - Responsive design - Status badges and action buttons --- backend/app/services/auth.py | 11 +- backend/backend.pid | 2 +- frontend/src/app/admin/page.tsx | 1607 +++++------------ frontend/src/app/dashboard/page.tsx | 250 +-- frontend/src/app/watchlist/page.tsx | 36 +- .../src/components/CommandCenterLayout.tsx | 17 +- frontend/src/components/DataTable.tsx | 224 +++ frontend/src/components/Sidebar.tsx | 193 +- frontend/src/lib/store.ts | 25 +- 9 files changed, 982 insertions(+), 1383 deletions(-) create mode 100644 frontend/src/components/DataTable.tsx diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py index 56aa280..6360b77 100644 --- a/backend/app/services/auth.py +++ b/backend/app/services/auth.py @@ -58,8 +58,11 @@ class AuthService: @staticmethod async def get_user_by_email(db: AsyncSession, email: str) -> Optional[User]: - """Get user by email.""" - result = await db.execute(select(User).where(User.email == email)) + """Get user by email (case-insensitive).""" + from sqlalchemy import func + result = await db.execute( + select(User).where(func.lower(User.email) == email.lower()) + ) return result.scalar_one_or_none() @staticmethod @@ -89,9 +92,9 @@ class AuthService: name: Optional[str] = None ) -> User: """Create a new user with default subscription.""" - # Create user + # Create user (normalize email to lowercase) user = User( - email=email, + email=email.lower().strip(), hashed_password=AuthService.hash_password(password), name=name, ) diff --git a/backend/backend.pid b/backend/backend.pid index df260d9..3757e92 100644 --- a/backend/backend.pid +++ b/backend/backend.pid @@ -1 +1 @@ -4645 +7503 diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index e3be9cd..51f5d88 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' -import { Header } from '@/components/Header' -import { Footer } from '@/components/Footer' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { DataTable, StatusBadge, TableAction } from '@/components/DataTable' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { @@ -32,33 +32,21 @@ import { Clock, History, X, - ChevronDown, BookOpen, Plus, Edit2, - Trash2 as TrashIcon, ExternalLink, + Sparkles, } from 'lucide-react' import clsx from 'clsx' type TabType = 'overview' | 'users' | 'alerts' | 'newsletter' | 'tld' | 'auctions' | 'blog' | 'system' | 'activity' interface AdminStats { - users: { - total: number - active: number - verified: number - new_this_week: number - } + users: { total: number; active: number; verified: number; new_this_week: number } subscriptions: Record - domains: { - watched: number - portfolio: number - } - tld_data: { - unique_tlds: number - price_records: number - } + domains: { watched: number; portfolio: number } + tld_data: { unique_tlds: number; price_records: number } newsletter_subscribers: number auctions: number price_alerts: number @@ -74,94 +62,28 @@ interface AdminUser { created_at: string last_login: string | null domain_count: number - subscription: { - tier: string - tier_name: string - status: string | null - domain_limit: number - } -} - -interface NewsletterSubscriber { - id: number - email: string - is_active: boolean - subscribed_at: string - unsubscribed_at: string | null -} - -interface PriceAlert { - id: number - tld: string - target_price: number | null - alert_type: string - created_at: string - user: { id: number; email: string; name: string | null } -} - -interface ActivityLogEntry { - id: number - action: string - details: string - created_at: string - admin: { id: number; email: string; name: string | null } -} - -interface BlogPostAdmin { - id: number - title: string - slug: string - excerpt: string | null - cover_image: string | null - category: string | null - tags: string[] - is_published: boolean - published_at: string | null - created_at: string - view_count: number - author: { id: number; name: string | null } -} - -interface SchedulerJob { - id: string - name: string - next_run: string | null - trigger: string + subscription: { tier: string; tier_name: string; status: string | null; domain_limit: number } } export default function AdminPage() { const router = useRouter() - const { user, isAuthenticated, isLoading, checkAuth } = useStore() + const { user, isAuthenticated, isLoading } = useStore() const [activeTab, setActiveTab] = useState('overview') const [stats, setStats] = useState(null) const [users, setUsers] = useState([]) const [usersTotal, setUsersTotal] = useState(0) const [selectedUsers, setSelectedUsers] = useState([]) - const [newsletter, setNewsletter] = useState([]) + const [newsletter, setNewsletter] = useState([]) const [newsletterTotal, setNewsletterTotal] = useState(0) - const [priceAlerts, setPriceAlerts] = useState([]) + const [priceAlerts, setPriceAlerts] = useState([]) const [priceAlertsTotal, setPriceAlertsTotal] = useState(0) - const [activityLog, setActivityLog] = useState([]) + const [activityLog, setActivityLog] = useState([]) const [activityLogTotal, setActivityLogTotal] = useState(0) - const [blogPosts, setBlogPosts] = useState([]) + const [blogPosts, setBlogPosts] = useState([]) const [blogPostsTotal, setBlogPostsTotal] = useState(0) - const [showBlogEditor, setShowBlogEditor] = useState(false) - const [editingPost, setEditingPost] = useState(null) - const [blogForm, setBlogForm] = useState({ - title: '', - content: '', - excerpt: '', - category: '', - tags: '', - cover_image: '', - is_published: false, - }) - const [schedulerStatus, setSchedulerStatus] = useState<{ - scheduler_running: boolean - jobs: SchedulerJob[] - last_runs: { tld_scrape: string | null; auction_scrape: string | null; domain_check: string | null } - } | null>(null) + const [schedulerStatus, setSchedulerStatus] = useState(null) + const [systemHealth, setSystemHealth] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) @@ -170,15 +92,8 @@ export default function AdminPage() { const [auctionScraping, setAuctionScraping] = useState(false) const [domainChecking, setDomainChecking] = useState(false) const [sendingEmail, setSendingEmail] = useState(false) - const [auctionStatus, setAuctionStatus] = useState(null) - const [systemHealth, setSystemHealth] = useState(null) - const [selectedUser, setSelectedUser] = useState(null) const [bulkTier, setBulkTier] = useState('trader') - useEffect(() => { - checkAuth() - }, [checkAuth]) - useEffect(() => { if (!isLoading && !isAuthenticated) { router.push('/login') @@ -211,13 +126,6 @@ export default function AdminPage() { const nlData = await api.getAdminNewsletter(100, 0) setNewsletter(nlData.subscribers) setNewsletterTotal(nlData.total) - } else if (activeTab === 'auctions') { - try { - const statusData = await api.getAuctionScrapeStatus() - setAuctionStatus(statusData) - } catch { - setAuctionStatus(null) - } } else if (activeTab === 'system') { try { const [healthData, schedulerData] = await Promise.all([ @@ -226,32 +134,23 @@ export default function AdminPage() { ]) setSystemHealth(healthData) setSchedulerStatus(schedulerData) - } catch { - setSystemHealth(null) - setSchedulerStatus(null) - } + } catch { /* ignore */ } } else if (activeTab === 'activity') { try { const logData = await api.getActivityLog(50, 0) setActivityLog(logData.logs) setActivityLogTotal(logData.total) - } catch { - setActivityLog([]) - setActivityLogTotal(0) - } + } catch { /* ignore */ } } else if (activeTab === 'blog') { try { const blogData = await api.getAdminBlogPosts(50, 0) setBlogPosts(blogData.posts) setBlogPostsTotal(blogData.total) - } catch { - setBlogPosts([]) - setBlogPostsTotal(0) - } + } catch { /* ignore */ } } } catch (err) { if (err instanceof Error && err.message.includes('403')) { - setError('Admin privileges required. You are not authorized to access this page.') + setError('Admin privileges required.') } else { setError(err instanceof Error ? err.message : 'Failed to load admin data') } @@ -334,13 +233,10 @@ export default function AdminPage() { } const handleDeleteUser = async (userId: number, userEmail: string) => { - if (!confirm(`Are you sure you want to delete user "${userEmail}" and ALL their data?\n\nThis action cannot be undone.`)) { - return - } + if (!confirm(`Delete user "${userEmail}" and ALL their data?\n\nThis cannot be undone.`)) return try { await api.deleteAdminUser(userId) - setSuccess(`User ${userEmail} and all their data have been deleted`) - setSelectedUser(null) + setSuccess(`User ${userEmail} deleted`) loadAdminData() } catch (err) { setError(err instanceof Error ? err.message : 'Delete failed') @@ -377,1126 +273,489 @@ export default function AdminPage() { } } - const toggleUserSelection = (userId: number) => { - setSelectedUsers(prev => - prev.includes(userId) - ? prev.filter(id => id !== userId) - : [...prev, userId] - ) - } - - const toggleSelectAll = () => { - if (selectedUsers.length === users.length) { - setSelectedUsers([]) - } else { - setSelectedUsers(users.map(u => u.id)) - } - } - if (isLoading) { return ( -
-
+
+
) } if (error && error.includes('403')) { return ( -
-
-
-
- -

Access Denied

-

- You don't have admin privileges to access this page. -

- -
-
-
-
+ +
+ +

Admin Access Required

+

You don't have admin privileges.

+ +
+
) } const tabs = [ { id: 'overview' as const, label: 'Overview', icon: Activity }, { id: 'users' as const, label: 'Users', icon: Users }, - { id: 'alerts' as const, label: 'Price Alerts', icon: Bell }, + { id: 'alerts' as const, label: 'Alerts', icon: Bell }, { id: 'newsletter' as const, label: 'Newsletter', icon: Mail }, { id: 'tld' as const, label: 'TLD Data', icon: Globe }, { id: 'auctions' as const, label: 'Auctions', icon: Gavel }, - { id: 'blog' as const, label: 'Briefings', icon: BookOpen }, + { id: 'blog' as const, label: 'Blog', icon: BookOpen }, { id: 'system' as const, label: 'System', icon: Database }, { id: 'activity' as const, label: 'Activity', icon: History }, ] return ( -
- {/* Background Effects */} -
-
-
+ + {/* Messages */} + {error && !error.includes('403') && ( +
+ +

{error}

+ +
+ )} + + {success && ( +
+ +

{success}

+ +
+ )} + + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))}
-
+ {loading ? ( +
+ +
+ ) : ( + <> + {/* Overview Tab */} + {activeTab === 'overview' && stats && ( +
+
+ + + + +
-
-
- {/* Header */} -
- Admin HQ -

- Mission Control. -

-

- Users. Data. System. All under your command. -

-
+
+ {['scout', 'trader', 'tycoon'].map((tier) => ( +
+
+ {tier === 'tycoon' ? : + tier === 'trader' ? : + } + {tier} +
+

{stats.subscriptions[tier] || 0}

+
+ ))} +
- {/* Messages */} - {error && !error.includes('403') && ( -
- -

{error}

- +
+
+

Active Auctions

+

{stats.auctions.toLocaleString()}

+

from all platforms

+
+
+

Price Alerts

+

{stats.price_alerts.toLocaleString()}

+

active alerts

+
+
)} - {success && ( -
- -

{success}

- -
- )} + {/* Users Tab */} + {activeTab === 'users' && ( +
+
+
+ + setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && loadAdminData()} + placeholder="Search users..." + className="w-full pl-11 pr-4 py-3 bg-background-secondary/50 border border-border/30 rounded-xl text-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50" + /> +
+ +
- {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {loading ? ( -
- -
- ) : ( - <> - {/* Overview Tab */} - {activeTab === 'overview' && stats && ( -
-
- - - - -
- -
-

Subscriptions by Tier

-
-
-
- - Scout -
-

{stats.subscriptions.scout || 0}

-
-
-
- - Trader -
-

{stats.subscriptions.trader || 0}

-
-
-
- - Tycoon -
-

{stats.subscriptions.tycoon || 0}

-
-
-
- -
-
-

Active Auctions

-

{stats.auctions.toLocaleString()}

-

from all platforms

-
-
-

Price Alerts

-

{stats.price_alerts.toLocaleString()}

-

active alerts

-
+ {selectedUsers.length > 0 && ( +
+ {selectedUsers.length} users selected +
+ + +
)} - {/* Users Tab */} - {activeTab === 'users' && ( -
-
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && loadAdminData()} - placeholder="Search users..." - className="w-full pl-11 pr-4 py-2.5 bg-background-secondary border border-border rounded-xl text-body-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50" + u.id} + selectable + selectedIds={selectedUsers} + onSelectionChange={(ids) => setSelectedUsers(ids as number[])} + columns={[ + { + key: 'email', + header: 'User', + render: (u) => ( +
+

{u.email}

+

{u.name || 'No name'}

+
+ ), + }, + { + key: 'status', + header: 'Status', + hideOnMobile: true, + render: (u) => ( +
+ {u.is_admin && } + {u.is_verified && } + {!u.is_active && } +
+ ), + }, + { + key: 'tier', + header: 'Tier', + render: (u) => ( + -
- -
- - {selectedUsers.length > 0 && ( -
- {selectedUsers.length} users selected -
+ ), + }, + { + key: 'domains', + header: 'Domains', + hideOnMobile: true, + render: (u) => {u.domain_count}, + }, + { + key: 'actions', + header: 'Actions', + className: 'text-right', + headerClassName: 'text-right', + render: (u) => ( +
- - + handleToggleAdmin(u.id, u.is_admin)} + variant={u.is_admin ? 'accent' : 'default'} + title={u.is_admin ? 'Remove admin' : 'Make admin'} + /> + handleDeleteUser(u.id, u.email)} + variant="danger" + disabled={u.is_admin} + title={u.is_admin ? 'Cannot delete admin' : 'Delete user'} + />
-
- )} - -
-
- - - - - - - - - - - - - {users.map((u) => ( - - - - - - - - - ))} - -
- 0} - onChange={toggleSelectAll} - className="w-4 h-4 rounded border-border" - /> - UserStatusTierDomainsActions
- toggleUserSelection(u.id)} - className="w-4 h-4 rounded border-border" - /> - - - -
- {u.is_admin && Admin} - {u.is_verified && Verified} - {!u.is_active && Inactive} -
-
- - {u.subscription.tier_name} - - - {u.domain_count} - -
- - - -
-
-
+ ), + }, + ]} + emptyState={ +
+ +

No users found

-

Showing {users.length} of {usersTotal} users

-
- )} + } + /> +

Showing {users.length} of {usersTotal} users

+
+ )} - {/* Price Alerts Tab */} - {activeTab === 'alerts' && ( -
-

{priceAlertsTotal} active price alerts

- - {priceAlerts.length === 0 ? ( -
- -

No active price alerts

-
- ) : ( -
- - - - - - - - - - - - {priceAlerts.map((alert) => ( - - - - - - - - ))} - -
TLDTarget PriceTypeUserCreated
.{alert.tld} - {alert.target_price ? `$${alert.target_price.toFixed(2)}` : '—'} - - - {alert.alert_type} - - {alert.user.email} - {new Date(alert.created_at).toLocaleDateString()} -
-
- )} -
- )} + {/* Price Alerts Tab */} + {activeTab === 'alerts' && ( +
+

{priceAlertsTotal} active price alerts

+ a.id} + columns={[ + { key: 'tld', header: 'TLD', render: (a) => .{a.tld} }, + { key: 'target_price', header: 'Target', render: (a) => a.target_price ? `$${a.target_price.toFixed(2)}` : '—' }, + { key: 'alert_type', header: 'Type', render: (a) => }, + { key: 'user', header: 'User', render: (a) => a.user.email }, + { key: 'created_at', header: 'Created', hideOnMobile: true, render: (a) => new Date(a.created_at).toLocaleDateString() }, + ]} + emptyState={<>

No active alerts

} + /> +
+ )} - {/* Newsletter Tab */} - {activeTab === 'newsletter' && ( -
-
-

{newsletterTotal} total subscribers

- +
+ s.id} + columns={[ + { key: 'email', header: 'Email', render: (s) => {s.email} }, + { key: 'is_active', header: 'Status', render: (s) => }, + { key: 'subscribed_at', header: 'Subscribed', render: (s) => new Date(s.subscribed_at).toLocaleDateString() }, + ]} + /> +
+ )} + + {/* TLD Tab */} + {activeTab === 'tld' && ( +
+
+

TLD Price Data

+
+
Unique TLDs{stats?.tld_data.unique_tlds || 0}
+
Price Records{stats?.tld_data.price_records?.toLocaleString() || 0}
+
+
+
+

Scrape TLD Prices

+

Manually trigger a TLD price scrape.

+ +
+
+ )} + + {/* Auctions Tab */} + {activeTab === 'auctions' && ( +
+
+

Auction Data

+

{stats?.auctions || 0}

+

Total auctions

+
+
+

Scrape Auctions

+

GoDaddy, Sedo, NameJet, DropCatch

+ +
+
+ )} + + {/* System Tab */} + {activeTab === 'system' && ( +
+
+

System Status

+
+ {[ + { label: 'Database', ok: systemHealth?.database === 'healthy', text: systemHealth?.database || 'Unknown' }, + { label: 'Email (SMTP)', ok: systemHealth?.email_configured, text: systemHealth?.email_configured ? 'Configured' : 'Not configured' }, + { label: 'Stripe', ok: systemHealth?.stripe_configured, text: systemHealth?.stripe_configured ? 'Configured' : 'Not configured' }, + { label: 'Scheduler', ok: schedulerStatus?.scheduler_running, text: schedulerStatus?.scheduler_running ? 'Running' : 'Stopped' }, + ].map((item) => ( +
+ {item.label} + + {item.ok ? : } + {item.text} + +
+ ))} +
+
+ +
+
+

Manual Triggers

+
+ +
- -
- - - - - - - - - - {newsletter.map((s) => ( - - - - - - ))} - -
EmailStatusSubscribed
{s.email} - - {s.is_active ? 'Active' : 'Unsubscribed'} - - - {new Date(s.subscribed_at).toLocaleDateString()} -
-
- )} - {/* TLD Tab */} - {activeTab === 'tld' && ( -
-
-
-

TLD Price Data

-
-
- Unique TLDs - {stats?.tld_data.unique_tlds || 0} -
-
- Price Records - {stats?.tld_data.price_records?.toLocaleString() || 0} -
-
-
- -
-

Scrape TLD Prices

-

Manually trigger a TLD price scrape.

- -
-
-
- )} - - {/* Auctions Tab */} - {activeTab === 'auctions' && ( -
-
-
-

Auction Data

-
-
- Total Auctions - {stats?.auctions || 0} -
-
-
- -
-

Scrape Auctions

-

Scrape from GoDaddy, Sedo, NameJet, DropCatch.

- -
-
- -
-

Platforms

-
- {['GoDaddy', 'Sedo', 'NameJet', 'DropCatch'].map((platform) => ( -
-
- - {platform} -
-

Active

+ {schedulerStatus && ( +
+

Scheduled Jobs

+
+ {schedulerStatus.jobs?.slice(0, 4).map((job: any) => ( +
+ {job.name} + {job.trigger}
))}
-
- )} - - {/* System Tab */} - {activeTab === 'system' && ( -
-
-

System Status

-
-
- Database - - {systemHealth?.database === 'healthy' ? ( - <>Healthy - ) : ( - <>{systemHealth?.database || 'Unknown'} - )} - -
-
- Email (SMTP) - - {systemHealth?.email_configured ? ( - <>Configured - ) : ( - <>Not configured - )} - -
-
- Stripe Payments - - {systemHealth?.stripe_configured ? ( - <>Configured - ) : ( - <>Not configured - )} - -
-
- Scheduler - - {schedulerStatus?.scheduler_running ? ( - <>Running - ) : ( - <>Stopped - )} - -
-
-
- - {/* Scheduler Jobs */} - {schedulerStatus && ( -
-

Scheduled Jobs

-
- {schedulerStatus.jobs.map((job) => ( -
-
-

{job.name}

-

{job.trigger}

-
-
-

Next run:

-

- {job.next_run ? new Date(job.next_run).toLocaleString() : 'Not scheduled'} -

-
-
- ))} -
-
-

Last Runs

-
-
-

TLD Scrape

-

- {schedulerStatus.last_runs.tld_scrape - ? new Date(schedulerStatus.last_runs.tld_scrape).toLocaleString() - : 'Never'} -

-
-
-

Auction Scrape

-

- {schedulerStatus.last_runs.auction_scrape - ? new Date(schedulerStatus.last_runs.auction_scrape).toLocaleString() - : 'Never'} -

-
-
-

Domain Check

-

- {schedulerStatus.last_runs.domain_check - ? new Date(schedulerStatus.last_runs.domain_check).toLocaleString() - : 'Never'} -

-
-
-
-
- )} - - {/* Quick Actions */} -
-
-

Manual Triggers

-
- - -
-
- -
-

Environment

-
-
- SMTP_HOST - smtp.zoho.eu -
-
- SMTP_PORT - 465 -
-
- DATABASE - SQLite -
- {systemHealth?.timestamp && ( -
- Last check: {new Date(systemHealth.timestamp).toLocaleString()} -
- )} -
-
-
-
- )} - - {/* Blog Tab */} - {activeTab === 'blog' && ( -
-
-

{blogPostsTotal} blog posts

- -
- - {blogPosts.length === 0 ? ( -
- -

No blog posts yet

-

Create your first post to get started

-
- ) : ( -
- - - - - - - - - - - - {blogPosts.map((post) => ( - - - - - - - - ))} - -
TitleCategoryStatusViewsActions
-

{post.title}

-

{post.slug}

-
- {post.category ? ( - - {post.category} - - ) : ( - - )} - - - {post.is_published ? 'Published' : 'Draft'} - - - {post.view_count} - -
- - - - - - {!post.is_published ? ( - - ) : ( - - )} -
-
-
- )} -
- )} - - {/* Activity Log Tab */} - {activeTab === 'activity' && ( -
-

{activityLogTotal} log entries

- - {activityLog.length === 0 ? ( -
- -

No activity logged yet

-
- ) : ( -
- - - - - - - - - - - {activityLog.map((log) => ( - - - - - - - ))} - -
ActionDetailsAdminTime
- - {log.action} - - {log.details}{log.admin.email} - {new Date(log.created_at).toLocaleString()} -
-
- )} -
- )} - + )} +
+
)} -
-
- {/* User Detail Modal */} - {selectedUser && ( -
-
-
-

User Details

- -
-
-
-

Email

-

{selectedUser.email}

-
-
-

Name

-

{selectedUser.name || 'Not set'}

-
-
-
-

Status

-
- {selectedUser.is_admin && Admin} - {selectedUser.is_verified && Verified} - {selectedUser.is_active ? ( - Active - ) : ( - Inactive - )} -
-
-
-

Subscription

- - {selectedUser.subscription.tier_name} - -
-
-
-
-

Domains

-

{selectedUser.domain_count} / {selectedUser.subscription.domain_limit}

-
-
-

Created

-

{new Date(selectedUser.created_at).toLocaleDateString()}

-
-
- {selectedUser.last_login && ( -
-

Last Login

-

{new Date(selectedUser.last_login).toLocaleString()}

-
- )} -
-
- -
- -
+ p.id} + columns={[ + { key: 'title', header: 'Title', render: (p) =>

{p.title}

{p.slug}

}, + { key: 'category', header: 'Category', hideOnMobile: true, render: (p) => p.category ? : '—' }, + { key: 'is_published', header: 'Status', render: (p) => }, + { key: 'view_count', header: 'Views', hideOnMobile: true, render: (p) => p.view_count }, + { key: 'actions', header: 'Actions', className: 'text-right', headerClassName: 'text-right', render: (p) => ( +
+ window.open(`/blog/${p.slug}`, '_blank')} title="View" /> + + +
+ ) }, + ]} + emptyState={<>

No blog posts yet

} + />
-
-
- )} + )} - {/* Blog Editor Modal */} - {showBlogEditor && ( -
-
-
-

- {editingPost ? 'Edit Post' : 'New Blog Post'} -

- + {/* Activity Tab */} + {activeTab === 'activity' && ( +
+

{activityLogTotal} log entries

+ l.id} + columns={[ + { key: 'action', header: 'Action', render: (l) => }, + { key: 'details', header: 'Details', render: (l) => {l.details} }, + { key: 'admin', header: 'Admin', render: (l) => l.admin.email }, + { key: 'created_at', header: 'Time', hideOnMobile: true, render: (l) => new Date(l.created_at).toLocaleString() }, + ]} + emptyState={<>

No activity logged

} + />
-
-
- - setBlogForm({ ...blogForm, title: e.target.value })} - placeholder="Enter post title..." - className="w-full px-4 py-3 bg-background-secondary border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50" - /> -
-
- -