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}
|
||||
onChange={e => { setSearch(e.target.value); setPage(1) }}
|
||||
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>
|
||||
@ -81,7 +81,7 @@ export default function AuditPage() {
|
||||
</div>
|
||||
|
||||
{/* 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 ? (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<Spinner size="lg" />
|
||||
@ -95,7 +95,7 @@ export default function AuditPage() {
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/50">
|
||||
<thead className="bg-white/[0.05]">
|
||||
<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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-white/[0.08]">
|
||||
{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">
|
||||
{formatDateTime(log.createdAt)}
|
||||
</td>
|
||||
@ -139,7 +139,7 @@ export default function AuditPage() {
|
||||
|
||||
{/* Pagination */}
|
||||
{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">
|
||||
Page {page} of {totalPages}
|
||||
</p>
|
||||
@ -147,14 +147,14 @@ export default function AuditPage() {
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 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
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
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
|
||||
</button>
|
||||
|
||||
@ -93,9 +93,9 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Server List */}
|
||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
||||
<h2 className="text-lg font-semibold text-gray-100">Servers</h2>
|
||||
<div className="glass rounded-xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||
<h2 className="text-lg font-semibold text-white">Servers</h2>
|
||||
</div>
|
||||
|
||||
{servers.length === 0 ? (
|
||||
@ -103,12 +103,12 @@ export default function DashboardPage() {
|
||||
No servers yet. Create your first Minecraft server!
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-700">
|
||||
<div className="divide-y divide-white/[0.08]">
|
||||
{servers.map(server => (
|
||||
<a
|
||||
key={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">
|
||||
<Server size={20} className="text-gray-500" />
|
||||
@ -140,21 +140,21 @@ function StatCard({
|
||||
value: number
|
||||
color: string
|
||||
}) {
|
||||
const colorMap: Record<string, string> = {
|
||||
cyan: 'bg-cyan-500/20 text-cyan-400',
|
||||
emerald: 'bg-emerald-500/20 text-emerald-400',
|
||||
amber: 'bg-amber-500/20 text-amber-400',
|
||||
gray: 'bg-gray-500/20 text-gray-400',
|
||||
const colorMap: Record<string, { badge: string; glow: string }> = {
|
||||
cyan: { badge: 'bg-cyan-400/20 text-cyan-400', glow: 'shadow-cyan-500/15' },
|
||||
emerald: { badge: 'bg-emerald-400/20 text-emerald-400', glow: 'shadow-emerald-500/15' },
|
||||
amber: { badge: 'bg-amber-400/20 text-amber-400', glow: 'shadow-amber-500/15' },
|
||||
gray: { badge: 'bg-white/[0.1] text-gray-400', glow: '' },
|
||||
}
|
||||
|
||||
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={`p-2.5 rounded-lg ${colorMap[color]}`}>
|
||||
<div className={`p-2.5 rounded-lg ${colorMap[color].badge}`}>
|
||||
<Icon size={20} />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (loading) {
|
||||
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" />
|
||||
</div>
|
||||
)
|
||||
@ -18,9 +18,9 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
if (!user) return null // Middleware will redirect
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-950">
|
||||
<div className="flex h-screen">
|
||||
<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}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -233,7 +233,7 @@ export default function RolesPage() {
|
||||
const PermissionGrid = () => (
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
||||
{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>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allActions[resource].map(action => (
|
||||
@ -241,10 +241,10 @@ export default function RolesPage() {
|
||||
key={`${resource}:${action}`}
|
||||
type="button"
|
||||
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)
|
||||
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50'
|
||||
: 'bg-gray-700 text-gray-400 border border-gray-600 hover:bg-gray-600'
|
||||
? 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/35 shadow-[0_0_10px_rgba(6,182,212,0.2)]'
|
||||
: 'bg-white/[0.08] text-gray-400 border border-white/[0.12] hover:bg-white/[0.14] hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{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 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<HardDrive size={40} className="mx-auto mb-3 text-gray-600" />
|
||||
@ -161,7 +161,7 @@ export default function BackupsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/50">
|
||||
<thead className="bg-white/[0.05]">
|
||||
<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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-white/[0.08]">
|
||||
{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">
|
||||
<Badge variant={backup.type === 'manual' ? 'info' : 'neutral'}>
|
||||
|
||||
@ -119,7 +119,7 @@ export default function ConfigurationPage() {
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<Input
|
||||
value={jvmArgs}
|
||||
@ -132,11 +132,11 @@ export default function ConfigurationPage() {
|
||||
</div>
|
||||
|
||||
{/* Server Properties */}
|
||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
||||
<div className="glass rounded-xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||
<h3 className="text-sm font-semibold text-gray-200">server.properties</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-700">
|
||||
<div className="divide-y divide-white/[0.08]">
|
||||
{sortedKeys.map(key => (
|
||||
<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}>
|
||||
@ -145,7 +145,7 @@ export default function ConfigurationPage() {
|
||||
<input
|
||||
value={properties[key]}
|
||||
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>
|
||||
))}
|
||||
|
||||
@ -640,7 +640,7 @@ export default function FilesPage() {
|
||||
{i > 0 && <ChevronRight size={14} className="text-gray-600" />}
|
||||
<button
|
||||
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
|
||||
? 'text-cyan-400 font-medium'
|
||||
: 'text-gray-400 hover:text-gray-200'
|
||||
@ -655,7 +655,7 @@ export default function FilesPage() {
|
||||
|
||||
{/* New Folder Inline Form */}
|
||||
{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" />
|
||||
<input
|
||||
type="text"
|
||||
@ -664,7 +664,7 @@ export default function FilesPage() {
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
|
||||
placeholder="Folder name…"
|
||||
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()}>
|
||||
Create
|
||||
@ -677,8 +677,8 @@ export default function FilesPage() {
|
||||
|
||||
{viewingFile ? (
|
||||
/* ─── File Viewer / Editor ─────────────────────────────── */
|
||||
<div className="bg-gray-950 rounded-lg border border-gray-700/50 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="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-white/[0.1] bg-white/[0.04]">
|
||||
<div className="flex items-center gap-2">
|
||||
<File size={16} className="text-cyan-400" />
|
||||
<span className="text-sm font-medium text-gray-200">{fileMeta?.name || viewingFile}</span>
|
||||
@ -705,7 +705,7 @@ export default function FilesPage() {
|
||||
<textarea
|
||||
value={editContent || ''}
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
@ -719,7 +719,7 @@ export default function FilesPage() {
|
||||
<div>
|
||||
{/* ─── Bulk Action Bar ───────────────────────────────────── */}
|
||||
{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">
|
||||
{selected.size} selected
|
||||
</span>
|
||||
@ -762,7 +762,7 @@ export default function FilesPage() {
|
||||
</Button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<X size={16} />
|
||||
@ -772,7 +772,7 @@ export default function FilesPage() {
|
||||
)}
|
||||
|
||||
{/* ─── 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 ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" />
|
||||
@ -783,10 +783,10 @@ export default function FilesPage() {
|
||||
<p className="text-gray-400">Empty directory</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-700/50">
|
||||
<div className="divide-y divide-white/[0.08]">
|
||||
{/* Select all header */}
|
||||
{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
|
||||
onClick={toggleSelectAll}
|
||||
className="flex-shrink-0 text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
@ -811,7 +811,7 @@ export default function FilesPage() {
|
||||
parts.pop()
|
||||
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" />}
|
||||
<Folder size={18} className="text-amber-400/70 flex-shrink-0" />
|
||||
@ -822,8 +822,8 @@ export default function FilesPage() {
|
||||
{entries.map((entry) => (
|
||||
<div
|
||||
key={entry.path}
|
||||
className={`flex items-center gap-3 px-5 py-3 hover:bg-gray-800/60 transition-colors group relative ${
|
||||
selected.has(entry.path) ? 'bg-cyan-500/5' : ''
|
||||
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-400/[0.03]' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Checkbox */}
|
||||
@ -869,7 +869,7 @@ export default function FilesPage() {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleUnzip(entry) }}
|
||||
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"
|
||||
>
|
||||
<ArchiveRestore size={15} />
|
||||
@ -879,7 +879,7 @@ export default function FilesPage() {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleZip(entry) }}
|
||||
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"
|
||||
>
|
||||
<Archive size={15} />
|
||||
@ -891,7 +891,7 @@ export default function FilesPage() {
|
||||
setMoveDest(entry.path)
|
||||
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"
|
||||
>
|
||||
<FolderInput size={15} />
|
||||
@ -904,7 +904,7 @@ export default function FilesPage() {
|
||||
setCopyDest(`${dir}${entry.name}-copy${entry.isDirectory ? '' : ''}`)
|
||||
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"
|
||||
>
|
||||
<Copy size={15} />
|
||||
@ -913,7 +913,7 @@ export default function FilesPage() {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(entry) }}
|
||||
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"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
@ -942,9 +942,9 @@ export default function FilesPage() {
|
||||
|
||||
{/* ─── Copy Modal ────────────────────────────────────────── */}
|
||||
{copyModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Copy “{copyModal.name}”</h3>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||
<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-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>
|
||||
<input
|
||||
type="text"
|
||||
@ -952,7 +952,7 @@ export default function FilesPage() {
|
||||
onChange={(e) => setCopyDest(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCopy()}
|
||||
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"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
@ -975,9 +975,9 @@ export default function FilesPage() {
|
||||
|
||||
{/* ─── Bulk Copy Modal ───────────────────────────────────── */}
|
||||
{bulkCopyModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Copy {selected.size} item(s)</h3>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||
<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-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>
|
||||
<input
|
||||
type="text"
|
||||
@ -985,7 +985,7 @@ export default function FilesPage() {
|
||||
onChange={(e) => setBulkCopyDest(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleBulkCopy()}
|
||||
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"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
@ -1008,9 +1008,9 @@ export default function FilesPage() {
|
||||
|
||||
{/* ─── Move Modal ────────────────────────────────────────── */}
|
||||
{moveModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Move “{moveModal.name}”</h3>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||
<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-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>
|
||||
<input
|
||||
type="text"
|
||||
@ -1018,7 +1018,7 @@ export default function FilesPage() {
|
||||
onChange={(e) => setMoveDest(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleMove()}
|
||||
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"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
@ -1041,9 +1041,9 @@ export default function FilesPage() {
|
||||
|
||||
{/* ─── Bulk Move Modal ───────────────────────────────────── */}
|
||||
{bulkMoveModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-md p-6 mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-100 mb-1">Move {selected.size} item(s)</h3>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md">
|
||||
<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-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>
|
||||
<input
|
||||
type="text"
|
||||
@ -1051,7 +1051,7 @@ export default function FilesPage() {
|
||||
onChange={(e) => setBulkMoveDest(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleBulkMove()}
|
||||
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"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
|
||||
@ -129,8 +129,8 @@ export default function LogsPage() {
|
||||
|
||||
{selectedFile ? (
|
||||
/* Log File Viewer */
|
||||
<div className="bg-gray-950 rounded-lg border border-gray-700/50 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="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-white/[0.1] bg-white/[0.04]">
|
||||
<FileText size={16} className="text-cyan-400" />
|
||||
<span className="text-sm font-medium text-gray-200">{selectedFile}</span>
|
||||
</div>
|
||||
@ -148,7 +148,7 @@ export default function LogsPage() {
|
||||
</div>
|
||||
) : (
|
||||
/* 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 ? (
|
||||
<div className="text-center py-12">
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-700/50">
|
||||
<div className="divide-y divide-white/[0.08]">
|
||||
{files.map((file) => (
|
||||
<button
|
||||
key={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" />
|
||||
<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 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<Package size={40} className="mx-auto mb-3 text-gray-600" />
|
||||
@ -170,7 +170,7 @@ export default function ModsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/50">
|
||||
<thead className="bg-white/[0.05]">
|
||||
<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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-white/[0.08]">
|
||||
{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">
|
||||
<p className="text-sm font-medium text-gray-200">{mod.name}</p>
|
||||
<p className="text-xs text-gray-500 font-mono">{mod.filename}</p>
|
||||
|
||||
@ -266,10 +266,10 @@ export default function ServerDetailPage() {
|
||||
<Link
|
||||
key={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="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" />
|
||||
</div>
|
||||
<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 }) {
|
||||
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-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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -137,14 +137,14 @@ export default function PlayersPage() {
|
||||
|
||||
{/* Online Players */}
|
||||
{onlinePlayers.length > 0 && (
|
||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
||||
<div className="glass rounded-xl overflow-hidden">
|
||||
<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">
|
||||
<div className="w-2 h-2 bg-emerald-400 rounded-full" />
|
||||
Online ({onlinePlayers.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-700">
|
||||
<div className="divide-y divide-white/[0.08]">
|
||||
{onlinePlayers.map(player => (
|
||||
<PlayerRow
|
||||
key={player.name}
|
||||
@ -159,8 +159,8 @@ export default function PlayersPage() {
|
||||
)}
|
||||
|
||||
{/* All Known Players */}
|
||||
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-700/50">
|
||||
<div className="glass rounded-xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/[0.1]">
|
||||
<h3 className="text-sm font-semibold text-gray-200">
|
||||
All Players ({players.length})
|
||||
</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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-700">
|
||||
<div className="divide-y divide-white/[0.08]">
|
||||
{[...onlinePlayers, ...offlinePlayers].map(player => (
|
||||
<PlayerRow
|
||||
key={player.name}
|
||||
@ -200,10 +200,10 @@ export default function PlayersPage() {
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400'
|
||||
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700'
|
||||
? 'bg-cyan-400/20 border-cyan-400/35 text-cyan-400 shadow-lg shadow-cyan-500/10'
|
||||
: 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
|
||||
}`}
|
||||
>
|
||||
Add to Whitelist
|
||||
@ -211,10 +211,10 @@ export default function PlayersPage() {
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-amber-500/20 border-amber-500/50 text-amber-400'
|
||||
: 'bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700'
|
||||
? 'bg-amber-400/20 border-amber-400/35 text-amber-400 shadow-lg shadow-amber-500/10'
|
||||
: 'bg-white/[0.06] border-white/[0.12] text-gray-400 hover:bg-white/[0.1]'
|
||||
}`}
|
||||
>
|
||||
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 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<Puzzle size={40} className="mx-auto mb-3 text-gray-600" />
|
||||
@ -170,7 +170,7 @@ export default function PluginsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/50">
|
||||
<thead className="bg-white/[0.05]">
|
||||
<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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-white/[0.08]">
|
||||
{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">
|
||||
<p className="text-sm font-medium text-gray-200">{plugin.name}</p>
|
||||
<p className="text-xs text-gray-500 font-mono">{plugin.filename}</p>
|
||||
|
||||
@ -6,8 +6,94 @@
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background-color: #030712; /* gray-950 */
|
||||
color: #f3f4f6; /* gray-100 */
|
||||
background-color: #050510;
|
||||
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 ──────────────────────────────────────────────── */
|
||||
@ -18,8 +104,8 @@ body {
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
from { opacity: 0; transform: scale(0.96) translateY(8px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
@ -32,40 +118,57 @@ body {
|
||||
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 {
|
||||
animation: fade-in 200ms ease-out;
|
||||
}
|
||||
|
||||
.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 {
|
||||
animation: slide-in-right 300ms ease-out;
|
||||
animation: slide-in-right 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #111827; /* gray-900 */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #374151; /* gray-700 */
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4b5563; /* gray-600 */
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* ─── Console Font ────────────────────────────────────────────── */
|
||||
@ -73,3 +176,10 @@ body {
|
||||
.font-mono {
|
||||
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 (
|
||||
<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>
|
||||
<ToastProvider>
|
||||
<ConfirmationProvider>
|
||||
|
||||
@ -55,22 +55,23 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="min-h-screen flex items-center justify-center p-4 relative">
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
{/* Logo */}
|
||||
<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">
|
||||
<Gamepad2 size={32} className="text-cyan-400" />
|
||||
<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 relative z-10" />
|
||||
<div className="absolute inset-0 rounded-2xl bg-cyan-400/20 blur-xl" />
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* 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' ? (
|
||||
<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 && (
|
||||
<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 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">
|
||||
A 6-digit code has been sent to your email.
|
||||
</p>
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -20,7 +20,7 @@ export default function HomePage() {
|
||||
}, [user, loading, router])
|
||||
|
||||
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" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -84,7 +84,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
||||
}
|
||||
|
||||
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 */}
|
||||
<div
|
||||
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>
|
||||
) : (
|
||||
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}
|
||||
</div>
|
||||
))
|
||||
@ -103,8 +103,8 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
||||
|
||||
{/* Command Input */}
|
||||
{!readOnly && (
|
||||
<div className="flex items-center gap-2 p-3 border-t border-gray-700/50 bg-gray-900/50">
|
||||
<span className="text-cyan-500 font-mono text-sm">></span>
|
||||
<div className="flex items-center gap-2 p-3 border-t border-white/[0.1] bg-white/[0.04]">
|
||||
<span className="text-cyan-400 font-mono text-sm">></span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
@ -112,11 +112,11 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
||||
onChange={e => setCommand(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
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
|
||||
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"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
@ -124,7 +124,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
|
||||
<button
|
||||
onClick={handleSendCommand}
|
||||
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"
|
||||
>
|
||||
<Send size={16} />
|
||||
|
||||
@ -33,17 +33,17 @@ export default function Drawer({ isOpen, onClose, title, children }: DrawerProps
|
||||
return (
|
||||
<div className="fixed inset-0 z-40">
|
||||
{/* 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 */}
|
||||
<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 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50 bg-gray-900/60 backdrop-blur-sm">
|
||||
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
|
||||
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<X size={20} />
|
||||
|
||||
@ -33,16 +33,16 @@ export default function Modal({ isOpen, onClose, title, children, maxWidth = 'ma
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* 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 */}
|
||||
<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 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700/50">
|
||||
<h2 className="text-lg font-semibold text-gray-100">{title}</h2>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.1]">
|
||||
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<X size={20} />
|
||||
|
||||
@ -9,12 +9,12 @@ interface PageHeaderProps {
|
||||
|
||||
export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) {
|
||||
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 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">
|
||||
<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>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,28 +8,28 @@ interface ServerStatusBadgeProps {
|
||||
const statusConfig: Record<ServerStatus, { label: string; className: string; dot: string }> = {
|
||||
online: {
|
||||
label: 'Online',
|
||||
className: 'bg-emerald-500/20 text-emerald-400',
|
||||
dot: 'bg-emerald-400',
|
||||
className: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
|
||||
dot: 'bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.6)]',
|
||||
},
|
||||
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',
|
||||
},
|
||||
starting: {
|
||||
label: 'Starting',
|
||||
className: 'bg-amber-500/20 text-amber-400',
|
||||
dot: 'bg-amber-400 animate-pulse',
|
||||
className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
|
||||
dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
|
||||
},
|
||||
stopping: {
|
||||
label: 'Stopping',
|
||||
className: 'bg-amber-500/20 text-amber-400',
|
||||
dot: 'bg-amber-400 animate-pulse',
|
||||
className: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
|
||||
dot: 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.6)]',
|
||||
},
|
||||
crashed: {
|
||||
label: 'Crashed',
|
||||
className: 'bg-red-500/20 text-red-400',
|
||||
dot: 'bg-red-400',
|
||||
className: 'bg-red-400/20 text-red-400 border border-red-400/30',
|
||||
dot: 'bg-red-400 shadow-[0_0_8px_rgba(248,113,113,0.6)]',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -63,16 +63,19 @@ export default function Sidebar() {
|
||||
const sidebarContent = (
|
||||
<>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-700/50">
|
||||
<Gamepad2 size={28} className="text-cyan-500 flex-shrink-0" />
|
||||
<div className="flex items-center gap-3 px-4 h-16 border-b border-white/[0.1]">
|
||||
<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) && (
|
||||
<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 */}
|
||||
{mobileOpen && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<X size={20} />
|
||||
@ -90,10 +93,10 @@ export default function Sidebar() {
|
||||
<Link
|
||||
key={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
|
||||
? 'bg-cyan-500/20 text-cyan-400'
|
||||
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'
|
||||
? '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-white/[0.08] hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} className="flex-shrink-0" />
|
||||
@ -104,7 +107,7 @@ export default function Sidebar() {
|
||||
</nav>
|
||||
|
||||
{/* 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 && (!collapsed || mobileOpen) && (
|
||||
<div className="px-3 py-2">
|
||||
@ -116,7 +119,7 @@ export default function Sidebar() {
|
||||
{/* Logout */}
|
||||
<button
|
||||
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" />
|
||||
{(!collapsed || mobileOpen) && <span>Logout</span>}
|
||||
@ -125,7 +128,7 @@ export default function Sidebar() {
|
||||
{/* Collapse Toggle — desktop only */}
|
||||
<button
|
||||
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 && <span className="text-sm">Collapse</span>}
|
||||
@ -137,10 +140,10 @@ export default function Sidebar() {
|
||||
return (
|
||||
<>
|
||||
{/* 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
|
||||
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"
|
||||
>
|
||||
<Menu size={22} />
|
||||
@ -159,7 +162,7 @@ export default function Sidebar() {
|
||||
|
||||
{/* Mobile sidebar drawer */}
|
||||
<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 ${
|
||||
mobileOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
@ -169,7 +172,7 @@ export default function Sidebar() {
|
||||
|
||||
{/* Desktop sidebar */}
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -66,7 +66,7 @@ export default function StatsChart({
|
||||
}, [data, maxDataPoints, height, maxY])
|
||||
|
||||
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 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@ -132,7 +132,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
||||
/>
|
||||
|
||||
{/* 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">
|
||||
{/* Search */}
|
||||
<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}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
|
||||
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent text-sm"
|
||||
className="w-full pl-10 pr-4 py-2 glass-input text-gray-100 rounded-lg
|
||||
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -153,8 +153,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
||||
key={filter.filterKey}
|
||||
value={activeFilters[filter.filterKey] || ''}
|
||||
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
|
||||
focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
|
||||
className="px-3 py-2 glass-input text-gray-100 rounded-lg text-sm
|
||||
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent"
|
||||
>
|
||||
<option value="">{filter.label}</option>
|
||||
{filter.options.map(opt => (
|
||||
@ -168,7 +168,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-gray-900 rounded-lg border border-gray-700/50 overflow-hidden">
|
||||
<div className="glass rounded-xl overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Spinner size="lg" />
|
||||
@ -176,7 +176,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
||||
) : filteredItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
{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" />
|
||||
</div>
|
||||
)}
|
||||
@ -190,8 +190,8 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-700">
|
||||
<thead className="bg-gray-800/50">
|
||||
<table className="min-w-full divide-y divide-white/[0.1]">
|
||||
<thead className="bg-white/[0.05]">
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th
|
||||
@ -211,12 +211,12 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-white/[0.08]">
|
||||
{filteredItems.map(item => (
|
||||
<tr
|
||||
key={getRowKey(item)}
|
||||
onClick={() => onRowClick?.(item)}
|
||||
className={`hover:bg-gray-800 transition-colors ${
|
||||
className={`hover:bg-white/[0.06] transition-colors ${
|
||||
onRowClick ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -5,11 +5,11 @@ interface BadgeProps {
|
||||
}
|
||||
|
||||
const badgeVariants = {
|
||||
info: 'bg-cyan-500/20 text-cyan-400',
|
||||
success: 'bg-emerald-500/20 text-emerald-400',
|
||||
warning: 'bg-amber-500/20 text-amber-400',
|
||||
error: 'bg-red-500/20 text-red-400',
|
||||
neutral: 'bg-gray-500/20 text-gray-400',
|
||||
info: 'bg-cyan-400/20 text-cyan-400 border border-cyan-400/30',
|
||||
success: 'bg-emerald-400/20 text-emerald-400 border border-emerald-400/30',
|
||||
warning: 'bg-amber-400/20 text-amber-400 border border-amber-400/30',
|
||||
error: 'bg-red-400/20 text-red-400 border border-red-400/30',
|
||||
neutral: 'bg-white/[0.1] text-gray-400 border border-white/[0.12]',
|
||||
}
|
||||
|
||||
export default function Badge({ variant = 'neutral', children, className = '' }: BadgeProps) {
|
||||
|
||||
@ -10,10 +10,10 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
}
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-cyan-500 hover:bg-cyan-600 text-white shadow-sm hover:shadow-md',
|
||||
secondary: 'bg-gray-700 hover:bg-gray-600 text-gray-200 border border-gray-600',
|
||||
danger: 'bg-red-500 hover:bg-red-600 text-white',
|
||||
ghost: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200',
|
||||
primary: 'bg-cyan-500 hover:bg-cyan-400 text-white shadow-lg shadow-cyan-500/25 hover:shadow-cyan-400/35',
|
||||
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-400 text-white shadow-lg shadow-red-500/25',
|
||||
ghost: 'text-gray-400 hover:bg-white/[0.08] hover:text-gray-200',
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
@ -36,7 +36,7 @@ export default function Button({
|
||||
return (
|
||||
<Component
|
||||
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
|
||||
${variants[variant]} ${sizes[size]} ${className}`}
|
||||
disabled={disabled || loading}
|
||||
|
||||
@ -18,9 +18,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
|
||||
placeholder-gray-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
|
||||
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
|
||||
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
|
||||
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' : ''
|
||||
} ${className}`}
|
||||
{...props}
|
||||
|
||||
@ -20,9 +20,10 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
)}
|
||||
<select
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-2 bg-gray-800 border border-gray-700 text-gray-100 rounded-lg
|
||||
focus:ring-2 focus:ring-cyan-500 focus:border-transparent
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
|
||||
className={`w-full px-4 py-2 glass-input text-gray-100 rounded-lg
|
||||
focus:ring-2 focus:ring-cyan-400/40 focus:border-transparent
|
||||
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' : ''
|
||||
} ${className}`}
|
||||
{...props}
|
||||
|
||||
@ -12,7 +12,9 @@ const sizes = {
|
||||
export default function Spinner({ size = 'md', className = '' }: SpinnerProps) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,16 +20,16 @@ const ConfirmationContext = createContext<ConfirmationContextType | undefined>(u
|
||||
|
||||
const typeStyles = {
|
||||
danger: {
|
||||
icon: 'bg-red-500/20 text-red-400',
|
||||
button: 'bg-red-500 hover:bg-red-600 text-white',
|
||||
icon: 'bg-red-400/15 text-red-400 border border-red-400/20',
|
||||
button: 'bg-red-500/90 hover:bg-red-400/90 text-white shadow-lg shadow-red-500/20 hover:shadow-red-400/30',
|
||||
},
|
||||
warning: {
|
||||
icon: 'bg-amber-500/20 text-amber-400',
|
||||
button: 'bg-amber-500 hover:bg-amber-600 text-white',
|
||||
icon: 'bg-amber-400/15 text-amber-400 border border-amber-400/20',
|
||||
button: 'bg-amber-500/90 hover:bg-amber-400/90 text-white shadow-lg shadow-amber-500/20 hover:shadow-amber-400/30',
|
||||
},
|
||||
info: {
|
||||
icon: 'bg-cyan-500/20 text-cyan-400',
|
||||
button: 'bg-cyan-500 hover:bg-cyan-600 text-white',
|
||||
icon: 'bg-cyan-400/15 text-cyan-400 border border-cyan-400/20',
|
||||
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 && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* 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 */}
|
||||
<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={`p-3 rounded-full ${styles.icon}`}>
|
||||
<AlertTriangle size={24} />
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{options.message}
|
||||
{options.itemName && (
|
||||
@ -94,7 +94,7 @@ export function ConfirmationProvider({ children }: { children: ReactNode }) {
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
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'}
|
||||
</button>
|
||||
|
||||
@ -20,10 +20,10 @@ const ToastContext = createContext<ToastContextType | undefined>(undefined)
|
||||
const TOAST_DURATION = 5000
|
||||
|
||||
const toastStyles: Record<ToastType, string> = {
|
||||
success: 'bg-emerald-500/20 border-emerald-500/50 text-emerald-400',
|
||||
error: 'bg-red-500/20 border-red-500/50 text-red-400',
|
||||
info: 'bg-cyan-500/20 border-cyan-500/50 text-cyan-400',
|
||||
warning: 'bg-amber-500/20 border-amber-500/50 text-amber-400',
|
||||
success: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
|
||||
error: 'bg-red-500/10 border-red-500/30 text-red-400',
|
||||
info: 'bg-cyan-500/10 border-cyan-500/30 text-cyan-400',
|
||||
warning: 'bg-amber-500/10 border-amber-500/30 text-amber-400',
|
||||
}
|
||||
|
||||
const toastIcons: Record<ToastType, typeof CheckCircle> = {
|
||||
@ -58,7 +58,8 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
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" />
|
||||
<span className="text-sm font-medium">{toast.message}</span>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user