mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-02-10 17:40:30 -08:00
UI Overhaul
This commit is contained in:
parent
c3db15827d
commit
fdb7b19cbc
@ -68,7 +68,7 @@ export default function AuditPage() {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={e => { setSearch(e.target.value); setPage(1) }}
|
onChange={e => { setSearch(e.target.value); setPage(1) }}
|
||||||
placeholder="Search audit logs..."
|
placeholder="Search audit logs..."
|
||||||
className="w-full bg-gray-800 border border-gray-700 text-gray-100 text-sm rounded-lg pl-10 pr-4 py-2 focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 outline-none"
|
className="w-full glass-input text-gray-100 text-sm rounded-lg pl-10 pr-4 py-2 focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,7 +81,7 @@ export default function AuditPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center p-12">
|
<div className="flex items-center justify-center p-12">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
@ -95,7 +95,7 @@ export default function AuditPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-800/50">
|
<thead className="bg-white/[0.05]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Timestamp</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Timestamp</th>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">User</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">User</th>
|
||||||
@ -105,9 +105,9 @@ export default function AuditPage() {
|
|||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">IP</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">IP</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody className="divide-y divide-white/[0.08]">
|
||||||
{logs.map(log => (
|
{logs.map(log => (
|
||||||
<tr key={log._id} className="hover:bg-gray-800">
|
<tr key={log._id} className="hover:bg-white/[0.06]">
|
||||||
<td className="px-6 py-3 text-xs text-gray-400 whitespace-nowrap">
|
<td className="px-6 py-3 text-xs text-gray-400 whitespace-nowrap">
|
||||||
{formatDateTime(log.createdAt)}
|
{formatDateTime(log.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
@ -139,7 +139,7 @@ export default function AuditPage() {
|
|||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-700">
|
<div className="flex items-center justify-between px-6 py-3 border-t border-white/[0.1]">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Page {page} of {totalPages}
|
Page {page} of {totalPages}
|
||||||
</p>
|
</p>
|
||||||
@ -147,14 +147,14 @@ export default function AuditPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
className="px-3 py-1.5 text-sm bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1.5 text-sm bg-white/[0.08] border border-white/[0.12] hover:border-white/[0.2] text-gray-200 rounded-lg hover:bg-white/[0.14] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={page === totalPages}
|
disabled={page === totalPages}
|
||||||
className="px-3 py-1.5 text-sm bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1.5 text-sm bg-white/[0.08] border border-white/[0.12] hover:border-white/[0.2] text-gray-200 rounded-lg hover:bg-white/[0.14] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -93,9 +93,9 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server List */}
|
{/* Server List */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||||
<h2 className="text-lg font-semibold text-gray-100">Servers</h2>
|
<h2 className="text-lg font-semibold text-white">Servers</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{servers.length === 0 ? (
|
{servers.length === 0 ? (
|
||||||
@ -103,12 +103,12 @@ export default function DashboardPage() {
|
|||||||
No servers yet. Create your first Minecraft server!
|
No servers yet. Create your first Minecraft server!
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-700">
|
<div className="divide-y divide-white/[0.08]">
|
||||||
{servers.map(server => (
|
{servers.map(server => (
|
||||||
<a
|
<a
|
||||||
key={server._id}
|
key={server._id}
|
||||||
href={`/servers/${server._id}`}
|
href={`/servers/${server._id}`}
|
||||||
className="flex items-center justify-between px-4 sm:px-6 py-4 hover:bg-gray-800 transition-colors gap-3"
|
className="flex items-center justify-between px-4 sm:px-6 py-4 hover:bg-white/[0.06] transition-colors gap-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Server size={20} className="text-gray-500" />
|
<Server size={20} className="text-gray-500" />
|
||||||
@ -140,21 +140,21 @@ function StatCard({
|
|||||||
value: number
|
value: number
|
||||||
color: string
|
color: string
|
||||||
}) {
|
}) {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, { badge: string; glow: string }> = {
|
||||||
cyan: 'bg-cyan-500/20 text-cyan-400',
|
cyan: { badge: 'bg-cyan-400/20 text-cyan-400', glow: 'shadow-cyan-500/15' },
|
||||||
emerald: 'bg-emerald-500/20 text-emerald-400',
|
emerald: { badge: 'bg-emerald-400/20 text-emerald-400', glow: 'shadow-emerald-500/15' },
|
||||||
amber: 'bg-amber-500/20 text-amber-400',
|
amber: { badge: 'bg-amber-400/20 text-amber-400', glow: 'shadow-amber-500/15' },
|
||||||
gray: 'bg-gray-500/20 text-gray-400',
|
gray: { badge: 'bg-white/[0.1] text-gray-400', glow: '' },
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-5">
|
<div className={`glass rounded-xl p-5 ${colorMap[color].glow ? `shadow-lg ${colorMap[color].glow}` : ''}`}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`p-2.5 rounded-lg ${colorMap[color]}`}>
|
<div className={`p-2.5 rounded-lg ${colorMap[color].badge}`}>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-bold text-gray-100">{value}</p>
|
<p className="text-2xl font-bold text-white">{value}</p>
|
||||||
<p className="text-xs text-gray-500">{label}</p>
|
<p className="text-xs text-gray-500">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-950">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -18,9 +18,9 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
if (!user) return null // Middleware will redirect
|
if (!user) return null // Middleware will redirect
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-950">
|
<div className="flex h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 overflow-y-auto p-4 pt-18 lg:p-6 lg:pt-6">
|
<main className="flex-1 overflow-y-auto overflow-x-hidden p-4 pt-[4.5rem] lg:p-6 lg:pt-6">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -233,7 +233,7 @@ export default function RolesPage() {
|
|||||||
const PermissionGrid = () => (
|
const PermissionGrid = () => (
|
||||||
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
||||||
{allResources.map(resource => (
|
{allResources.map(resource => (
|
||||||
<div key={resource} className="bg-gray-800/50 rounded-lg p-3">
|
<div key={resource} className="bg-white/[0.06] rounded-lg p-3 border border-white/[0.1]">
|
||||||
<p className="text-xs font-semibold text-gray-300 uppercase tracking-wider mb-2">{resource}</p>
|
<p className="text-xs font-semibold text-gray-300 uppercase tracking-wider mb-2">{resource}</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{allActions[resource].map(action => (
|
{allActions[resource].map(action => (
|
||||||
@ -241,10 +241,10 @@ export default function RolesPage() {
|
|||||||
key={`${resource}:${action}`}
|
key={`${resource}:${action}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => togglePermission(resource, action)}
|
onClick={() => togglePermission(resource, action)}
|
||||||
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors ${
|
className={`px-2.5 py-1 rounded text-xs font-medium transition-all ${
|
||||||
hasPermissionToggle(resource, action)
|
hasPermissionToggle(resource, action)
|
||||||
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50'
|
? 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/35 shadow-[0_0_10px_rgba(6,182,212,0.2)]'
|
||||||
: 'bg-gray-700 text-gray-400 border border-gray-600 hover:bg-gray-600'
|
: 'bg-white/[0.08] text-gray-400 border border-white/[0.12] hover:bg-white/[0.14] hover:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{action}
|
{action}
|
||||||
|
|||||||
@ -152,7 +152,7 @@ export default function BackupsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{backups.length === 0 ? (
|
{backups.length === 0 ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
<HardDrive size={40} className="mx-auto mb-3 text-gray-600" />
|
<HardDrive size={40} className="mx-auto mb-3 text-gray-600" />
|
||||||
@ -161,7 +161,7 @@ export default function BackupsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-800/50">
|
<thead className="bg-white/[0.05]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Filename</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Filename</th>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Type</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Type</th>
|
||||||
@ -171,9 +171,9 @@ export default function BackupsPage() {
|
|||||||
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
|
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody className="divide-y divide-white/[0.08]">
|
||||||
{backups.map(backup => (
|
{backups.map(backup => (
|
||||||
<tr key={backup._id} className="hover:bg-gray-800">
|
<tr key={backup._id} className="hover:bg-white/[0.06]">
|
||||||
<td className="px-6 py-4 text-sm text-gray-200 font-mono">{backup.filename}</td>
|
<td className="px-6 py-4 text-sm text-gray-200 font-mono">{backup.filename}</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<Badge variant={backup.type === 'manual' ? 'info' : 'neutral'}>
|
<Badge variant={backup.type === 'manual' ? 'info' : 'neutral'}>
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export default function ConfigurationPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* JVM Args */}
|
{/* JVM Args */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-6">
|
<div className="glass rounded-xl p-6">
|
||||||
<h3 className="text-sm font-semibold text-gray-200 mb-3">JVM Arguments</h3>
|
<h3 className="text-sm font-semibold text-gray-200 mb-3">JVM Arguments</h3>
|
||||||
<Input
|
<Input
|
||||||
value={jvmArgs}
|
value={jvmArgs}
|
||||||
@ -132,11 +132,11 @@ export default function ConfigurationPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server Properties */}
|
{/* Server Properties */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||||
<h3 className="text-sm font-semibold text-gray-200">server.properties</h3>
|
<h3 className="text-sm font-semibold text-gray-200">server.properties</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-gray-700">
|
<div className="divide-y divide-white/[0.08]">
|
||||||
{sortedKeys.map(key => (
|
{sortedKeys.map(key => (
|
||||||
<div key={key} className="flex items-center gap-4 px-6 py-3">
|
<div key={key} className="flex items-center gap-4 px-6 py-3">
|
||||||
<label className="w-1/3 text-sm text-gray-400 font-mono truncate" title={key}>
|
<label className="w-1/3 text-sm text-gray-400 font-mono truncate" title={key}>
|
||||||
@ -145,7 +145,7 @@ export default function ConfigurationPage() {
|
|||||||
<input
|
<input
|
||||||
value={properties[key]}
|
value={properties[key]}
|
||||||
onChange={e => handlePropertyChange(key, e.target.value)}
|
onChange={e => handlePropertyChange(key, e.target.value)}
|
||||||
className="flex-1 bg-gray-800 border border-gray-700 text-gray-100 text-sm rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 outline-none"
|
className="flex-1 glass-input text-gray-100 text-sm rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -640,7 +640,7 @@ export default function FilesPage() {
|
|||||||
{i > 0 && <ChevronRight size={14} className="text-gray-600" />}
|
{i > 0 && <ChevronRight size={14} className="text-gray-600" />}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateTo(crumb.path)}
|
onClick={() => navigateTo(crumb.path)}
|
||||||
className={`px-1.5 py-0.5 rounded hover:bg-gray-800 transition-colors ${
|
className={`px-1.5 py-0.5 rounded hover:bg-white/[0.06] transition-colors ${
|
||||||
i === breadcrumbs.length - 1
|
i === breadcrumbs.length - 1
|
||||||
? 'text-cyan-400 font-medium'
|
? 'text-cyan-400 font-medium'
|
||||||
: 'text-gray-400 hover:text-gray-200'
|
: 'text-gray-400 hover:text-gray-200'
|
||||||
@ -655,7 +655,7 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
{/* New Folder Inline Form */}
|
{/* New Folder Inline Form */}
|
||||||
{showNewFolder && !viewingFile && (
|
{showNewFolder && !viewingFile && (
|
||||||
<div className="flex items-center gap-3 bg-gray-900/80 p-3 rounded-lg border border-gray-700/50">
|
<div className="flex items-center gap-3 glass p-3 rounded-xl">
|
||||||
<FolderPlus size={18} className="text-amber-400 flex-shrink-0" />
|
<FolderPlus size={18} className="text-amber-400 flex-shrink-0" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -664,7 +664,7 @@ export default function FilesPage() {
|
|||||||
onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
|
onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
|
||||||
placeholder="Folder name…"
|
placeholder="Folder name…"
|
||||||
autoFocus
|
autoFocus
|
||||||
className="flex-1 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500"
|
className="flex-1 glass-input text-gray-100 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
<Button size="sm" variant="primary" onClick={handleCreateFolder} disabled={actionLoading === 'new-folder' || !newFolderName.trim()}>
|
<Button size="sm" variant="primary" onClick={handleCreateFolder} disabled={actionLoading === 'new-folder' || !newFolderName.trim()}>
|
||||||
Create
|
Create
|
||||||
@ -677,8 +677,8 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
{viewingFile ? (
|
{viewingFile ? (
|
||||||
/* ─── File Viewer / Editor ─────────────────────────────── */
|
/* ─── File Viewer / Editor ─────────────────────────────── */
|
||||||
<div className="bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="bg-[#050510]/80 backdrop-blur-xl rounded-xl border border-white/[0.08] overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700/50 bg-gray-900/50">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.1] bg-white/[0.04]">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<File size={16} className="text-cyan-400" />
|
<File size={16} className="text-cyan-400" />
|
||||||
<span className="text-sm font-medium text-gray-200">{fileMeta?.name || viewingFile}</span>
|
<span className="text-sm font-medium text-gray-200">{fileMeta?.name || viewingFile}</span>
|
||||||
@ -705,7 +705,7 @@ export default function FilesPage() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={editContent || ''}
|
value={editContent || ''}
|
||||||
onChange={(e) => setEditContent(e.target.value)}
|
onChange={(e) => setEditContent(e.target.value)}
|
||||||
className="w-full h-[calc(100vh-400px)] min-h-[400px] bg-gray-950 text-gray-300 font-mono text-sm p-4 resize-none focus:outline-none"
|
className="w-full h-[calc(100vh-400px)] min-h-[400px] bg-transparent text-gray-300 font-mono text-sm p-4 resize-none focus:outline-none"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -719,7 +719,7 @@ export default function FilesPage() {
|
|||||||
<div>
|
<div>
|
||||||
{/* ─── Bulk Action Bar ───────────────────────────────────── */}
|
{/* ─── Bulk Action Bar ───────────────────────────────────── */}
|
||||||
{selectMode && selected.size > 0 && (
|
{selectMode && selected.size > 0 && (
|
||||||
<div className="flex items-center gap-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg px-4 py-2.5 mb-4">
|
<div className="flex items-center gap-3 bg-cyan-400/5 border border-cyan-400/20 rounded-xl px-4 py-2.5 mb-4 backdrop-blur-sm">
|
||||||
<span className="text-sm font-medium text-cyan-400">
|
<span className="text-sm font-medium text-cyan-400">
|
||||||
{selected.size} selected
|
{selected.size} selected
|
||||||
</span>
|
</span>
|
||||||
@ -762,7 +762,7 @@ export default function FilesPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
onClick={clearSelection}
|
onClick={clearSelection}
|
||||||
className="p-1.5 rounded hover:bg-gray-700 text-gray-400 hover:text-gray-200 transition-colors"
|
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-400 hover:text-gray-200 transition-colors"
|
||||||
title="Clear selection"
|
title="Clear selection"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
@ -772,7 +772,7 @@ export default function FilesPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Directory Listing ────────────────────────────────── */}
|
{/* ─── Directory Listing ────────────────────────────────── */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
@ -783,10 +783,10 @@ export default function FilesPage() {
|
|||||||
<p className="text-gray-400">Empty directory</p>
|
<p className="text-gray-400">Empty directory</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-700/50">
|
<div className="divide-y divide-white/[0.08]">
|
||||||
{/* Select all header */}
|
{/* Select all header */}
|
||||||
{selectMode && entries.length > 0 && (
|
{selectMode && entries.length > 0 && (
|
||||||
<div className="flex items-center gap-3 px-5 py-2 bg-gray-800/30">
|
<div className="flex items-center gap-3 px-5 py-2 bg-white/[0.04]">
|
||||||
<button
|
<button
|
||||||
onClick={toggleSelectAll}
|
onClick={toggleSelectAll}
|
||||||
className="flex-shrink-0 text-gray-400 hover:text-cyan-400 transition-colors"
|
className="flex-shrink-0 text-gray-400 hover:text-cyan-400 transition-colors"
|
||||||
@ -811,7 +811,7 @@ export default function FilesPage() {
|
|||||||
parts.pop()
|
parts.pop()
|
||||||
navigateTo(parts.length ? parts.join('/') : '/')
|
navigateTo(parts.length ? parts.join('/') : '/')
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-3 px-5 py-3 hover:bg-gray-800/60 transition-colors text-left group"
|
className="w-full flex items-center gap-3 px-5 py-3 hover:bg-white/[0.06] transition-colors text-left group"
|
||||||
>
|
>
|
||||||
{selectMode && <div className="w-[18px] flex-shrink-0" />}
|
{selectMode && <div className="w-[18px] flex-shrink-0" />}
|
||||||
<Folder size={18} className="text-amber-400/70 flex-shrink-0" />
|
<Folder size={18} className="text-amber-400/70 flex-shrink-0" />
|
||||||
@ -822,8 +822,8 @@ export default function FilesPage() {
|
|||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<div
|
<div
|
||||||
key={entry.path}
|
key={entry.path}
|
||||||
className={`flex items-center gap-3 px-5 py-3 hover:bg-gray-800/60 transition-colors group relative ${
|
className={`flex items-center gap-3 px-5 py-3 hover:bg-white/[0.06] transition-colors group relative ${
|
||||||
selected.has(entry.path) ? 'bg-cyan-500/5' : ''
|
selected.has(entry.path) ? 'bg-cyan-400/[0.03]' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Checkbox */}
|
{/* Checkbox */}
|
||||||
@ -869,7 +869,7 @@ export default function FilesPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleUnzip(entry) }}
|
onClick={(e) => { e.stopPropagation(); handleUnzip(entry) }}
|
||||||
disabled={actionLoading === entry.path}
|
disabled={actionLoading === entry.path}
|
||||||
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-amber-400 transition-colors"
|
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-amber-400 transition-colors"
|
||||||
title="Extract archive"
|
title="Extract archive"
|
||||||
>
|
>
|
||||||
<ArchiveRestore size={15} />
|
<ArchiveRestore size={15} />
|
||||||
@ -879,7 +879,7 @@ export default function FilesPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleZip(entry) }}
|
onClick={(e) => { e.stopPropagation(); handleZip(entry) }}
|
||||||
disabled={actionLoading === entry.path}
|
disabled={actionLoading === entry.path}
|
||||||
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-cyan-400 transition-colors"
|
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-cyan-400 transition-colors"
|
||||||
title="Compress to zip"
|
title="Compress to zip"
|
||||||
>
|
>
|
||||||
<Archive size={15} />
|
<Archive size={15} />
|
||||||
@ -891,7 +891,7 @@ export default function FilesPage() {
|
|||||||
setMoveDest(entry.path)
|
setMoveDest(entry.path)
|
||||||
setMoveModal({ source: entry.path, name: entry.name })
|
setMoveModal({ source: entry.path, name: entry.name })
|
||||||
}}
|
}}
|
||||||
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-cyan-400 transition-colors"
|
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-cyan-400 transition-colors"
|
||||||
title="Move / Rename"
|
title="Move / Rename"
|
||||||
>
|
>
|
||||||
<FolderInput size={15} />
|
<FolderInput size={15} />
|
||||||
@ -904,7 +904,7 @@ export default function FilesPage() {
|
|||||||
setCopyDest(`${dir}${entry.name}-copy${entry.isDirectory ? '' : ''}`)
|
setCopyDest(`${dir}${entry.name}-copy${entry.isDirectory ? '' : ''}`)
|
||||||
setCopyModal({ source: entry.path, name: entry.name })
|
setCopyModal({ source: entry.path, name: entry.name })
|
||||||
}}
|
}}
|
||||||
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-cyan-400 transition-colors"
|
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-cyan-400 transition-colors"
|
||||||
title="Copy"
|
title="Copy"
|
||||||
>
|
>
|
||||||
<Copy size={15} />
|
<Copy size={15} />
|
||||||
@ -913,7 +913,7 @@ export default function FilesPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(entry) }}
|
onClick={(e) => { e.stopPropagation(); handleDelete(entry) }}
|
||||||
disabled={actionLoading === entry.path}
|
disabled={actionLoading === entry.path}
|
||||||
className="p-1.5 rounded hover:bg-gray-700 text-gray-500 hover:text-red-400 transition-colors"
|
className="p-1.5 rounded hover:bg-white/[0.08] text-gray-500 hover:text-red-400 transition-colors"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<Trash2 size={15} />
|
<Trash2 size={15} />
|
||||||
@ -942,9 +942,9 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
{/* ─── Copy Modal ────────────────────────────────────────── */}
|
{/* ─── Copy Modal ────────────────────────────────────────── */}
|
||||||
{copyModal && (
|
{copyModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
|
||||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Copy “{copyModal.name}”</h3>
|
<h3 className="text-lg font-semibold text-white mb-1">Copy “{copyModal.name}”</h3>
|
||||||
<p className="text-sm text-gray-400 mb-4">Enter the destination path (relative to server root):</p>
|
<p className="text-sm text-gray-400 mb-4">Enter the destination path (relative to server root):</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -952,7 +952,7 @@ export default function FilesPage() {
|
|||||||
onChange={(e) => setCopyDest(e.target.value)}
|
onChange={(e) => setCopyDest(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleCopy()}
|
onKeyDown={(e) => e.key === 'Enter' && handleCopy()}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
|
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
|
||||||
placeholder="path/to/destination"
|
placeholder="path/to/destination"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
@ -975,9 +975,9 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
{/* ─── Bulk Copy Modal ───────────────────────────────────── */}
|
{/* ─── Bulk Copy Modal ───────────────────────────────────── */}
|
||||||
{bulkCopyModal && (
|
{bulkCopyModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
|
||||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Copy {selected.size} item(s)</h3>
|
<h3 className="text-lg font-semibold text-white mb-1">Copy {selected.size} item(s)</h3>
|
||||||
<p className="text-sm text-gray-400 mb-4">Enter the destination folder (relative to server root). Items will be copied into this folder:</p>
|
<p className="text-sm text-gray-400 mb-4">Enter the destination folder (relative to server root). Items will be copied into this folder:</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -985,7 +985,7 @@ export default function FilesPage() {
|
|||||||
onChange={(e) => setBulkCopyDest(e.target.value)}
|
onChange={(e) => setBulkCopyDest(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleBulkCopy()}
|
onKeyDown={(e) => e.key === 'Enter' && handleBulkCopy()}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
|
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
|
||||||
placeholder="path/to/destination-folder"
|
placeholder="path/to/destination-folder"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
@ -1008,9 +1008,9 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
{/* ─── Move Modal ────────────────────────────────────────── */}
|
{/* ─── Move Modal ────────────────────────────────────────── */}
|
||||||
{moveModal && (
|
{moveModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
|
||||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Move “{moveModal.name}”</h3>
|
<h3 className="text-lg font-semibold text-white mb-1">Move “{moveModal.name}”</h3>
|
||||||
<p className="text-sm text-gray-400 mb-4">Enter the new path (relative to server root). Change the name to rename:</p>
|
<p className="text-sm text-gray-400 mb-4">Enter the new path (relative to server root). Change the name to rename:</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -1018,7 +1018,7 @@ export default function FilesPage() {
|
|||||||
onChange={(e) => setMoveDest(e.target.value)}
|
onChange={(e) => setMoveDest(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleMove()}
|
onKeyDown={(e) => e.key === 'Enter' && handleMove()}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
|
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
|
||||||
placeholder="path/to/new-location"
|
placeholder="path/to/new-location"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
@ -1041,9 +1041,9 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
{/* ─── Bulk Move Modal ───────────────────────────────────── */}
|
{/* ─── Bulk Move Modal ───────────────────────────────────── */}
|
||||||
{bulkMoveModal && (
|
{bulkMoveModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
<div className="glass-lg rounded-xl shadow-2xl shadow-black/40 w-full max-w-md p-6 mx-4 glass-shine">
|
||||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Move {selected.size} item(s)</h3>
|
<h3 className="text-lg font-semibold text-white mb-1">Move {selected.size} item(s)</h3>
|
||||||
<p className="text-sm text-gray-400 mb-4">Enter the destination folder (relative to server root). Items will be moved into this folder:</p>
|
<p className="text-sm text-gray-400 mb-4">Enter the destination folder (relative to server root). Items will be moved into this folder:</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -1051,7 +1051,7 @@ export default function FilesPage() {
|
|||||||
onChange={(e) => setBulkMoveDest(e.target.value)}
|
onChange={(e) => setBulkMoveDest(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleBulkMove()}
|
onKeyDown={(e) => e.key === 'Enter' && handleBulkMove()}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:border-transparent placeholder-gray-500 mb-4"
|
className="w-full glass-input text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent mb-4"
|
||||||
placeholder="path/to/destination-folder"
|
placeholder="path/to/destination-folder"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
|
|||||||
@ -129,8 +129,8 @@ export default function LogsPage() {
|
|||||||
|
|
||||||
{selectedFile ? (
|
{selectedFile ? (
|
||||||
/* Log File Viewer */
|
/* Log File Viewer */
|
||||||
<div className="bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="bg-[#050510]/80 backdrop-blur-xl rounded-xl border border-white/[0.08] overflow-hidden">
|
||||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50 bg-gray-900/50">
|
<div className="flex items-center gap-2 px-4 py-3 border-b border-white/[0.1] bg-white/[0.04]">
|
||||||
<FileText size={16} className="text-cyan-400" />
|
<FileText size={16} className="text-cyan-400" />
|
||||||
<span className="text-sm font-medium text-gray-200">{selectedFile}</span>
|
<span className="text-sm font-medium text-gray-200">{selectedFile}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -148,7 +148,7 @@ export default function LogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* File List */
|
/* File List */
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FileText size={40} className="mx-auto text-gray-600 mb-3" />
|
<FileText size={40} className="mx-auto text-gray-600 mb-3" />
|
||||||
@ -156,12 +156,12 @@ export default function LogsPage() {
|
|||||||
<p className="text-gray-600 text-sm mt-1">Logs will appear after the server has started</p>
|
<p className="text-gray-600 text-sm mt-1">Logs will appear after the server has started</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-700/50">
|
<div className="divide-y divide-white/[0.08]">
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<button
|
<button
|
||||||
key={file.name}
|
key={file.name}
|
||||||
onClick={() => openFile(file.name)}
|
onClick={() => openFile(file.name)}
|
||||||
className="w-full flex items-center gap-3 px-5 py-4 hover:bg-gray-800/60 transition-colors text-left group"
|
className="w-full flex items-center gap-3 px-5 py-4 hover:bg-white/[0.06] transition-colors text-left group"
|
||||||
>
|
>
|
||||||
<FileText size={18} className="text-gray-500 group-hover:text-cyan-400 transition-colors flex-shrink-0" />
|
<FileText size={18} className="text-gray-500 group-hover:text-cyan-400 transition-colors flex-shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@ -161,7 +161,7 @@ export default function ModsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{mods.length === 0 ? (
|
{mods.length === 0 ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
<Package size={40} className="mx-auto mb-3 text-gray-600" />
|
<Package size={40} className="mx-auto mb-3 text-gray-600" />
|
||||||
@ -170,7 +170,7 @@ export default function ModsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-800/50">
|
<thead className="bg-white/[0.05]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Mod</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Mod</th>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</th>
|
||||||
@ -178,9 +178,9 @@ export default function ModsPage() {
|
|||||||
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
|
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody className="divide-y divide-white/[0.08]">
|
||||||
{mods.map(mod => (
|
{mods.map(mod => (
|
||||||
<tr key={mod.filename} className="hover:bg-gray-800">
|
<tr key={mod.filename} className="hover:bg-white/[0.06]">
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<p className="text-sm font-medium text-gray-200">{mod.name}</p>
|
<p className="text-sm font-medium text-gray-200">{mod.name}</p>
|
||||||
<p className="text-xs text-gray-500 font-mono">{mod.filename}</p>
|
<p className="text-xs text-gray-500 font-mono">{mod.filename}</p>
|
||||||
|
|||||||
@ -266,10 +266,10 @@ export default function ServerDetailPage() {
|
|||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-5 hover:border-cyan-500/50 hover:bg-gray-800/60 transition-all group"
|
className="glass rounded-xl p-5 hover:border-cyan-500/40 hover:bg-white/[0.1] transition-all duration-200 group hover:shadow-lg hover:shadow-cyan-500/10"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2.5 rounded-lg bg-gray-800 group-hover:bg-cyan-500/20 transition-colors">
|
<div className="p-2.5 rounded-lg bg-white/[0.08] group-hover:bg-cyan-400/20 transition-colors">
|
||||||
<Icon size={20} className="text-gray-400 group-hover:text-cyan-400 transition-colors" />
|
<Icon size={20} className="text-gray-400 group-hover:text-cyan-400 transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-gray-200">{link.label}</span>
|
<span className="text-sm font-medium text-gray-200">{link.label}</span>
|
||||||
@ -284,9 +284,9 @@ export default function ServerDetailPage() {
|
|||||||
|
|
||||||
function InfoCard({ label, value }: { label: string; value: string }) {
|
function InfoCard({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4">
|
<div className="glass rounded-xl p-4">
|
||||||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">{label}</p>
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">{label}</p>
|
||||||
<p className="text-lg font-semibold text-gray-100 mt-1 capitalize">{value}</p>
|
<p className="text-lg font-semibold text-white mt-1 capitalize">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,14 +137,14 @@ export default function PlayersPage() {
|
|||||||
|
|
||||||
{/* Online Players */}
|
{/* Online Players */}
|
||||||
{onlinePlayers.length > 0 && (
|
{onlinePlayers.length > 0 && (
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||||
<h3 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
<h3 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
||||||
<div className="w-2 h-2 bg-emerald-400 rounded-full" />
|
<div className="w-2 h-2 bg-emerald-400 rounded-full" />
|
||||||
Online ({onlinePlayers.length})
|
Online ({onlinePlayers.length})
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-gray-700">
|
<div className="divide-y divide-white/[0.08]">
|
||||||
{onlinePlayers.map(player => (
|
{onlinePlayers.map(player => (
|
||||||
<PlayerRow
|
<PlayerRow
|
||||||
key={player.name}
|
key={player.name}
|
||||||
@ -159,8 +159,8 @@ export default function PlayersPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* All Known Players */}
|
{/* All Known Players */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||||
<h3 className="text-sm font-semibold text-gray-200">
|
<h3 className="text-sm font-semibold text-gray-200">
|
||||||
All Players ({players.length})
|
All Players ({players.length})
|
||||||
</h3>
|
</h3>
|
||||||
@ -172,7 +172,7 @@ export default function PlayersPage() {
|
|||||||
<p className="text-sm mt-1">Add players to the whitelist or start the server to see players.</p>
|
<p className="text-sm mt-1">Add players to the whitelist or start the server to see players.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-700">
|
<div className="divide-y divide-white/[0.08]">
|
||||||
{[...onlinePlayers, ...offlinePlayers].map(player => (
|
{[...onlinePlayers, ...offlinePlayers].map(player => (
|
||||||
<PlayerRow
|
<PlayerRow
|
||||||
key={player.name}
|
key={player.name}
|
||||||
@ -200,10 +200,10 @@ export default function PlayersPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAddAction('whitelist')}
|
onClick={() => setAddAction('whitelist')}
|
||||||
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-colors ${
|
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
|
||||||
addAction === 'whitelist'
|
addAction === 'whitelist'
|
||||||
? 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400'
|
? 'bg-cyan-400/20 border-cyan-400/35 text-cyan-400 shadow-lg shadow-cyan-500/10'
|
||||||
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700'
|
: 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Add to Whitelist
|
Add to Whitelist
|
||||||
@ -211,10 +211,10 @@ export default function PlayersPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAddAction('op')}
|
onClick={() => setAddAction('op')}
|
||||||
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-colors ${
|
className={`flex-1 p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
|
||||||
addAction === 'op'
|
addAction === 'op'
|
||||||
? 'bg-amber-500/20 border-amber-500/50 text-amber-400'
|
? 'bg-amber-400/20 border-amber-400/35 text-amber-400 shadow-lg shadow-amber-500/10'
|
||||||
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700'
|
: 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Make Operator
|
Make Operator
|
||||||
|
|||||||
@ -161,7 +161,7 @@ export default function PluginsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{plugins.length === 0 ? (
|
{plugins.length === 0 ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
<Puzzle size={40} className="mx-auto mb-3 text-gray-600" />
|
<Puzzle size={40} className="mx-auto mb-3 text-gray-600" />
|
||||||
@ -170,7 +170,7 @@ export default function PluginsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-800/50">
|
<thead className="bg-white/[0.05]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Plugin</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Plugin</th>
|
||||||
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</th>
|
<th className="text-left text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Size</th>
|
||||||
@ -178,9 +178,9 @@ export default function PluginsPage() {
|
|||||||
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
|
<th className="text-right text-xs font-medium text-gray-400 uppercase tracking-wider px-6 py-3">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody className="divide-y divide-white/[0.08]">
|
||||||
{plugins.map(plugin => (
|
{plugins.map(plugin => (
|
||||||
<tr key={plugin.filename} className="hover:bg-gray-800">
|
<tr key={plugin.filename} className="hover:bg-white/[0.06]">
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<p className="text-sm font-medium text-gray-200">{plugin.name}</p>
|
<p className="text-sm font-medium text-gray-200">{plugin.name}</p>
|
||||||
<p className="text-xs text-gray-500 font-mono">{plugin.filename}</p>
|
<p className="text-xs text-gray-500 font-mono">{plugin.filename}</p>
|
||||||
|
|||||||
@ -6,8 +6,94 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
background-color: #030712; /* gray-950 */
|
background-color: #050510;
|
||||||
color: #f3f4f6; /* gray-100 */
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Glassmorphism Utilities ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
backdrop-filter: blur(24px) saturate(1.4);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(1.4);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-lg {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
backdrop-filter: blur(40px) saturate(1.5);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(1.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 48px rgba(0, 0, 0, 0.5),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-surface {
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
backdrop-filter: blur(16px) saturate(1.3);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(1.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-input {
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-input:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(6, 182, 212, 0.6);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 2px 4px rgba(0, 0, 0, 0.2),
|
||||||
|
0 0 0 3px rgba(6, 182, 212, 0.15),
|
||||||
|
0 0 24px rgba(6, 182, 212, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Glass Shine (top border gradient) ───────────────────────── */
|
||||||
|
|
||||||
|
.glass-shine {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-shine::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 5%;
|
||||||
|
right: 5%;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18), transparent);
|
||||||
|
border-radius: 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Glow Effects ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.glow-cyan {
|
||||||
|
box-shadow: 0 0 30px rgba(6, 182, 212, 0.25), 0 0 80px rgba(6, 182, 212, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-emerald {
|
||||||
|
box-shadow: 0 0 30px rgba(16, 185, 129, 0.25), 0 0 80px rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-amber {
|
||||||
|
box-shadow: 0 0 30px rgba(245, 158, 11, 0.25), 0 0 80px rgba(245, 158, 11, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-red {
|
||||||
|
box-shadow: 0 0 30px rgba(239, 68, 68, 0.25), 0 0 80px rgba(239, 68, 68, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Animations ──────────────────────────────────────────────── */
|
/* ─── Animations ──────────────────────────────────────────────── */
|
||||||
@ -18,8 +104,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scale-in {
|
@keyframes scale-in {
|
||||||
from { opacity: 0; transform: scale(0.95); }
|
from { opacity: 0; transform: scale(0.96) translateY(8px); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-right {
|
@keyframes slide-in-right {
|
||||||
@ -32,40 +118,57 @@ body {
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
|
50% { transform: translateY(-20px) scale(1.02); }
|
||||||
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fade-in 200ms ease-out;
|
animation: fade-in 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-scale-in {
|
.animate-scale-in {
|
||||||
animation: scale-in 200ms ease-out;
|
animation: scale-in 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-in-right {
|
.animate-slide-in-right {
|
||||||
animation: slide-in-right 300ms ease-out;
|
animation: slide-in-right 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-up {
|
.animate-slide-up {
|
||||||
animation: slide-up 300ms ease-out;
|
animation: slide-up 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Scrollbar (Dark Theme) ──────────────────────────────────── */
|
.animate-float {
|
||||||
|
animation: float 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float-delayed {
|
||||||
|
animation: float 10s ease-in-out 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float-slow {
|
||||||
|
animation: float 12s ease-in-out 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Scrollbar (Glassmorphism) ───────────────────────────────── */
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #111827; /* gray-900 */
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #374151; /* gray-700 */
|
background: rgba(255, 255, 255, 0.14);
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4b5563; /* gray-600 */
|
background: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Console Font ────────────────────────────────────────────── */
|
/* ─── Console Font ────────────────────────────────────────────── */
|
||||||
@ -73,3 +176,10 @@ body {
|
|||||||
.font-mono {
|
.font-mono {
|
||||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Selection Color ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: rgba(6, 182, 212, 0.3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,7 +16,14 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className="min-h-screen bg-gray-950 text-gray-100 antialiased">
|
<body className="min-h-screen bg-[#050510] text-gray-100 antialiased">
|
||||||
|
{/* Gradient mesh background — smaller/lighter on mobile for GPU perf */}
|
||||||
|
<div className="fixed inset-0 -z-10 overflow-hidden" aria-hidden="true">
|
||||||
|
<div className="absolute top-[-20%] left-[-10%] w-[80%] h-[50%] sm:w-[60%] sm:h-[60%] rounded-full bg-cyan-500/15 sm:bg-cyan-500/20 blur-[80px] sm:blur-[150px] animate-float" />
|
||||||
|
<div className="absolute top-[10%] right-[-15%] w-[60%] h-[40%] sm:w-[50%] sm:h-[50%] rounded-full bg-violet-500/10 sm:bg-violet-500/15 blur-[80px] sm:blur-[150px] animate-float-delayed" />
|
||||||
|
<div className="absolute bottom-[-10%] left-[20%] w-[60%] h-[30%] sm:w-[50%] sm:h-[40%] rounded-full bg-emerald-500/[0.08] sm:bg-emerald-500/[0.12] blur-[80px] sm:blur-[150px] animate-float-slow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<ConfirmationProvider>
|
<ConfirmationProvider>
|
||||||
|
|||||||
@ -55,22 +55,23 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
<div className="min-h-screen flex items-center justify-center p-4 relative">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md relative z-10">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-cyan-500/20 rounded-2xl mb-4">
|
<div className="inline-flex items-center justify-center w-16 h-16 bg-cyan-400/15 rounded-2xl mb-4 relative">
|
||||||
<Gamepad2 size={32} className="text-cyan-400" />
|
<Gamepad2 size={32} className="text-cyan-400 relative z-10" />
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-cyan-400/20 blur-xl" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-gray-100">MC-Manager</h1>
|
<h1 className="text-2xl font-bold text-white">MC-Manager</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">Minecraft Server Manager</p>
|
<p className="text-sm text-gray-500 mt-1">Minecraft Server Manager</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 shadow-xl p-8">
|
<div className="glass-lg rounded-xl p-8 glass-shine">
|
||||||
{step === 'credentials' ? (
|
{step === 'credentials' ? (
|
||||||
<form onSubmit={handleLogin} className="space-y-5">
|
<form onSubmit={handleLogin} className="space-y-5">
|
||||||
<h2 className="text-lg font-semibold text-gray-100 text-center">Sign In</h2>
|
<h2 className="text-lg font-semibold text-white text-center">Sign In</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
|
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
|
||||||
@ -103,13 +104,13 @@ export default function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleVerify} className="space-y-5">
|
<form onSubmit={handleVerify} className="space-y-5">
|
||||||
<h2 className="text-lg font-semibold text-gray-100 text-center">Verification Code</h2>
|
<h2 className="text-lg font-semibold text-white text-center">Verification Code</h2>
|
||||||
<p className="text-sm text-gray-400 text-center">
|
<p className="text-sm text-gray-400 text-center">
|
||||||
A 6-digit code has been sent to your email.
|
A 6-digit code has been sent to your email.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm rounded-lg p-3">
|
<div className="bg-red-500/10 border border-red-500/20 text-red-400 text-sm rounded-lg p-3">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export default function HomePage() {
|
|||||||
}, [user, loading, router])
|
}, [user, loading, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-950">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-gray-950 rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="flex flex-col h-full bg-[#050510]/80 backdrop-blur-xl rounded-xl border border-white/[0.08] overflow-hidden">
|
||||||
{/* Console Output */}
|
{/* Console Output */}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@ -94,7 +94,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
|||||||
<p className="text-gray-600 italic">Waiting for server output...</p>
|
<p className="text-gray-600 italic">Waiting for server output...</p>
|
||||||
) : (
|
) : (
|
||||||
lines.map((line, i) => (
|
lines.map((line, i) => (
|
||||||
<div key={i} className="whitespace-pre-wrap break-all hover:bg-gray-900/50">
|
<div key={i} className="whitespace-pre-wrap break-all hover:bg-white/[0.04]">
|
||||||
{line}
|
{line}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@ -103,8 +103,8 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
|||||||
|
|
||||||
{/* Command Input */}
|
{/* Command Input */}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="flex items-center gap-2 p-3 border-t border-gray-700/50 bg-gray-900/50">
|
<div className="flex items-center gap-2 p-3 border-t border-white/[0.1] bg-white/[0.04]">
|
||||||
<span className="text-cyan-500 font-mono text-sm">></span>
|
<span className="text-cyan-400 font-mono text-sm">></span>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@ -112,11 +112,11 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
|||||||
onChange={e => setCommand(e.target.value)}
|
onChange={e => setCommand(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Type a command..."
|
placeholder="Type a command..."
|
||||||
className="flex-1 bg-transparent text-gray-100 font-mono text-sm placeholder-gray-600 focus:outline-none"
|
className="flex-1 bg-transparent text-gray-100 font-mono text-sm placeholder:text-white/25 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => setLines([])}
|
onClick={() => setLines([])}
|
||||||
className="p-1.5 rounded text-gray-500 hover:text-gray-300 hover:bg-gray-800 transition-colors"
|
className="p-1.5 rounded text-gray-500 hover:text-gray-300 hover:bg-white/[0.08] transition-colors"
|
||||||
aria-label="Clear console"
|
aria-label="Clear console"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
@ -124,7 +124,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
|||||||
<button
|
<button
|
||||||
onClick={handleSendCommand}
|
onClick={handleSendCommand}
|
||||||
disabled={!command.trim() || sending}
|
disabled={!command.trim() || sending}
|
||||||
className="p-1.5 rounded text-cyan-400 hover:text-cyan-300 hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="p-1.5 rounded text-cyan-400 hover:text-cyan-300 hover:bg-white/[0.08] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
aria-label="Send command"
|
aria-label="Send command"
|
||||||
>
|
>
|
||||||
<Send size={16} />
|
<Send size={16} />
|
||||||
|
|||||||
@ -33,17 +33,17 @@ export default function Drawer({ isOpen, onClose, title, children }: DrawerProps
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-40">
|
<div className="fixed inset-0 z-40">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-md" onClick={onClose} />
|
||||||
|
|
||||||
{/* Drawer Panel */}
|
{/* Drawer Panel */}
|
||||||
<div className="absolute inset-y-0 right-0 w-full max-w-3xl animate-slide-in-right">
|
<div className="absolute inset-y-0 right-0 w-full max-w-3xl animate-slide-in-right">
|
||||||
<div className="h-full flex flex-col bg-gray-900/95 backdrop-blur-md border-l border-gray-700/50 shadow-2xl">
|
<div className="h-full flex flex-col bg-[#0a0a1a]/90 backdrop-blur-2xl border-l border-white/[0.1] shadow-2xl shadow-black/40">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50 bg-gray-900/60 backdrop-blur-sm">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
|
||||||
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
|
className="p-2 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors"
|
||||||
aria-label="Close drawer"
|
aria-label="Close drawer"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
|
|||||||
@ -33,16 +33,16 @@ export default function Modal({ isOpen, onClose, title, children, maxWidth = 'ma
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-md" onClick={onClose} />
|
||||||
|
|
||||||
{/* Modal Panel */}
|
{/* Modal Panel */}
|
||||||
<div className={`relative ${maxWidth} w-full mx-4 bg-gray-900 border border-gray-700 rounded-lg shadow-2xl animate-scale-in`}>
|
<div className={`relative ${maxWidth} w-full mx-4 glass-lg rounded-xl shadow-2xl shadow-black/40 animate-scale-in glass-shine`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
|
||||||
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
|
className="p-2 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors"
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
|
|||||||
@ -9,12 +9,12 @@ interface PageHeaderProps {
|
|||||||
|
|
||||||
export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) {
|
export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg shadow-lg border border-gray-700/50 p-4 sm:p-6">
|
<div className="glass rounded-xl glass-shine p-4 sm:p-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{Icon && <Icon size={28} className="text-gray-400 flex-shrink-0" />}
|
{Icon && <Icon size={28} className="text-cyan-400/80 flex-shrink-0" />}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-100 truncate">{title}</h1>
|
<h1 className="text-xl sm:text-2xl font-bold text-white truncate">{title}</h1>
|
||||||
{description && <p className="text-sm text-gray-400 mt-1 truncate">{description}</p>}
|
{description && <p className="text-sm text-gray-400 mt-1 truncate">{description}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,28 +8,28 @@ interface ServerStatusBadgeProps {
|
|||||||
const statusConfig: Record<ServerStatus, { label: string; className: string; dot: string }> = {
|
const statusConfig: Record<ServerStatus, { label: string; className: string; dot: string }> = {
|
||||||
online: {
|
online: {
|
||||||
label: 'Online',
|
label: 'Online',
|
||||||
className: 'bg-emerald-500/20 text-emerald-400',
|
className: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
|
||||||
dot: 'bg-emerald-400',
|
dot: 'bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.6)]',
|
||||||
},
|
},
|
||||||
offline: {
|
offline: {
|
||||||
label: 'Offline',
|
label: 'Offline',
|
||||||
className: 'bg-gray-500/20 text-gray-400',
|
className: 'bg-white/[0.1] text-gray-400 border border-white/[0.15]',
|
||||||
dot: 'bg-gray-400',
|
dot: 'bg-gray-400',
|
||||||
},
|
},
|
||||||
starting: {
|
starting: {
|
||||||
label: 'Starting',
|
label: 'Starting',
|
||||||
className: 'bg-amber-500/20 text-amber-400',
|
className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
|
||||||
dot: 'bg-amber-400 animate-pulse',
|
dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
|
||||||
},
|
},
|
||||||
stopping: {
|
stopping: {
|
||||||
label: 'Stopping',
|
label: 'Stopping',
|
||||||
className: 'bg-amber-500/20 text-amber-400',
|
className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
|
||||||
dot: 'bg-amber-400 animate-pulse',
|
dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
|
||||||
},
|
},
|
||||||
crashed: {
|
crashed: {
|
||||||
label: 'Crashed',
|
label: 'Crashed',
|
||||||
className: 'bg-red-500/20 text-red-400',
|
className: 'bg-red-400/20 text-red-400 border border-red-400/30',
|
||||||
dot: 'bg-red-400',
|
dot: 'bg-red-400 shadow-[0_0_8px_rgba(248,113,113,0.6)]',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,16 +63,19 @@ export default function Sidebar() {
|
|||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<>
|
<>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-700/50">
|
<div className="flex items-center gap-3 px-4 h-16 border-b border-white/[0.1]">
|
||||||
<Gamepad2 size={28} className="text-cyan-500 flex-shrink-0" />
|
<div className="relative">
|
||||||
|
<Gamepad2 size={28} className="text-cyan-400 flex-shrink-0" />
|
||||||
|
<div className="absolute inset-0 blur-lg bg-cyan-400/40 rounded-full" />
|
||||||
|
</div>
|
||||||
{(!collapsed || mobileOpen) && (
|
{(!collapsed || mobileOpen) && (
|
||||||
<span className="text-lg font-bold text-gray-100 truncate">MC-Manager</span>
|
<span className="text-lg font-bold text-white truncate">MC-Manager</span>
|
||||||
)}
|
)}
|
||||||
{/* Mobile close button */}
|
{/* Mobile close button */}
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
className="ml-auto p-1.5 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors lg:hidden"
|
className="ml-auto p-1.5 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors lg:hidden"
|
||||||
aria-label="Close menu"
|
aria-label="Close menu"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
@ -90,10 +93,10 @@ export default function Sidebar() {
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-sm font-medium ${
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm font-medium ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-cyan-500/20 text-cyan-400'
|
? 'bg-cyan-500/20 text-cyan-400 shadow-[0_0_24px_rgba(6,182,212,0.15)] border border-cyan-500/25'
|
||||||
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'
|
: 'text-gray-400 hover:bg-white/[0.08] hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon size={20} className="flex-shrink-0" />
|
<Icon size={20} className="flex-shrink-0" />
|
||||||
@ -104,7 +107,7 @@ export default function Sidebar() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="border-t border-gray-700/50 p-2 space-y-1">
|
<div className="border-t border-white/[0.1] p-2 space-y-1">
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
{user && (!collapsed || mobileOpen) && (
|
{user && (!collapsed || mobileOpen) && (
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
@ -116,7 +119,7 @@ export default function Sidebar() {
|
|||||||
{/* Logout */}
|
{/* Logout */}
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-400 hover:bg-red-500/10 hover:text-red-400 transition-colors text-sm font-medium w-full"
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-400 hover:bg-red-500/10 hover:text-red-400 transition-all duration-200 text-sm font-medium w-full"
|
||||||
>
|
>
|
||||||
<LogOut size={20} className="flex-shrink-0" />
|
<LogOut size={20} className="flex-shrink-0" />
|
||||||
{(!collapsed || mobileOpen) && <span>Logout</span>}
|
{(!collapsed || mobileOpen) && <span>Logout</span>}
|
||||||
@ -125,7 +128,7 @@ export default function Sidebar() {
|
|||||||
{/* Collapse Toggle — desktop only */}
|
{/* Collapse Toggle — desktop only */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setCollapsed(prev => !prev)}
|
onClick={() => setCollapsed(prev => !prev)}
|
||||||
className="hidden lg:flex items-center justify-center gap-3 px-3 py-2.5 rounded-lg text-gray-500 hover:bg-gray-800 hover:text-gray-300 transition-colors w-full"
|
className="hidden lg:flex items-center justify-center gap-3 px-3 py-2.5 rounded-lg text-gray-500 hover:bg-white/[0.06] hover:text-gray-300 transition-all duration-200 w-full"
|
||||||
>
|
>
|
||||||
{collapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
|
{collapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
|
||||||
{!collapsed && <span className="text-sm">Collapse</span>}
|
{!collapsed && <span className="text-sm">Collapse</span>}
|
||||||
@ -137,10 +140,10 @@ export default function Sidebar() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile top bar */}
|
{/* Mobile top bar */}
|
||||||
<div className="lg:hidden fixed top-0 left-0 right-0 z-30 flex items-center gap-3 px-4 h-14 bg-gray-900 border-b border-gray-700/50">
|
<div className="lg:hidden fixed top-0 left-0 right-0 z-30 flex items-center gap-3 px-4 h-14 glass border-b border-white/[0.1]">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileOpen(true)}
|
onClick={() => setMobileOpen(true)}
|
||||||
className="p-2 -ml-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
|
className="p-2 -ml-2 rounded-lg text-gray-400 hover:bg-white/[0.08] hover:text-gray-200 transition-colors"
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<Menu size={22} />
|
<Menu size={22} />
|
||||||
@ -159,7 +162,7 @@ export default function Sidebar() {
|
|||||||
|
|
||||||
{/* Mobile sidebar drawer */}
|
{/* Mobile sidebar drawer */}
|
||||||
<aside
|
<aside
|
||||||
className={`fixed inset-y-0 left-0 z-50 flex flex-col bg-gray-900 border-r border-gray-700/50 w-72
|
className={`fixed inset-y-0 left-0 z-50 flex flex-col bg-[#0a0a1a]/90 backdrop-blur-xl border-r border-white/[0.1] w-72
|
||||||
transform transition-transform duration-300 ease-in-out lg:hidden ${
|
transform transition-transform duration-300 ease-in-out lg:hidden ${
|
||||||
mobileOpen ? 'translate-x-0' : '-translate-x-full'
|
mobileOpen ? 'translate-x-0' : '-translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
@ -169,7 +172,7 @@ export default function Sidebar() {
|
|||||||
|
|
||||||
{/* Desktop sidebar */}
|
{/* Desktop sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`hidden lg:flex flex-col bg-gray-900 border-r border-gray-700/50 transition-all duration-300 ${
|
className={`hidden lg:flex flex-col bg-[#0a0a1a]/70 backdrop-blur-2xl border-r border-white/[0.1] transition-all duration-300 ${
|
||||||
collapsed ? 'w-16' : 'w-64'
|
collapsed ? 'w-16' : 'w-64'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export default function StatsChart({
|
|||||||
}, [data, maxDataPoints, height, maxY])
|
}, [data, maxDataPoints, height, maxY])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4 overflow-hidden">
|
<div className="glass rounded-xl p-4 overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search & Filters Bar */}
|
{/* Search & Filters Bar */}
|
||||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4">
|
<div className="glass rounded-xl p-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 sm:gap-4">
|
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 sm:gap-4">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative flex-1 min-w-0 sm:min-w-[200px]">
|
<div className="relative flex-1 min-w-0 sm:min-w-[200px]">
|
||||||
@ -142,8 +142,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
|
className="w-full pl-10 pr-4 py-2 glass-input text-gray-100 rounded-lg
|
||||||
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent text-sm"
|
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -153,8 +153,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
key={filter.filterKey}
|
key={filter.filterKey}
|
||||||
value={activeFilters[filter.filterKey] || ''}
|
value={activeFilters[filter.filterKey] || ''}
|
||||||
onChange={e => setActiveFilters(prev => ({ ...prev, [filter.filterKey]: e.target.value }))}
|
onChange={e => setActiveFilters(prev => ({ ...prev, [filter.filterKey]: e.target.value }))}
|
||||||
className="px-3 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg text-sm
|
className="px-3 py-2 glass-input text-gray-100 rounded-lg text-sm
|
||||||
focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
|
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="">{filter.label}</option>
|
<option value="">{filter.label}</option>
|
||||||
{filter.options.map(opt => (
|
{filter.options.map(opt => (
|
||||||
@ -168,7 +168,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="bg-gray-900 rounded-lg border border-gray-700/50 overflow-hidden">
|
<div className="glass rounded-xl overflow-hidden">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-16">
|
<div className="flex items-center justify-center py-16">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
@ -176,7 +176,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
) : filteredItems.length === 0 ? (
|
) : filteredItems.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||||
{EmptyIcon && (
|
{EmptyIcon && (
|
||||||
<div className="bg-gray-800 rounded-full p-4 mb-4">
|
<div className="bg-white/[0.08] rounded-full p-4 mb-4">
|
||||||
<EmptyIcon size={32} className="text-gray-500" />
|
<EmptyIcon size={32} className="text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -190,8 +190,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-700">
|
<table className="min-w-full divide-y divide-white/[0.1]">
|
||||||
<thead className="bg-gray-800/50">
|
<thead className="bg-white/[0.05]">
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map(col => (
|
{columns.map(col => (
|
||||||
<th
|
<th
|
||||||
@ -211,12 +211,12 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
|||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody className="divide-y divide-white/[0.08]">
|
||||||
{filteredItems.map(item => (
|
{filteredItems.map(item => (
|
||||||
<tr
|
<tr
|
||||||
key={getRowKey(item)}
|
key={getRowKey(item)}
|
||||||
onClick={() => onRowClick?.(item)}
|
onClick={() => onRowClick?.(item)}
|
||||||
className={`hover:bg-gray-800 transition-colors ${
|
className={`hover:bg-white/[0.06] transition-colors ${
|
||||||
onRowClick ? 'cursor-pointer' : ''
|
onRowClick ? 'cursor-pointer' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,11 +5,11 @@ interface BadgeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const badgeVariants = {
|
const badgeVariants = {
|
||||||
info: 'bg-cyan-500/20 text-cyan-400',
|
info: 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/30',
|
||||||
success: 'bg-emerald-500/20 text-emerald-400',
|
success: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
|
||||||
warning: 'bg-amber-500/20 text-amber-400',
|
warning: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
|
||||||
error: 'bg-red-500/20 text-red-400',
|
error: 'bg-red-400/20 text-red-400 border border-red-400/30',
|
||||||
neutral: 'bg-gray-500/20 text-gray-400',
|
neutral: 'bg-white/[0.1] text-gray-400 border border-white/[0.12]',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Badge({ variant = 'neutral', children, className = '' }: BadgeProps) {
|
export default function Badge({ variant = 'neutral', children, className = '' }: BadgeProps) {
|
||||||
|
|||||||
@ -10,10 +10,10 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: 'bg-cyan-500 hover:bg-cyan-600 text-white shadow-sm hover:shadow-md',
|
primary: 'bg-cyan-500 hover:bg-cyan-400 text-white shadow-lg shadow-cyan-500/25 hover:shadow-cyan-400/35',
|
||||||
secondary: 'bg-gray-700 hover:bg-gray-600 text-gray-200 border border-gray-600',
|
secondary: 'bg-white/[0.08] hover:bg-white/[0.14] text-gray-200 border border-white/[0.12] hover:border-white/[0.2]',
|
||||||
danger: 'bg-red-500 hover:bg-red-600 text-white',
|
danger: 'bg-red-500 hover:bg-red-400 text-white shadow-lg shadow-red-500/25',
|
||||||
ghost: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200',
|
ghost: 'text-gray-400 hover:bg-white/[0.08] hover:text-gray-200',
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
@ -36,7 +36,7 @@ export default function Button({
|
|||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={`inline-flex items-center justify-center gap-2 font-medium rounded-lg
|
className={`inline-flex items-center justify-center gap-2 font-medium rounded-lg
|
||||||
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 focus:ring-offset-gray-900
|
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-cyan-400/40 focus:ring-offset-2 focus:ring-offset-[#050510]
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
${variants[variant]} ${sizes[size]} ${className}`}
|
${variants[variant]} ${sizes[size]} ${className}`}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
|
|||||||
@ -18,9 +18,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
|
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
|
||||||
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent
|
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
|
focus:bg-white/[0.06] focus:shadow-[0_0_20px_rgba(6,182,212,0.08)]
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${
|
||||||
error ? 'border-red-500 focus:ring-red-500' : ''
|
error ? 'border-red-500 focus:ring-red-500' : ''
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -20,9 +20,10 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
)}
|
)}
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
|
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
|
||||||
focus:ring-2 focus:ring-cyan-500 focus:border-transparent
|
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
|
focus:bg-white/[0.06] focus:shadow-[0_0_20px_rgba(6,182,212,0.08)]
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 ${
|
||||||
error ? 'border-red-500 focus:ring-red-500' : ''
|
error ? 'border-red-500 focus:ring-red-500' : ''
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -12,7 +12,9 @@ const sizes = {
|
|||||||
export default function Spinner({ size = 'md', className = '' }: SpinnerProps) {
|
export default function Spinner({ size = 'md', className = '' }: SpinnerProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center justify-center ${className}`}>
|
<div className={`flex items-center justify-center ${className}`}>
|
||||||
<div className={`animate-spin rounded-full border-b-2 border-cyan-500 ${sizes[size]}`} />
|
<div className={`animate-spin rounded-full border-2 border-white/[0.08] border-t-cyan-400 ${sizes[size]}`}
|
||||||
|
style={{ filter: 'drop-shadow(0 0 8px rgba(6, 182, 212, 0.3))' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,16 +20,16 @@ const ConfirmationContext = createContext<ConfirmationContextType | undefined>(u
|
|||||||
|
|
||||||
const typeStyles = {
|
const typeStyles = {
|
||||||
danger: {
|
danger: {
|
||||||
icon: 'bg-red-500/20 text-red-400',
|
icon: 'bg-red-400/15 text-red-400 border border-red-400/20',
|
||||||
button: 'bg-red-500 hover:bg-red-600 text-white',
|
button: 'bg-red-500/90 hover:bg-red-400/90 text-white shadow-lg shadow-red-500/20 hover:shadow-red-400/30',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
icon: 'bg-amber-500/20 text-amber-400',
|
icon: 'bg-amber-400/15 text-amber-400 border border-amber-400/20',
|
||||||
button: 'bg-amber-500 hover:bg-amber-600 text-white',
|
button: 'bg-amber-500/90 hover:bg-amber-400/90 text-white shadow-lg shadow-amber-500/20 hover:shadow-amber-400/30',
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
icon: 'bg-cyan-500/20 text-cyan-400',
|
icon: 'bg-cyan-400/15 text-cyan-400 border border-cyan-400/20',
|
||||||
button: 'bg-cyan-500 hover:bg-cyan-600 text-white',
|
button: 'bg-cyan-500/90 hover:bg-cyan-400/90 text-white shadow-lg shadow-cyan-500/20 hover:shadow-cyan-400/30',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,17 +70,17 @@ export function ConfirmationProvider({ children }: { children: ReactNode }) {
|
|||||||
{isOpen && options && (
|
{isOpen && options && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleCancel} />
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-md" onClick={handleCancel} />
|
||||||
|
|
||||||
{/* Dialog */}
|
{/* Dialog */}
|
||||||
<div className="relative bg-gray-900 border border-gray-700 rounded-lg shadow-2xl p-6 max-w-md w-full mx-4 animate-scale-in">
|
<div className="relative glass-lg rounded-xl shadow-2xl shadow-black/40 p-6 max-w-md w-full mx-4 animate-scale-in glass-shine">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className={`p-3 rounded-full ${styles.icon}`}>
|
<div className={`p-3 rounded-full ${styles.icon}`}>
|
||||||
<AlertTriangle size={24} />
|
<AlertTriangle size={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-gray-100">{options.title}</h3>
|
<h3 className="text-lg font-semibold text-white">{options.title}</h3>
|
||||||
<p className="mt-2 text-sm text-gray-400">
|
<p className="mt-2 text-sm text-gray-400">
|
||||||
{options.message}
|
{options.message}
|
||||||
{options.itemName && (
|
{options.itemName && (
|
||||||
@ -94,7 +94,7 @@ export function ConfirmationProvider({ children }: { children: ReactNode }) {
|
|||||||
<div className="flex justify-end gap-3 mt-6">
|
<div className="flex justify-end gap-3 mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 rounded-lg transition-colors text-sm font-medium"
|
className="px-4 py-2 bg-white/[0.08] hover:bg-white/[0.14] border border-white/[0.12] hover:border-white/[0.2] text-gray-200 rounded-lg transition-all duration-200 text-sm font-medium"
|
||||||
>
|
>
|
||||||
{options.cancelText || 'Cancel'}
|
{options.cancelText || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -20,10 +20,10 @@ const ToastContext = createContext<ToastContextType | undefined>(undefined)
|
|||||||
const TOAST_DURATION = 5000
|
const TOAST_DURATION = 5000
|
||||||
|
|
||||||
const toastStyles: Record<ToastType, string> = {
|
const toastStyles: Record<ToastType, string> = {
|
||||||
success: 'bg-emerald-500/20 border-emerald-500/50 text-emerald-400',
|
success: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
|
||||||
error: 'bg-red-500/20 border-red-500/50 text-red-400',
|
error: 'bg-red-500/10 border-red-500/30 text-red-400',
|
||||||
info: 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400',
|
info: 'bg-cyan-500/10 border-cyan-500/30 text-cyan-400',
|
||||||
warning: 'bg-amber-500/20 border-amber-500/50 text-amber-400',
|
warning: 'bg-amber-500/10 border-amber-500/30 text-amber-400',
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastIcons: Record<ToastType, typeof CheckCircle> = {
|
const toastIcons: Record<ToastType, typeof CheckCircle> = {
|
||||||
@ -58,7 +58,8 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={toast.id}
|
key={toast.id}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg border backdrop-blur-lg shadow-xl animate-slide-in-right ${toastStyles[toast.type]}`}
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl border shadow-2xl shadow-black/30 animate-slide-in-right ${toastStyles[toast.type]}`}
|
||||||
|
style={{ backdropFilter: 'blur(24px) saturate(1.3)', WebkitBackdropFilter: 'blur(24px) saturate(1.3)' }}
|
||||||
>
|
>
|
||||||
<Icon size={18} className="flex-shrink-0" />
|
<Icon size={18} className="flex-shrink-0" />
|
||||||
<span className="text-sm font-medium">{toast.message}</span>
|
<span className="text-sm font-medium">{toast.message}</span>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user