feat: inisialisasi project kit v2
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import React from 'react';
|
||||
import { PageProps } from '@/types';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ArcElement,
|
||||
} from 'chart.js';
|
||||
import { Line, Bar, Doughnut } from 'react-chartjs-2';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
ArcElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
|
||||
interface RecentUser {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
status: string;
|
||||
avatar_url?: string;
|
||||
created_at: string;
|
||||
roles: { name: string }[];
|
||||
}
|
||||
|
||||
interface ChartItem {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
totalRoles: number;
|
||||
recentUsers: RecentUser[];
|
||||
charts: {
|
||||
userGrowth: ChartItem[];
|
||||
activityStats: ChartItem[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardProps extends PageProps {
|
||||
stats: DashboardStats;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, icon, variant = 'white', delay = '0s' }: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
sub: string;
|
||||
icon: React.ReactNode;
|
||||
variant?: 'white' | 'dark' | 'gold' | 'teal';
|
||||
delay?: string;
|
||||
}) {
|
||||
const styles = {
|
||||
white: 'bg-white text-[#3D4E4B] border border-gray-100',
|
||||
dark: 'bg-[#3D4E4B] text-white border border-[#3D4E4B]',
|
||||
gold: 'bg-[#D4A017] text-white border border-[#D4A017]',
|
||||
teal: 'bg-[#21A59F] text-white border border-[#21A59F]',
|
||||
};
|
||||
return (
|
||||
<div className={`relative rounded-2xl p-6 anim-up shadow-sm hover:-translate-y-0.5 transition-transform duration-200 ${styles[variant]}`} style={{ animationDelay: delay }}>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold opacity-60">{label}</p>
|
||||
<p className="text-3xl font-bold tracking-tighter mt-1">{typeof value === 'number' ? value.toLocaleString() : value}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-black/5 border border-white/10 flex items-center justify-center shrink-0">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-semibold opacity-60">{sub}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Dashboard({ stats }: DashboardProps) {
|
||||
const { totalUsers, activeUsers, totalRoles, recentUsers, charts } = stats;
|
||||
const inactiveUsers = totalUsers - activeUsers;
|
||||
|
||||
// User Growth Chart Configuration
|
||||
const growthData = {
|
||||
labels: charts.userGrowth.map(d => d.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'New Registrations',
|
||||
data: charts.userGrowth.map(d => d.value),
|
||||
borderColor: '#D4A017',
|
||||
backgroundColor: 'rgba(212, 160, 23, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#D4A017',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Activity Bar Chart Configuration
|
||||
const activityData = {
|
||||
labels: charts.activityStats.map(d => d.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'System Activity',
|
||||
data: charts.activityStats.map(d => d.value),
|
||||
backgroundColor: '#3D4E4B',
|
||||
borderRadius: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: '#3D4E4B',
|
||||
titleFont: { size: 12, weight: 'bold' as const },
|
||||
padding: 12,
|
||||
cornerRadius: 12,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { font: { size: 10, weight: 'bold' as const }, color: '#9ca3af' } },
|
||||
y: { border: { display: false }, ticks: { font: { size: 10, weight: 'bold' as const }, color: '#9ca3af' }, grid: { color: '#f3f4f6' } },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label="Total Users" value={totalUsers} sub="All registered accounts"
|
||||
variant="gold" delay="0s"
|
||||
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>}
|
||||
/>
|
||||
<StatCard
|
||||
label="Active Users" value={activeUsers} sub={`${totalUsers > 0 ? Math.round((activeUsers / totalUsers) * 100) : 0}% of total`}
|
||||
variant="dark" delay="0.08s"
|
||||
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>}
|
||||
/>
|
||||
<StatCard
|
||||
label="Inactive Users" value={inactiveUsers} sub="Suspended or deactivated"
|
||||
variant="teal" delay="0.16s"
|
||||
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>}
|
||||
/>
|
||||
<StatCard
|
||||
label="Roles" value={totalRoles} sub="Defined permission sets"
|
||||
variant="white" delay="0.24s"
|
||||
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* Growth Chart */}
|
||||
<div className="lg:col-span-8 bg-white rounded-2xl border border-gray-100 shadow-sm p-8 anim-up" style={{ animationDelay: '0.32s' }}>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h3 className="text-base font-black text-[#3D4E4B] tracking-tight leading-none">User Growth</h3>
|
||||
<p className="text-xs font-bold text-gray-400 mt-2 tracking-tight">Registration metrics over the last 6 months</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#D4A017]" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-[#D4A017]">Accounts</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[300px]">
|
||||
<Line data={growthData} options={chartOptions as any} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdown Chart */}
|
||||
<div className="lg:col-span-4 bg-[#3D4E4B] rounded-2xl p-8 text-white anim-up shadow-lg shadow-[#3D4E4B]/20" style={{ animationDelay: '0.4s' }}>
|
||||
<h3 className="text-base font-black tracking-tight leading-none mb-2">Account Integrity</h3>
|
||||
<p className="text-xs font-bold text-white/40 tracking-tight mb-8 uppercase tracking-widest">Global Status Registry</p>
|
||||
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-48 h-48 mb-8">
|
||||
<Doughnut
|
||||
data={{
|
||||
labels: ['Active', 'Inactive'],
|
||||
datasets: [{
|
||||
data: [activeUsers, inactiveUsers],
|
||||
backgroundColor: ['#D4A017', 'rgba(255,255,255,0.1)'],
|
||||
borderWidth: 0,
|
||||
cutout: '75%',
|
||||
}]
|
||||
}}
|
||||
options={{
|
||||
plugins: { legend: { display: false } },
|
||||
maintainAspectRatio: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-[#D4A017]" />
|
||||
<span className="text-xs font-bold opacity-60">Verified Active</span>
|
||||
</div>
|
||||
<span className="text-sm font-black">{activeUsers}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-white/20" />
|
||||
<span className="text-xs font-bold opacity-60">Restricted/Inactive</span>
|
||||
</div>
|
||||
<span className="text-sm font-black">{inactiveUsers}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* Activity Logs Bar Chart */}
|
||||
<div className="lg:col-span-4 bg-white rounded-2xl border border-gray-100 shadow-sm p-8 anim-up" style={{ animationDelay: '0.48s' }}>
|
||||
<h3 className="text-base font-black text-[#3D4E4B] tracking-tight leading-none mb-8">System Pulse</h3>
|
||||
<div className="h-[250px]">
|
||||
<Bar data={activityData} options={chartOptions as any} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Users Table */}
|
||||
<div className="lg:col-span-8 bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden anim-up" style={{ animationDelay: '0.56s' }}>
|
||||
<div className="flex items-center justify-between px-8 py-6 border-b border-gray-50">
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-[#3D4E4B] tracking-tight uppercase tracking-widest">Recent Registry</h3>
|
||||
</div>
|
||||
<Link href="/users" className="text-[10px] font-black uppercase tracking-widest text-[#D4A017] hover:text-[#B88B14] transition-colors">
|
||||
View Full Archive →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-50">
|
||||
{recentUsers.map(u => {
|
||||
const initials = `${u.first_name?.charAt(0) ?? ''}${u.last_name?.charAt(0) ?? ''}`.toUpperCase();
|
||||
return (
|
||||
<div key={u.id} className="flex items-center justify-between px-8 py-5 hover:bg-gray-50/30 transition-colors group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-2xl bg-[#3D4E4B] text-white text-[10px] font-black flex items-center justify-center border-2 border-white shadow-lg shadow-[#3D4E4B]/10 overflow-hidden">
|
||||
{u.avatar_url ? <img src={u.avatar_url} className="w-full h-full object-cover" alt="" /> : initials}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-[#3D4E4B] tracking-tight">{u.first_name} {u.last_name}</p>
|
||||
<p className="text-[10px] font-bold text-gray-300 uppercase tracking-widest mt-1">{u.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<span className={`px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border ${u.status === 'active' ? 'text-emerald-600 border-emerald-100 bg-emerald-50/50' : 'text-gray-400 border-gray-100 bg-gray-50/50'}`}>
|
||||
{u.status}
|
||||
</span>
|
||||
<Link href={route('users.show', u.id)} className="w-8 h-8 rounded-xl bg-gray-50 flex items-center justify-center text-gray-400 hover:bg-[#3D4E4B] hover:text-white transition-all">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M9 5l7 7-7 7" /></svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user