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:
4
apps/api/dist/auth/auth.controller.d.ts
vendored
4
apps/api/dist/auth/auth.controller.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
5
apps/api/dist/auth/auth.service.d.ts
vendored
5
apps/api/dist/auth/auth.service.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
6
apps/api/dist/auth/auth.service.js
vendored
6
apps/api/dist/auth/auth.service.js
vendored
@@ -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) {
|
||||||
|
|||||||
2
apps/api/dist/auth/auth.service.js.map
vendored
2
apps/api/dist/auth/auth.service.js.map
vendored
File diff suppressed because one or more lines are too long
2
apps/api/dist/tsconfig.build.tsbuildinfo
vendored
2
apps/api/dist/tsconfig.build.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
33
apps/web/src/components/admin/AdminBreadcrumb.tsx
Normal file
33
apps/web/src/components/admin/AdminBreadcrumb.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
129
apps/web/src/components/admin/AdminSidebar.tsx
Normal file
129
apps/web/src/components/admin/AdminSidebar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user