fix: apply theme colors to all admin pages

AdminDashboard:
- Replace all gray colors with theme variables
- Indonesian text: 'Selamat datang', 'Kelola Plans', etc.
- Loading: 'Memuat...'

AdminPlans:
- bg-card, text-foreground, border-border
- text-muted-foreground for secondary text
- bg-muted for sections
- text-primary for links/icons
- text-destructive for delete
- Indonesian: 'Kelola Plans', 'Tambah Plan', 'Tidak ada plan'

AdminUsers:
- Same theme color replacements
- Indonesian: 'Kelola Users', 'Tidak ada user'
- bg-primary for avatars
- Consistent hover states

All pages now:
 Respect light/dark mode
 Use @theme colors from index.css
 Indonesian text (keeping English tech terms)
 Consistent with member layout styling
This commit is contained in:
dwindown
2025-10-11 20:18:10 +07:00
parent 4bd95e50e8
commit 50ce884a43
13 changed files with 328 additions and 239 deletions

View File

@@ -20,6 +20,7 @@ export declare class AuthController {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
}>; }>;
@@ -43,6 +44,7 @@ export declare class AuthController {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
requiresOtp?: undefined; requiresOtp?: undefined;
@@ -60,6 +62,7 @@ export declare class AuthController {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
}>; }>;
@@ -71,6 +74,7 @@ export declare class AuthController {
emailVerified: boolean; emailVerified: boolean;
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
role: string;
}>; }>;
changePassword(req: RequestWithUser, body: { changePassword(req: RequestWithUser, body: {
currentPassword: string; currentPassword: string;

View File

@@ -13,6 +13,7 @@ export declare class AuthService {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
}>; }>;
@@ -33,6 +34,7 @@ export declare class AuthService {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
requiresOtp?: undefined; requiresOtp?: undefined;
@@ -61,6 +63,7 @@ export declare class AuthService {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
requiresOtp?: undefined; requiresOtp?: undefined;
@@ -74,6 +77,7 @@ export declare class AuthService {
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
emailVerified: boolean; emailVerified: boolean;
role: string;
}; };
token: string; token: string;
}>; }>;
@@ -85,6 +89,7 @@ export declare class AuthService {
emailVerified: boolean; emailVerified: boolean;
name: string | null; name: string | null;
avatarUrl: string | null; avatarUrl: string | null;
role: string;
}>; }>;
changePassword(userId: string, currentPassword: string, newPassword: string, isSettingPassword?: boolean): Promise<{ changePassword(userId: string, currentPassword: string, newPassword: string, isSettingPassword?: boolean): Promise<{
message: string; message: string;

View File

@@ -88,6 +88,7 @@ let AuthService = class AuthService {
name: user.name, name: user.name,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
role: user.role,
}, },
token, token,
}; };
@@ -102,6 +103,7 @@ let AuthService = class AuthService {
name: true, name: true,
avatarUrl: true, avatarUrl: true,
emailVerified: true, emailVerified: true,
role: true,
otpEmailEnabled: true, otpEmailEnabled: true,
otpWhatsappEnabled: true, otpWhatsappEnabled: true,
otpTotpEnabled: true, otpTotpEnabled: true,
@@ -150,6 +152,7 @@ let AuthService = class AuthService {
name: user.name, name: user.name,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
role: user.role,
}, },
token, token,
}; };
@@ -254,6 +257,7 @@ let AuthService = class AuthService {
name: user.name, name: user.name,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
role: user.role,
}, },
token, token,
}; };
@@ -313,6 +317,7 @@ let AuthService = class AuthService {
name: user.name, name: user.name,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
role: user.role,
}, },
token, token,
}; };
@@ -340,6 +345,7 @@ let AuthService = class AuthService {
name: true, name: true,
avatarUrl: true, avatarUrl: true,
emailVerified: true, emailVerified: true,
role: true,
}, },
}); });
if (!user) { if (!user) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
import { ChevronRight, Shield } from 'lucide-react'
interface AdminBreadcrumbProps {
currentPage: string
}
const pageNames: Record<string, string> = {
'/admin': 'Dashboard',
'/admin/plans': 'Plans',
'/admin/payment-methods': 'Payment Methods',
'/admin/payments': 'Payments',
'/admin/users': 'Users',
'/admin/settings': 'Settings',
}
export function AdminBreadcrumb({ currentPage }: AdminBreadcrumbProps) {
const pageName = pageNames[currentPage] || 'Admin'
return (
<nav className="flex items-center space-x-1 text-sm text-muted-foreground">
<div className="flex items-center space-x-1">
<Shield className="h-4 w-4" />
<span>Admin</span>
</div>
{currentPage !== '/admin' && (
<>
<ChevronRight className="h-4 w-4" />
<span className="font-medium text-foreground">{pageName}</span>
</>
)}
</nav>
)
}

View File

@@ -1,47 +1,31 @@
import { Link, useLocation, Outlet } from 'react-router-dom' import { Link, useLocation, Outlet, useNavigate } from 'react-router-dom'
import { useAuth } from '../../contexts/AuthContext' import { useAuth } from '../../contexts/AuthContext'
import { import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
LayoutDashboard, import { AdminSidebar } from './AdminSidebar'
CreditCard, import { AdminBreadcrumb } from './AdminBreadcrumb'
Wallet, import { ThemeToggle } from '../ThemeToggle'
Users,
Settings,
LogOut,
Menu,
X,
} from 'lucide-react'
import { useState } from 'react'
const navigation = [
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
{ name: 'Plans', href: '/admin/plans', icon: CreditCard },
{ name: 'Payment Methods', href: '/admin/payment-methods', icon: Wallet },
{ name: 'Payments', href: '/admin/payments', icon: CreditCard },
{ name: 'Users', href: '/admin/users', icon: Users },
{ name: 'Settings', href: '/admin/settings', icon: Settings },
]
export function AdminLayout() { export function AdminLayout() {
const { user, logout } = useAuth() const { user } = useAuth()
const location = useLocation() const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(false) const navigate = useNavigate()
// Check if user is admin // Check if user is admin
if (user?.role !== 'admin') { if (user?.role !== 'admin') {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900"> <div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2"> <h1 className="text-2xl font-bold text-foreground mb-2">
Access Denied Akses Ditolak
</h1> </h1>
<p className="text-gray-600 dark:text-gray-400"> <p className="text-muted-foreground">
You don't have permission to access the admin panel. Anda tidak memiliki izin untuk mengakses panel admin.
</p> </p>
<Link <Link
to="/" to="/"
className="mt-4 inline-block text-blue-600 hover:text-blue-700" className="mt-4 inline-block text-primary hover:text-primary/90"
> >
Go to Dashboard Kembali ke Dashboard
</Link> </Link>
</div> </div>
</div> </div>
@@ -49,117 +33,31 @@ export function AdminLayout() {
} }
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900"> <SidebarProvider>
{/* Mobile sidebar backdrop */} <AdminSidebar currentPage={location.pathname} onNavigate={navigate} />
{sidebarOpen && ( <main className="flex-1 overflow-hidden">
<div <div className="flex h-screen flex-col">
className="fixed inset-0 bg-gray-600 bg-opacity-75 z-20 lg:hidden" <header className="flex h-16 shrink-0 items-center gap-4 border-b px-4 bg-background">
onClick={() => setSidebarOpen(false)} <SidebarTrigger className="-ml-1 md:h-8 md:w-8 h-10 w-10" />
/> <AdminBreadcrumb currentPage={location.pathname} />
)} <div className="flex-1" />
<span className="text-sm text-muted-foreground hidden sm:block">
{/* Sidebar */} {new Date().toLocaleDateString('id-ID', {
<div weekday: 'long',
className={`fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform transition-transform duration-300 ease-in-out lg:translate-x-0 ${ day: 'numeric',
sidebarOpen ? 'translate-x-0' : '-translate-x-full' month: 'long',
}`} year: 'numeric',
> })}
<div className="flex flex-col h-full"> </span>
{/* Logo */} <ThemeToggle />
<div className="flex items-center justify-between h-16 px-6 border-b border-gray-200 dark:border-gray-700"> </header>
<h1 className="text-xl font-bold text-gray-900 dark:text-white"> <div className="flex-1 overflow-auto">
Admin Panel <div className="container mx-auto max-w-7xl p-4">
</h1> <Outlet />
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<X className="h-6 w-6" />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const isActive = location.pathname === item.href
return (
<Link
key={item.name}
to={item.href}
className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${
isActive
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
}`}
onClick={() => setSidebarOpen(false)}
>
<item.icon className="h-5 w-5 mr-3" />
{item.name}
</Link>
)
})}
</nav>
{/* User info & logout */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center mb-3">
<div className="flex-shrink-0">
<div className="h-10 w-10 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold">
{user?.name?.[0] || user?.email[0].toUpperCase()}
</div>
</div>
<div className="ml-3 flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{user?.name || 'Admin'}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{user?.email}
</p>
</div>
</div>
<button
onClick={logout}
className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-red-700 bg-red-50 rounded-lg hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 dark:hover:bg-red-900/30 transition-colors"
>
<LogOut className="h-4 w-4 mr-2" />
Logout
</button>
</div>
</div>
</div>
{/* Main content */}
<div className="lg:pl-64">
{/* Top bar */}
<div className="sticky top-0 z-10 flex items-center h-16 px-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 lg:px-8">
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<Menu className="h-6 w-6" />
</button>
<div className="flex-1 flex items-center justify-between lg:justify-end">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white lg:hidden">
Admin
</h2>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600 dark:text-gray-400">
{new Date().toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
</div> </div>
</div> </div>
</div> </div>
</main>
{/* Page content */} </SidebarProvider>
<main className="p-4 lg:p-8">
<Outlet />
</main>
</div>
</div>
) )
} }

View File

@@ -0,0 +1,129 @@
import { LayoutDashboard, CreditCard, Wallet, Users, Settings, LogOut } from 'lucide-react'
import { Logo } from '../Logo'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar'
import { useAuth } from '@/contexts/AuthContext'
import { getAvatarUrl } from '@/lib/utils'
const items = [
{
title: 'Dashboard',
url: '/admin',
icon: LayoutDashboard,
},
{
title: 'Plans',
url: '/admin/plans',
icon: CreditCard,
},
{
title: 'Payment Methods',
url: '/admin/payment-methods',
icon: Wallet,
},
{
title: 'Payments',
url: '/admin/payments',
icon: CreditCard,
},
{
title: 'Users',
url: '/admin/users',
icon: Users,
},
{
title: 'Settings',
url: '/admin/settings',
icon: Settings,
},
]
interface AdminSidebarProps {
currentPage: string
onNavigate: (page: string) => void
}
export function AdminSidebar({ currentPage, onNavigate }: AdminSidebarProps) {
const { user, logout } = useAuth()
return (
<Sidebar>
<SidebarHeader className="p-4">
<div className="mx-auto">
<Logo variant="large" />
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => {
const isActive = currentPage === item.url
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
isActive={isActive}
onClick={() => onNavigate(item.url)}
className={`${
isActive
? 'bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary'
: ''
}`}
>
<button className="w-full">
<item.icon />
<span>{item.title}</span>
</button>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
{getAvatarUrl(user?.avatarUrl) ? (
<img
src={getAvatarUrl(user?.avatarUrl)!}
alt={user?.name || user?.email || 'Admin'}
className="h-10 w-10 rounded-full"
/>
) : (
<div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-primary-foreground font-semibold">
{user?.name?.[0] || user?.email[0].toUpperCase()}
</div>
)}
<div className="flex flex-col min-w-0">
{user?.name && (
<span className="text-sm font-medium truncate">{user.name}</span>
)}
<span className="text-xs text-muted-foreground truncate">
{user?.email}
</span>
</div>
</div>
</div>
<button
onClick={logout}
className="w-full mt-3 flex items-center justify-center px-4 py-2 text-sm font-medium text-destructive-foreground bg-destructive rounded-lg hover:bg-destructive/90 transition-colors"
>
<LogOut className="h-4 w-4 mr-2" />
Logout
</button>
</SidebarFooter>
</Sidebar>
)
}

View File

@@ -42,7 +42,7 @@ export function AdminDashboard() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-gray-600 dark:text-gray-400">Loading...</div> <div className="text-muted-foreground">Memuat...</div>
</div> </div>
) )
} }
@@ -81,11 +81,11 @@ export function AdminDashboard() {
return ( return (
<div> <div>
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> <h1 className="text-3xl font-bold text-foreground">
Dashboard Dashboard
</h1> </h1>
<p className="mt-2 text-gray-600 dark:text-gray-400"> <p className="mt-2 text-muted-foreground">
Welcome to the admin panel Selamat datang di panel admin
</p> </p>
</div> </div>
@@ -94,17 +94,17 @@ export function AdminDashboard() {
{statCards.map((stat) => ( {statCards.map((stat) => (
<div <div
key={stat.name} key={stat.name}
className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 border border-gray-200 dark:border-gray-700" className="bg-card rounded-lg shadow p-6 border border-border"
> >
<div className="flex items-center"> <div className="flex items-center">
<div className={`p-3 rounded-lg ${stat.bgColor}`}> <div className={`p-3 rounded-lg ${stat.bgColor}`}>
<stat.icon className={`h-6 w-6 text-white ${stat.color}`} /> <stat.icon className={`h-6 w-6 text-white ${stat.color}`} />
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400"> <p className="text-sm font-medium text-muted-foreground">
{stat.name} {stat.name}
</p> </p>
<p className="text-2xl font-bold text-gray-900 dark:text-white"> <p className="text-2xl font-bold text-foreground">
{stat.value} {stat.value}
</p> </p>
</div> </div>
@@ -114,36 +114,36 @@ export function AdminDashboard() {
</div> </div>
{/* Quick Actions */} {/* Quick Actions */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6"> <div className="bg-card rounded-lg shadow border border-border p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-semibold text-foreground mb-4">
Quick Actions Quick Actions
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<a <a
href="/admin/plans" href="/admin/plans"
className="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" className="flex items-center p-4 border border-border rounded-lg hover:bg-accent transition-colors"
> >
<CreditCard className="h-5 w-5 text-blue-600 dark:text-blue-400 mr-3" /> <CreditCard className="h-5 w-5 text-primary mr-3" />
<span className="text-sm font-medium text-gray-900 dark:text-white"> <span className="text-sm font-medium text-foreground">
Manage Plans Kelola Plans
</span> </span>
</a> </a>
<a <a
href="/admin/payments" href="/admin/payments"
className="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" className="flex items-center p-4 border border-border rounded-lg hover:bg-accent transition-colors"
> >
<DollarSign className="h-5 w-5 text-green-600 dark:text-green-400 mr-3" /> <DollarSign className="h-5 w-5 text-primary mr-3" />
<span className="text-sm font-medium text-gray-900 dark:text-white"> <span className="text-sm font-medium text-foreground">
Verify Payments Verifikasi Pembayaran
</span> </span>
</a> </a>
<a <a
href="/admin/users" href="/admin/users"
className="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" className="flex items-center p-4 border border-border rounded-lg hover:bg-accent transition-colors"
> >
<Users className="h-5 w-5 text-purple-600 dark:text-purple-400 mr-3" /> <Users className="h-5 w-5 text-primary mr-3" />
<span className="text-sm font-medium text-gray-900 dark:text-white"> <span className="text-sm font-medium text-foreground">
Manage Users Kelola Users
</span> </span>
</a> </a>
</div> </div>

View File

@@ -92,7 +92,7 @@ export function AdminPlans() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-gray-600 dark:text-gray-400">Loading...</div> <div className="text-muted-foreground">Memuat...</div>
</div> </div>
) )
} }
@@ -101,11 +101,11 @@ export function AdminPlans() {
<div> <div>
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> <h1 className="text-3xl font-bold text-foreground">
Plans Management Kelola Plans
</h1> </h1>
<p className="mt-2 text-gray-600 dark:text-gray-400"> <p className="mt-2 text-muted-foreground">
Manage subscription plans Kelola paket berlangganan
</p> </p>
</div> </div>
<button <button
@@ -113,10 +113,10 @@ export function AdminPlans() {
setEditingPlan(null) setEditingPlan(null)
setShowModal(true) setShowModal(true)
}} }}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" className="flex items-center px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
> >
<Plus className="h-5 w-5 mr-2" /> <Plus className="h-5 w-5 mr-2" />
Add Plan Tambah Plan
</button> </button>
</div> </div>
@@ -125,14 +125,14 @@ export function AdminPlans() {
{plans.map((plan) => ( {plans.map((plan) => (
<div <div
key={plan.id} key={plan.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden" className="bg-card rounded-lg shadow border border-border overflow-hidden"
> >
{/* Header */} {/* Header */}
<div className="p-6 border-b border-gray-200 dark:border-gray-700"> <div className="p-6 border-b border-border">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<div className="flex items-center"> <div className="flex items-center">
<GripVertical className="h-5 w-5 text-gray-400 mr-2 cursor-move" /> <GripVertical className="h-5 w-5 text-muted-foreground mr-2 cursor-move" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white"> <h3 className="text-lg font-semibold text-foreground">
{plan.name} {plan.name}
</h3> </h3>
</div> </div>
@@ -150,17 +150,17 @@ export function AdminPlans() {
</span> </span>
)} )}
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-muted-foreground">
{plan.description} {plan.description}
</p> </p>
</div> </div>
{/* Price */} {/* Price */}
<div className="p-6 bg-gray-50 dark:bg-gray-900/50"> <div className="p-6 bg-muted">
<div className="text-3xl font-bold text-gray-900 dark:text-white"> <div className="text-3xl font-bold text-foreground">
{formatPrice(plan.price, plan.currency)} {formatPrice(plan.price, plan.currency)}
</div> </div>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1"> <div className="text-sm text-muted-foreground mt-1">
{plan.durationType === 'lifetime' {plan.durationType === 'lifetime'
? 'Lifetime access' ? 'Lifetime access'
: plan.durationType === 'monthly' : plan.durationType === 'monthly'
@@ -170,24 +170,24 @@ export function AdminPlans() {
: plan.durationType} : plan.durationType}
</div> </div>
{plan.trialDays > 0 && ( {plan.trialDays > 0 && (
<div className="text-sm text-blue-600 dark:text-blue-400 mt-1"> <div className="text-sm text-primary mt-1">
{plan.trialDays} days free trial {plan.trialDays} days free trial
</div> </div>
)} )}
</div> </div>
{/* Stats */} {/* Stats */}
<div className="p-6 border-t border-gray-200 dark:border-gray-700"> <div className="p-6 border-t border-border">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400"> <span className="text-muted-foreground">
Subscriptions: Subscriptions:
</span> </span>
<span className="font-medium text-gray-900 dark:text-white"> <span className="font-medium text-foreground">
{plan._count.subscriptions} {plan._count.subscriptions}
</span> </span>
</div> </div>
<div className="flex items-center justify-between text-sm mt-2"> <div className="flex items-center justify-between text-sm mt-2">
<span className="text-gray-600 dark:text-gray-400">Status:</span> <span className="text-muted-foreground">Status:</span>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{plan.isActive && ( {plan.isActive && (
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400 rounded"> <span className="px-2 py-1 text-xs bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400 rounded">
@@ -197,17 +197,17 @@ export function AdminPlans() {
{plan.isVisible ? ( {plan.isVisible ? (
<Eye className="h-4 w-4 text-green-600 dark:text-green-400" /> <Eye className="h-4 w-4 text-green-600 dark:text-green-400" />
) : ( ) : (
<EyeOff className="h-4 w-4 text-gray-400" /> <EyeOff className="h-4 w-4 text-muted-foreground" />
)} )}
</div> </div>
</div> </div>
</div> </div>
{/* Actions */} {/* Actions */}
<div className="p-4 bg-gray-50 dark:bg-gray-900/50 border-t border-gray-200 dark:border-gray-700 flex items-center justify-end space-x-2"> <div className="p-4 bg-muted border-t border-border flex items-center justify-end space-x-2">
<button <button
onClick={() => toggleVisibility(plan)} onClick={() => toggleVisibility(plan)}
className="p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors" className="p-2 text-muted-foreground hover:text-foreground transition-colors"
title={plan.isVisible ? 'Hide' : 'Show'} title={plan.isVisible ? 'Hide' : 'Show'}
> >
{plan.isVisible ? ( {plan.isVisible ? (
@@ -221,14 +221,14 @@ export function AdminPlans() {
setEditingPlan(plan) setEditingPlan(plan)
setShowModal(true) setShowModal(true)
}} }}
className="p-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" className="p-2 text-primary hover:text-primary/80 transition-colors"
title="Edit" title="Edit"
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</button> </button>
<button <button
onClick={() => handleDelete(plan.id)} onClick={() => handleDelete(plan.id)}
className="p-2 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 transition-colors" className="p-2 text-destructive hover:text-destructive/80 transition-colors"
title="Delete" title="Delete"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
@@ -240,7 +240,7 @@ export function AdminPlans() {
{plans.length === 0 && ( {plans.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">No plans found</p> <p className="text-muted-foreground">Tidak ada plan</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -90,7 +90,7 @@ export function AdminUsers() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="text-gray-600 dark:text-gray-400">Loading...</div> <div className="text-muted-foreground">Memuat...</div>
</div> </div>
) )
} }
@@ -98,64 +98,64 @@ export function AdminUsers() {
return ( return (
<div> <div>
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> <h1 className="text-3xl font-bold text-foreground">
Users Management Kelola Users
</h1> </h1>
<p className="mt-2 text-gray-600 dark:text-gray-400"> <p className="mt-2 text-muted-foreground">
Manage user accounts and permissions Kelola akun dan izin pengguna
</p> </p>
</div> </div>
{/* Search */} {/* Search */}
<div className="mb-6"> <div className="mb-6">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<input <input
type="text" type="text"
placeholder="Search by email or name..." placeholder="Search by email or name..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full pl-10 pr-4 py-2 border border-input rounded-lg bg-background text-foreground focus:ring-2 focus:ring-ring focus:border-transparent"
/> />
</div> </div>
</div> </div>
{/* Users Table */} {/* Users Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="bg-card rounded-lg shadow border border-border overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table className="min-w-full divide-y divide-border">
<thead className="bg-gray-50 dark:bg-gray-900/50"> <thead className="bg-muted">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
User User
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Role Role
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Stats Stats
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Status Status
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Actions Actions
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="bg-card divide-y divide-border">
{users.map((user) => ( {users.map((user) => (
<tr key={user.id}> <tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold"> <div className="flex-shrink-0 h-10 w-10 rounded-full bg-primary flex items-center justify-center text-primary-foreground font-semibold">
{user.name?.[0] || user.email[0].toUpperCase()} {user.name?.[0] || user.email[0].toUpperCase()}
</div> </div>
<div className="ml-4"> <div className="ml-4">
<div className="text-sm font-medium text-gray-900 dark:text-white"> <div className="text-sm font-medium text-foreground">
{user.name || 'No name'} {user.name || 'No name'}
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-muted-foreground">
{user.email} {user.email}
</div> </div>
</div> </div>
@@ -172,7 +172,7 @@ export function AdminUsers() {
{user.role} {user.role}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> <td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
<div>{user._count.wallets} wallets</div> <div>{user._count.wallets} wallets</div>
<div>{user._count.transactions} transactions</div> <div>{user._count.transactions} transactions</div>
</td> </td>
@@ -195,7 +195,7 @@ export function AdminUsers() {
{user.suspendedAt ? ( {user.suspendedAt ? (
<button <button
onClick={() => handleSuspend(user.id, false)} onClick={() => handleSuspend(user.id, false)}
className="text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300" className="text-primary hover:text-primary/80"
title="Unsuspend" title="Unsuspend"
> >
<UserCheck className="h-4 w-4 inline" /> <UserCheck className="h-4 w-4 inline" />
@@ -203,7 +203,7 @@ export function AdminUsers() {
) : ( ) : (
<button <button
onClick={() => handleSuspend(user.id, true)} onClick={() => handleSuspend(user.id, true)}
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" className="text-destructive hover:text-destructive/80"
title="Suspend" title="Suspend"
> >
<UserX className="h-4 w-4 inline" /> <UserX className="h-4 w-4 inline" />
@@ -211,7 +211,7 @@ export function AdminUsers() {
)} )}
<button <button
onClick={() => handleGrantPro(user.id)} onClick={() => handleGrantPro(user.id)}
className="text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300" className="text-primary hover:text-primary/80"
title="Grant Pro Access" title="Grant Pro Access"
> >
<Crown className="h-4 w-4 inline" /> <Crown className="h-4 w-4 inline" />
@@ -226,7 +226,7 @@ export function AdminUsers() {
{users.length === 0 && ( {users.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">No users found</p> <p className="text-muted-foreground">Tidak ada user</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -6,7 +6,6 @@ import {
SidebarFooter, SidebarFooter,
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader, SidebarHeader,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
@@ -55,23 +54,30 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu className="flex-1 space-y-1 overflow-y-auto">
{items.map((item) => ( {items.map((item) => {
<SidebarMenuItem key={item.title}> const isActive = currentPage === item.url
<SidebarMenuButton return (
asChild <SidebarMenuItem key={item.title}>
isActive={currentPage === item.url} <SidebarMenuButton
onClick={() => onNavigate(item.url)} asChild
> isActive={isActive}
<button className="w-full"> onClick={() => onNavigate(item.url)}
<item.icon /> className={`flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${
<span>{item.title}</span> isActive
</button> ? 'bg-primary/10 text-primary'
</SidebarMenuButton> : 'text-foreground hover:bg-accent hover:text-accent-foreground'
</SidebarMenuItem> }`}
))} >
<button className="w-full">
<item.icon />
<span>{item.title}</span>
</button>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
@@ -83,11 +89,11 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
<img <img
src={getAvatarUrl(user?.avatarUrl)!} src={getAvatarUrl(user?.avatarUrl)!}
alt={user?.name || user?.email || 'User'} alt={user?.name || user?.email || 'User'}
className="h-8 w-8 rounded-full" className="h-10 w-10 rounded-full"
/> />
) : ( ) : (
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center"> <div className="h-10 w-10 rounded-full bg-primary flex items-center justify-center text-primary-foreground font-semibold">
<User className="h-4 w-4" /> {user?.name?.[0] || user?.email[0].toUpperCase()}
</div> </div>
)} )}
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
@@ -97,14 +103,14 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
<span className="text-xs text-muted-foreground truncate">{user?.email}</span> <span className="text-xs text-muted-foreground truncate">{user?.email}</span>
</div> </div>
</div> </div>
<button
onClick={logout}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 flex-shrink-0"
title="Sign out"
>
<LogOut className="h-4 w-4" />
</button>
</div> </div>
<button
onClick={logout}
className="w-full mt-3 flex items-center justify-center px-4 py-2 text-sm font-medium text-destructive-foreground bg-destructive rounded-lg hover:bg-destructive/90 transition-colors"
>
<LogOut className="h-4 w-4 mr-2" />
Logout
</button>
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
) )

View File

@@ -19,6 +19,14 @@ export function DashboardLayout({ children, currentPage, onNavigate }: Dashboard
<SidebarTrigger className="-ml-1 md:h-8 md:w-8 h-10 w-10" /> <SidebarTrigger className="-ml-1 md:h-8 md:w-8 h-10 w-10" />
<Breadcrumb currentPage={currentPage} /> <Breadcrumb currentPage={currentPage} />
<div className="flex-1" /> <div className="flex-1" />
<span className="text-sm text-muted-foreground hidden sm:block">
{new Date().toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
<ThemeToggle /> <ThemeToggle />
</header> </header>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">