feat: add admin-web application with React and Vite
feat(admin-web): implement basic layout and navigation feat(admin-web): implement dashboard page with key metrics feat(admin-web): implement user management page feat(admin-web): implement analytics page with user stats feat(admin-web): implement logs explorer page style(admin-web): add tailwind css and shadcn/ui components build(admin-web): configure eslint and prettier for code quality This commit introduces the admin-web application, a React-based administration console built with Vite. - The application provides a central interface for managing users, monitoring platform health, and troubleshooting issues. - It includes a basic layout with navigation, a dashboard displaying key metrics, a user management page, an analytics page with user statistics, and a logs explorer page. - Tailwind CSS and shadcn/ui components are used for styling. - ESLint and Prettier are configured to ensure code quality.
This commit is contained in:
61
admin-web/src/pages/Analytics.jsx
Normal file
61
admin-web/src/pages/Analytics.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
|
||||
const topUsers = [
|
||||
{ email: "ian.gomez@example.com", visits: 6950 },
|
||||
{ email: "admin@krow.com", visits: 193 },
|
||||
{ email: "paula.orte@example.com", visits: 81 },
|
||||
{ email: "test.user@example.com", visits: 50 },
|
||||
];
|
||||
|
||||
const topPages = [
|
||||
{ name: "VendorManagement", visits: 718 },
|
||||
{ name: "Dashboard", visits: 600 },
|
||||
{ name: "unknown", visits: 587 },
|
||||
{ name: "ClientDashboard", visits: 475 },
|
||||
{ name: "Events", visits: 456 },
|
||||
];
|
||||
|
||||
export default function Analytics() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-800">Analytics</h1>
|
||||
<p className="mt-2 text-base text-slate-600">
|
||||
An overview of your application's data.
|
||||
</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-white shadow-lg rounded-2xl p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900">Total Unique Users</h3>
|
||||
<p className="mt-2 text-4xl font-bold text-blue-600">3</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900">Top Users</h3>
|
||||
<ul className="mt-4 space-y-4">
|
||||
{topUsers.map((user) => (
|
||||
<li key={user.email} className="bg-white shadow-lg rounded-2xl p-4">
|
||||
<p className="font-semibold text-slate-800">{user.email}</p>
|
||||
<p className="text-slate-500">{user.visits.toLocaleString()} visits</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900">Top Pages</h3>
|
||||
<ul className="mt-4 space-y-4">
|
||||
{topPages.map((page) => (
|
||||
<li key={page.name} className="bg-white shadow-lg rounded-2xl p-4 flex justify-between items-center">
|
||||
<p className="font-semibold text-slate-800">{page.name}</p>
|
||||
<p className="text-slate-500 font-medium">{page.visits.toLocaleString()}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
admin-web/src/pages/Dashboard.jsx
Normal file
38
admin-web/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import { Users, Building2, Mail, FileText } from "lucide-react";
|
||||
|
||||
const stats = [
|
||||
{ name: "Total Users", stat: "15", icon: Users, color: "bg-blue-500" },
|
||||
{ name: "Active Vendors", stat: "8", icon: Building2, color: "bg-green-500" },
|
||||
{ name: "Pending Invitations", stat: "3", icon: Mail, color: "bg-yellow-500" },
|
||||
{ name: "Open Orders", stat: "12", icon: FileText, color: "bg-purple-500" },
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-800">Admin Dashboard</h1>
|
||||
<p className="mt-2 text-base text-slate-600">
|
||||
Welcome to the Krow Admin Console. Here is a quick overview of the platform.
|
||||
</p>
|
||||
|
||||
<div className="mt-8">
|
||||
<dl className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((item) => (
|
||||
<div key={item.name} className="relative overflow-hidden rounded-2xl bg-white px-4 py-5 shadow-lg sm:px-6 sm:py-6">
|
||||
<dt>
|
||||
<div className={`absolute rounded-xl p-3 ${item.color}`}>
|
||||
<item.icon className="h-8 w-8 text-white" aria-hidden="true" />
|
||||
</div>
|
||||
<p className="ml-20 truncate text-sm font-medium text-gray-500">{item.name}</p>
|
||||
</dt>
|
||||
<dd className="ml-20 flex items-baseline">
|
||||
<p className="text-3xl font-semibold text-gray-900">{item.stat}</p>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
admin-web/src/pages/Layout.jsx
Normal file
74
admin-web/src/pages/Layout.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Building2, Users, BarChart3, Terminal, LayoutDashboard, LogOut } from "lucide-react";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/admin-web/", icon: LayoutDashboard },
|
||||
{ name: "User Management", href: "/admin-web/user-management", icon: Users },
|
||||
{ name: "Analytics", href: "/admin-web/analytics", icon: BarChart3 },
|
||||
{ name: "Logs Explorer", href: "/admin-web/logs", icon: Terminal },
|
||||
];
|
||||
|
||||
export default function Layout({ children }) {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50">
|
||||
{/* Sidebar */}
|
||||
<div className="hidden md:flex md:flex-shrink-0">
|
||||
<div className="flex flex-col w-64">
|
||||
{/* Sidebar header */}
|
||||
<div className="flex items-center h-16 flex-shrink-0 px-4 bg-white border-b border-slate-200">
|
||||
<Building2 className="h-8 w-8 text-blue-600" />
|
||||
<span className="ml-3 font-bold text-xl text-slate-800">Krow Admin</span>
|
||||
</div>
|
||||
{/* Sidebar content */}
|
||||
<div className="flex-1 flex flex-col overflow-y-auto bg-white">
|
||||
<nav className="flex-1 px-4 py-4">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
className={`
|
||||
${
|
||||
location.pathname === item.href
|
||||
? "bg-blue-50 text-blue-600"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
|
||||
}
|
||||
group flex items-center px-3 py-3 text-sm font-medium rounded-lg
|
||||
`}
|
||||
>
|
||||
<item.icon
|
||||
className="mr-3 h-6 w-6"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="px-4 py-4 border-t border-slate-200">
|
||||
<Link
|
||||
to="#"
|
||||
className="group flex items-center px-3 py-3 text-sm font-medium rounded-lg text-slate-600 hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
<LogOut className="mr-3 h-6 w-6" />
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-col w-0 flex-1 overflow-hidden">
|
||||
<main className="flex-1 relative z-0 overflow-y-auto focus:outline-none">
|
||||
<div className="py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
admin-web/src/pages/Logs.jsx
Normal file
54
admin-web/src/pages/Logs.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
|
||||
const logs = [
|
||||
{ level: "INFO", message: "User admin@krow.com logged in.", timestamp: "2024-05-21T10:00:00Z" },
|
||||
{ level: "WARN", message: "API response time is slow: 2500ms for /api/vendors", timestamp: "2024-05-21T10:01:00Z" },
|
||||
{ level: "ERROR", message: "Failed to process payment for invoice INV-12345.", timestamp: "2024-05-21T10:02:00Z" },
|
||||
{ level: "INFO", message: "New vendor 'New Staffing Co' invited by admin@krow.com.", timestamp: "2024-05-21T10:05:00Z" },
|
||||
];
|
||||
|
||||
export default function Logs() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-800">Logs Explorer</h1>
|
||||
<p className="mt-2 text-base text-slate-600">
|
||||
Monitor system logs across your application.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flow-root">
|
||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-2xl">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Level</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Message</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{logs.map((log, index) => (
|
||||
<tr key={index}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
||||
log.level === 'ERROR' ? 'bg-red-100 text-red-800' :
|
||||
log.level === 'WARN' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{log.level}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-4 text-sm text-gray-500 font-mono">{log.message}</td>
|
||||
<td className="px-3 py-4 text-sm text-gray-500">{new Date(log.timestamp).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
146
admin-web/src/pages/UserManagement.jsx
Normal file
146
admin-web/src/pages/UserManagement.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useState } from "react";
|
||||
import { Plus, Send } from "lucide-react";
|
||||
|
||||
const users = [
|
||||
{ name: "Alex Johnson", email: "alex.j@krow.com", role: "Admin", status: "Active", avatar: "https://randomuser.me/api/portraits/men/1.jpg" },
|
||||
{ name: "Samantha Lee", email: "samantha.lee@vendor.com", role: "Vendor", status: "Active", avatar: "https://randomuser.me/api/portraits/women/2.jpg" },
|
||||
{ name: "Michael Chen", email: "michael.chen@client.com", role: "Client", status: "Pending", avatar: "https://randomuser.me/api/portraits/men/3.jpg" },
|
||||
];
|
||||
|
||||
function InviteUserModal({ isOpen, onClose }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
|
||||
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<h3 className="text-xl font-semibold leading-6 text-slate-900">Send Invitation</h3>
|
||||
<p className="mt-2 text-sm text-slate-500">The user will receive an email with a link to set up their account.</p>
|
||||
<div className="mt-6 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
className="mt-2 block w-full rounded-lg border-0 py-2.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
placeholder="colleague@company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="role" className="block text-sm font-medium text-gray-700">
|
||||
Access level
|
||||
</label>
|
||||
<select
|
||||
id="role"
|
||||
name="role"
|
||||
className="mt-2 block w-full rounded-lg border-0 py-2.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
defaultValue="User"
|
||||
>
|
||||
<option>Admin</option>
|
||||
<option>Vendor</option>
|
||||
<option>Client</option>
|
||||
<option>User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 sm:ml-3 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Send className="h-5 w-5 mr-2" />
|
||||
Send Invitation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserManagement() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<h1 className="text-3xl font-bold text-slate-800">User Management</h1>
|
||||
<p className="mt-2 text-base text-slate-600">
|
||||
A list of all the users in your account including their name, email and role.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-2" />
|
||||
Invite User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flow-root">
|
||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-2xl">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Name</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Email</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Role</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{users.map((user) => (
|
||||
<tr key={user.email}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
|
||||
<div className="flex items-center">
|
||||
<div className="h-11 w-11 flex-shrink-0">
|
||||
<img className="h-11 w-11 rounded-full" src={user.avatar} alt="" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-medium text-gray-900">{user.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.email}</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
user.status === 'Active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{user.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InviteUserModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user