feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js

- Added main application entry point (app.jsx) and bootstrap file for React setup.
- Created Wiki index page (Index.jsx) for documentation and file management.
- Implemented utility functions for formatting and status handling in utils.js.
- Set up Blade template (app.blade.php) for Inertia.js integration.
- Defined API routes for metrics and terminal access in api.php.
- Established console commands for scheduled tasks in console.php.
- Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php.
- Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments.
- Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js.
- Set up Vite for asset bundling and development server in vite.config.js.
This commit is contained in:
I-Fun
2026-04-07 20:29:32 +07:00
commit c649f055d8
126 changed files with 8122 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronUp, ChevronDown, Search, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
export function DataTable({ columns, data, onRowClick, emptyMessage = 'No data found', searchable = true, className }) {
const [search, setSearch] = useState('');
const [sortCol, setSortCol] = useState(null);
const [sortDir, setSortDir] = useState('asc');
const [page, setPage] = useState(1);
const perPage = 15;
const filtered = search
? data.filter(row => columns.some(col => {
const val = col.accessor ? row[col.accessor] : '';
return String(val ?? '').toLowerCase().includes(search.toLowerCase());
}))
: data;
const sorted = sortCol
? [...filtered].sort((a, b) => {
const va = a[sortCol] ?? '';
const vb = b[sortCol] ?? '';
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
return sortDir === 'asc' ? cmp : -cmp;
})
: filtered;
const totalPages = Math.ceil(sorted.length / perPage);
const paginated = sorted.slice((page - 1) * perPage, page * perPage);
const handleSort = (accessor) => {
if (sortCol === accessor) {
setSortDir(d => d === 'asc' ? 'desc' : 'asc');
} else {
setSortCol(accessor);
setSortDir('asc');
}
setPage(1);
};
return (
<div className={cn('space-y-3', className)}>
{searchable && (
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
<input
type="text"
placeholder="Search..."
value={search}
onChange={e => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-9 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-cyan-500/50 focus:bg-white/[0.07] transition-all"
/>
</div>
)}
<div className="overflow-x-auto rounded-xl border border-white/10">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/10 bg-white/[0.02]">
{columns.map(col => (
<th key={col.key ?? col.accessor}
className={cn(
'text-left px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider whitespace-nowrap',
col.accessor && 'cursor-pointer hover:text-white/70 select-none'
)}
onClick={() => col.accessor && handleSort(col.accessor)}
>
<div className="flex items-center gap-1">
{col.header}
{col.accessor && sortCol === col.accessor && (
sortDir === 'asc'
? <ChevronUp className="w-3 h-3 text-cyan-400" />
: <ChevronDown className="w-3 h-3 text-cyan-400" />
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
<AnimatePresence mode="wait">
{paginated.length === 0 ? (
<tr>
<td colSpan={columns.length} className="text-center py-12 text-white/30">
{emptyMessage}
</td>
</tr>
) : (
paginated.map((row, i) => (
<motion.tr
key={row.id ?? i}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.02 }}
onClick={() => onRowClick?.(row)}
className={cn(
'border-b border-white/[0.05] last:border-0 transition-colors',
onRowClick && 'cursor-pointer hover:bg-white/[0.03]'
)}
>
{columns.map(col => (
<td key={col.key ?? col.accessor} className="px-4 py-3 text-white/80">
{col.render ? col.render(row) : row[col.accessor]}
</td>
))}
</motion.tr>
))
)}
</AnimatePresence>
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between text-xs text-white/40">
<span>{filtered.length} results</span>
<div className="flex items-center gap-1">
<button onClick={() => setPage(1)} disabled={page === 1} className="p-1 hover:text-white disabled:opacity-30">
<ChevronsLeft className="w-4 h-4" />
</button>
<button onClick={() => setPage(p => p - 1)} disabled={page === 1} className="p-1 hover:text-white disabled:opacity-30">
<ChevronLeft className="w-4 h-4" />
</button>
<span className="px-3 text-white">Page {page} of {totalPages}</span>
<button onClick={() => setPage(p => p + 1)} disabled={page === totalPages} className="p-1 hover:text-white disabled:opacity-30">
<ChevronRight className="w-4 h-4" />
</button>
<button onClick={() => setPage(totalPages)} disabled={page === totalPages} className="p-1 hover:text-white disabled:opacity-30">
<ChevronsRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
);
}