Build a REACT projct for an IT infrastructure documentation and troubleshooting knowledge management platform called "MapIT".
Browse filesThis web application allows IT teams to inventory servers, virtual machines, network devices, and other infrastructure elements, map their relationships/dependencies, and record problems encountered with their solutions.
TARGET USERS:
- IT administrators and network engineers (primary)
- System administrators and DevOps teams
- IT managers viewing dashboards and reports
DESIGN REQUIREMENTS:
Visual Style:
- Modern, professional dashboard aesthetic similar to Tailwind UI or Vercel design system
- Clean, minimalist interface with ample white space
- Professional color palette: Primary (deep blue #1E40AF), Secondary (slate gray #475569), Accent (emerald green #10B981 for success states), Alert (amber #F59E0B for warnings)
- Consistent rounded corners (8px for cards, 6px for buttons)
- Subtle shadows and smooth transitions for depth
Typography:
- Font family: Inter or system fonts for readability
- Clear hierarchy: H1 (32px bold), H2 (24px semibold), Body (16px regular), Small text (14px)
- High contrast text for accessibility (WCAG AA compliant)
Layout & Responsiveness:
- Mobile-first responsive design (breakpoints: 640px, 768px, 1024px, 1280px)
- Sidebar navigation on desktop, collapsible hamburger menu on mobile
- Grid-based layouts with flexible containers
- Touch-friendly buttons (min 44x44px) for mobile
PAGES TO BUILD:
1. Login Page:
- Centered card layout with logo at top
- Email and password fields with icon prefixes
- "Remember me" checkbox and "Forgot password?" link
- Primary CTA button with loading state animation
- Optional: SSO buttons (Google, Microsoft) below divider
- Background: Subtle gradient or geometric pattern
2. Dashboard (Home):
- Top navigation bar: App logo, search bar, notifications bell, user avatar dropdown
- Left sidebar: Navigation menu with icons (Dashboard, Assets, Problems, Relationships, Reports, Settings)
- Main content area:
* KPI cards showing: Total Assets, Active Servers, Open Problems, Resolved This Month (with trend indicators)
* Interactive infrastructure topology visualization (network graph or tree view)
* Recent activity timeline
* Quick action buttons: Add Asset, Log Problem, View Map
3. Asset Inventory Page:
- Search and filter toolbar (by type, location, status, tags)
- Data table with sortable columns: Name, Type, IP Address, Location, Status, Last Updated
- Inline actions per row: View, Edit, Delete icons
- Pagination controls at bottom
- "Add New Asset" floating action button (FAB)
- Bulk actions checkbox for multi-select operations
4. Asset Detail Page:
- Header: Asset name, type badge, status indicator (online/offline dot)
- Tabbed interface: Overview, Specifications, Relationships, Problem History, Activity Log
- Overview tab: Key-value pairs in card layout (IP, OS, CPU, Memory, etc.)
- Relationships section: Visual graph showing connected assets with link labels
- Problem history: Accordion list of past issues with timestamps and resolution status
5. Problem/Solution Knowledge Base Page:
- Split view: Problem list on left (filterable by asset, severity, status), detail panel on right
- Each problem card: Title, affected asset tags, severity badge, timestamp, solved/unsolved indicator
- Detail panel: Problem description, affected assets list, solution steps (rich text with code blocks support), attachments section
- "Add New Problem" button with modal form
6. Relationship Map Page:
- Full-screen interactive network diagram showing asset dependencies
- Node types: Servers, VMs, Network devices, Services (color-coded)
- Zoom and pan controls
- Filter panel: Show/hide by asset type, location, or service
- Click node to view quick info tooltip, double-click to navigate to asset detail
7. Settings Page:
- Vertical tab navigation: Profile, Security, Notifications, Integrations, API Keys
- Form layouts with clear labels, input validation feedback
- Save/Cancel buttons with confirmation toasts
UI COMPONENTS TO INCLUDE:
- Reusable button variants (primary, secondary, danger, ghost)
- Input fields with floating labels or placeholder text, validation states
- Toast notifications for success/error feedback (top-right corner)
- Modal dialogs for confirmations and forms
- Dropdown menus with search capability
- Badge components for status indicators
- Loading skeletons for async content
- Empty states with illustrations and CTAs
- Breadcrumb navigation for deep pages
TECHNICAL SPECIFICATIONS:
- Framework: [React with TypeScript / Vue.js / Next.js - specify your preference]
- Styling: Tailwind CSS or styled-components
- Icons: Heroicons, Lucide Icons, or Font Awesome
- Charts/Graphs: Recharts or Chart.js for dashboards, Cytoscape.js or D3.js for network topology
- State management: Context API or Zustand for auth and global state
- Routing: React Router or Next.js built-in routing
- Form handling: React Hook Form with Zod validation
- API integration: Axios with mock endpoints (provide sample JSON structure)
ANIMATIONS & INTERACTIONS:
- Smooth page transitions (fade-in on mount)
- Hover effects on interactive elements (scale, color shift)
- Loading spinners during data fetches
- Success checkmark animations after form submissions
- Smooth expand/collapse for accordions and dropdowns
ACCESSIBILITY:
- Semantic HTML5 elements
- ARIA labels for screen readers
- Keyboard navigation support (tab order, focus indicators)
- Skip-to-content link
- Color contrast ratio ≥ 4.5:1
DO:
- Use consistent spacing (4px, 8px, 16px, 24px, 32px scale)
- Implement proper error boundaries
- Add proper TypeScript types for props
- Include helpful loading and error states
- Make forms accessible with proper labels
DON'T:
- Hardcode data - use mock API calls or props
- Mix design patterns - stay consistent
- Ignore mobile breakpoints
- Overcomplicate navigation structure
- Use low-contrast colors
OUTPUT FORMAT:
Provide complete, production-ready component code with:
- Organized folder structure (components/, pages/, utils/, styles/)
- Reusable component library
- README with setup instructions
- Responsive at all breakpoints
- Commented code for complex logic
- README.md +8 -5
- package.json +36 -0
- src/App.tsx +38 -0
- src/components/ActivityTimeline.tsx +121 -0
- src/components/KPICard.tsx +42 -0
- src/components/Layout.tsx +30 -0
- src/components/ProtectedRoute.tsx +30 -0
- src/components/Sidebar.tsx +94 -0
- src/components/ToastContainer.tsx +77 -0
- src/components/TopNav.tsx +78 -0
- src/contexts/AuthContext.tsx +87 -0
- src/contexts/ToastContext.tsx +57 -0
- src/index.css +106 -0
- src/main.tsx +16 -0
- src/pages/Dashboard.tsx +105 -0
- src/pages/Login.tsx +169 -0
- tailwind.config.js +73 -0
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: MapIT - Infrastructure Intelligence Hub 🗺️💻
|
| 3 |
+
colorFrom: purple
|
| 4 |
+
colorTo: green
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
json
|
| 2 |
+
{
|
| 3 |
+
"name": "mapit-infrastructure-docs",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"description": "IT infrastructure documentation and troubleshooting knowledge management platform",
|
| 6 |
+
"type": "module",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"dev": "vite",
|
| 9 |
+
"build": "vite build",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^18.2.0",
|
| 14 |
+
"react-dom": "^18.2.0",
|
| 15 |
+
"react-router-dom": "^6.8.1",
|
| 16 |
+
"lucide-react": "^0.294.0",
|
| 17 |
+
"axios": "^1.6.2",
|
| 18 |
+
"react-hook-form": "^7.48.2",
|
| 19 |
+
"zod": "^3.22.4",
|
| 20 |
+
"recharts": "^2.8.0",
|
| 21 |
+
"cytoscape": "^3.26.0",
|
| 22 |
+
"react-cytoscapejs": "^2.1.0"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@types/react": "^18.2.43",
|
| 26 |
+
"@types/react-dom": "^18.2.17",
|
| 27 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 28 |
+
"autoprefixer": "^10.4.16",
|
| 29 |
+
"postcss": "^8.4.32",
|
| 30 |
+
"tailwindcss": "^3.3.6",
|
| 31 |
+
"typescript": "^5.2.2",
|
| 32 |
+
"vite": "^5.0.8"
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { Routes, Route } from 'react-router-dom'
|
| 4 |
+
import { AuthProvider } from './contexts/AuthContext'
|
| 5 |
+
import { ToastProvider } from './contexts/ToastContext'
|
| 6 |
+
import ProtectedRoute from './components/ProtectedRoute'
|
| 7 |
+
import Layout from './components/Layout'
|
| 8 |
+
import Login from './pages/Login'
|
| 9 |
+
import Dashboard from './pages/Dashboard'
|
| 10 |
+
import AssetInventory from './pages/AssetInventory'
|
| 11 |
+
import AssetDetail from './pages/AssetDetail'
|
| 12 |
+
import KnowledgeBase from './pages/KnowledgeBase'
|
| 13 |
+
import RelationshipMap from './pages/RelationshipMap'
|
| 14 |
+
import Settings from './pages/Settings'
|
| 15 |
+
|
| 16 |
+
function App() {
|
| 17 |
+
return (
|
| 18 |
+
<ToastProvider>
|
| 19 |
+
<AuthProvider>
|
| 20 |
+
<Routes>
|
| 21 |
+
<Route path="/login" element={<Login />} />
|
| 22 |
+
<Route path="/" element={<ProtectedRoute><Layout /></ProtectedRoute>}>
|
| 23 |
+
<Route index element={<Dashboard />} />
|
| 24 |
+
<Route path="assets" element={<AssetInventory />} />
|
| 25 |
+
<Route path="assets/:id" element={<AssetDetail />} />
|
| 26 |
+
<Route path="knowledge" element={<KnowledgeBase />} />
|
| 27 |
+
<Route path="map" element={<RelationshipMap />} />
|
| 28 |
+
<Route path="settings" element={<Settings />} />
|
| 29 |
+
</Route>
|
| 30 |
+
</Routes>
|
| 31 |
+
</AuthProvider>
|
| 32 |
+
</ToastProvider>
|
| 33 |
+
)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export default App
|
| 37 |
+
|
| 38 |
+
</html>
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { CheckCircle, AlertTriangle, Info } from 'lucide-react'
|
| 4 |
+
|
| 5 |
+
interface ActivityItem {
|
| 6 |
+
id: string
|
| 7 |
+
type: 'success' | 'warning' | 'info'
|
| 8 |
+
title: string
|
| 9 |
+
description: string
|
| 10 |
+
timestamp: string
|
| 11 |
+
asset?: string
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const ActivityTimeline: React.FC = () => {
|
| 15 |
+
const activities: ActivityItem[] = [
|
| 16 |
+
{
|
| 17 |
+
id: '1',
|
| 18 |
+
type: 'success',
|
| 19 |
+
title: 'Database server restored',
|
| 20 |
+
description: 'Backup restoration completed successfully',
|
| 21 |
+
timestamp: '2 hours ago',
|
| 22 |
+
asset: 'db-prod-01'
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: '2',
|
| 26 |
+
type: 'warning',
|
| 27 |
+
title: 'High memory usage detected',
|
| 28 |
+
description: 'Web server memory usage above 90% threshold',
|
| 29 |
+
timestamp: '4 hours ago',
|
| 30 |
+
asset: 'web-staging-02'
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: '3',
|
| 34 |
+
type: 'info',
|
| 35 |
+
title: 'New asset added',
|
| 36 |
+
description: 'Added new load balancer to inventory',
|
| 37 |
+
timestamp: '6 hours ago'
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
id: '4',
|
| 41 |
+
type: 'success',
|
| 42 |
+
title: 'Network switch configured',
|
| 43 |
+
description: 'VLAN configuration updated successfully',
|
| 44 |
+
timestamp: '1 day ago',
|
| 45 |
+
asset: 'switch-dc-03'
|
| 46 |
+
}
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
const getActivityIcon = (type: string) => {
|
| 50 |
+
switch (type) {
|
| 51 |
+
case 'success':
|
| 52 |
+
return <CheckCircle className="h-4 w-4 text-accent-500" />
|
| 53 |
+
case 'warning':
|
| 54 |
+
return <AlertTriangle className="h-4 w-4 text-alert-500" />
|
| 55 |
+
case 'info':
|
| 56 |
+
return <Info className="h-4 w-4 text-primary-500" />
|
| 57 |
+
default:
|
| 58 |
+
return <Info className="h-4 w-4 text-primary-500" />
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
const getActivityColor = (type: string) => {
|
| 63 |
+
switch (type) {
|
| 64 |
+
case 'success':
|
| 65 |
+
return 'border-accent-200'
|
| 66 |
+
case 'warning':
|
| 67 |
+
return 'border-alert-200'
|
| 68 |
+
case 'info':
|
| 69 |
+
return 'border-primary-200'
|
| 70 |
+
default:
|
| 71 |
+
return 'border-gray-200'
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return (
|
| 76 |
+
<div className="flow-root">
|
| 77 |
+
<ul className="-mb-8">
|
| 78 |
+
{activities.map((activity, activityIdx) => (
|
| 79 |
+
<li key={activity.id}>
|
| 80 |
+
<div className="relative pb-8">
|
| 81 |
+
{activityIdx !== activities.length - 1 && (
|
| 82 |
+
<span
|
| 83 |
+
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
|
| 84 |
+
aria-hidden="true"
|
| 85 |
+
/>
|
| 86 |
+
)}
|
| 87 |
+
<div className="relative flex space-x-3">
|
| 88 |
+
<div>
|
| 89 |
+
<span className={`
|
| 90 |
+
h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white
|
| 91 |
+
${getActivityColor(activity.type)}
|
| 92 |
+
`}>
|
| 93 |
+
{getActivityIcon(activity.type)}
|
| 94 |
+
</span>
|
| 95 |
+
</div>
|
| 96 |
+
<div className="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
| 97 |
+
<div>
|
| 98 |
+
<p className="text-sm text-gray-900">{activity.title}</p>
|
| 99 |
+
<p className="text-sm text-gray-500">{activity.description}</p>
|
| 100 |
+
{activity.asset && (
|
| 101 |
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
| 102 |
+
{activity.asset}
|
| 103 |
+
</span>
|
| 104 |
+
)}
|
| 105 |
+
</div>
|
| 106 |
+
<div className="text-right text-sm whitespace-nowrap text-gray-500">
|
| 107 |
+
{activity.timestamp}
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</li>
|
| 113 |
+
))}
|
| 114 |
+
</ul>
|
| 115 |
+
</div>
|
| 116 |
+
)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
export default ActivityTimeline
|
| 120 |
+
|
| 121 |
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { LucideIcon } from 'lucide-react'
|
| 4 |
+
|
| 5 |
+
interface KPICardProps {
|
| 6 |
+
title: string
|
| 7 |
+
value: string
|
| 8 |
+
change: string
|
| 9 |
+
trend: 'up' | 'down'
|
| 10 |
+
icon: LucideIcon
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const KPICard: React.FC<KPICardProps> = ({ title, value, change, trend, icon: Icon }) => {
|
| 14 |
+
return (
|
| 15 |
+
<div className="card p-6 hover:shadow-md transition-shadow duration-200">
|
| 16 |
+
<div className="flex items-center">
|
| 17 |
+
<div className="flex-shrink-0">
|
| 18 |
+
<div className="h-12 w-12 bg-primary-100 rounded-full flex items-center justify-center">
|
| 19 |
+
<Icon className="h-6 w-6 text-primary-600" />
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div className="ml-4">
|
| 24 |
+
<p className="text-sm font-medium text-gray-600">{title}</p>
|
| 25 |
+
<p className="text-2xl font-bold text-gray-900">{value}</p>
|
| 26 |
+
<div className={`flex items-center text-sm ${trend === 'up' ? 'text-accent-600' : 'text-red-600'}`}>
|
| 27 |
+
{trend === 'up' ? (
|
| 28 |
+
<TrendingUp className="h-4 w-4 mr-1" />
|
| 29 |
+
) : (
|
| 30 |
+
<TrendingDown className="h-4 w-4 mr-1" />
|
| 31 |
+
)}
|
| 32 |
+
<span>{change} from last week</span>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export default KPICard
|
| 41 |
+
|
| 42 |
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React, { useState } from 'react'
|
| 3 |
+
import { Outlet } from 'react-router-dom'
|
| 4 |
+
import Sidebar from './Sidebar'
|
| 5 |
+
import TopNav from './TopNav'
|
| 6 |
+
import ToastContainer from './ToastContainer'
|
| 7 |
+
|
| 8 |
+
const Layout: React.FC = () => {
|
| 9 |
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
| 10 |
+
|
| 11 |
+
return (
|
| 12 |
+
<div className="flex h-screen bg-gray-50">
|
| 13 |
+
<Sidebar open={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
| 14 |
+
|
| 15 |
+
<div className="flex-1 flex flex-col overflow-hidden">
|
| 16 |
+
<TopNav onMenuClick={() => setSidebarOpen(true)} />
|
| 17 |
+
|
| 18 |
+
<main className="flex-1 overflow-y-auto p-4 md:p-6">
|
| 19 |
+
<Outlet />
|
| 20 |
+
</main>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<ToastContainer />
|
| 24 |
+
</div>
|
| 25 |
+
)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export default Layout
|
| 29 |
+
|
| 30 |
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { Navigate } from 'react-router-dom'
|
| 4 |
+
import { useAuth } from '../contexts/AuthContext'
|
| 5 |
+
|
| 6 |
+
interface ProtectedRouteProps {
|
| 7 |
+
children: React.ReactNode
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
| 11 |
+
const { user, isLoading } = useAuth()
|
| 12 |
+
|
| 13 |
+
if (isLoading) {
|
| 14 |
+
return (
|
| 15 |
+
<div className="min-h-screen flex items-center justify-center">
|
| 16 |
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
| 17 |
+
</div>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (!user) {
|
| 22 |
+
return <Navigate to="/login" replace />
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
return <>{children}</>
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export default ProtectedRoute
|
| 29 |
+
|
| 30 |
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { Link, useLocation } from 'react-router-dom'
|
| 4 |
+
import {
|
| 5 |
+
LayoutDashboard,
|
| 6 |
+
Server,
|
| 7 |
+
BookOpen,
|
| 8 |
+
Network,
|
| 9 |
+
Settings,
|
| 10 |
+
X
|
| 11 |
+
} from 'lucide-react'
|
| 12 |
+
|
| 13 |
+
interface SidebarProps {
|
| 14 |
+
open: boolean
|
| 15 |
+
onClose: () => void
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const navigation = [
|
| 19 |
+
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
| 20 |
+
{ name: 'Asset Inventory', href: '/assets', icon: Server },
|
| 21 |
+
{ name: 'Knowledge Base', href: '/knowledge', icon: BookOpen },
|
| 22 |
+
{ name: 'Relationship Map', href: '/map', icon: Network },
|
| 23 |
+
{ name: 'Settings', href: '/settings', icon: Settings },
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
const Sidebar: React.FC<SidebarProps> = ({ open, onClose }) => {
|
| 27 |
+
const location = useLocation()
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<>
|
| 31 |
+
{/* Mobile overlay */}
|
| 32 |
+
{open && (
|
| 33 |
+
<div
|
| 34 |
+
className="fixed inset-0 z-40 bg-gray-600 bg-opacity-75 lg:hidden"
|
| 35 |
+
onClick={onClose}
|
| 36 |
+
/>
|
| 37 |
+
)}
|
| 38 |
+
|
| 39 |
+
{/* Sidebar */}
|
| 40 |
+
<div className={`
|
| 41 |
+
fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-xl transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
|
| 42 |
+
${open ? 'translate-x-0' : '-translate-x-full'}
|
| 43 |
+
`}>
|
| 44 |
+
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
|
| 45 |
+
<div className="flex items-center">
|
| 46 |
+
<div className="flex-shrink-0">
|
| 47 |
+
<div className="h-8 w-8 bg-primary-600 rounded-button flex items-center justify-center">
|
| 48 |
+
<span className="text-white font-bold text-sm">MI</span>
|
| 49 |
+
</div>
|
| 50 |
+
<span className="ml-3 text-lg font-semibold text-gray-900">MapIT</span>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<button
|
| 55 |
+
onClick={onClose}
|
| 56 |
+
className="lg:hidden p-2 rounded-button text-gray-400 hover:text-gray-500 hover:bg-gray-100"
|
| 57 |
+
>
|
| 58 |
+
<X className="h-5 w-5" />
|
| 59 |
+
</button>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<nav className="mt-8 px-4 space-y-2">
|
| 63 |
+
{navigation.map((item) => {
|
| 64 |
+
const isActive = location.pathname === item.href
|
| 65 |
+
return (
|
| 66 |
+
<Link
|
| 67 |
+
key={item.name}
|
| 68 |
+
to={item.href}
|
| 69 |
+
className={`
|
| 70 |
+
group flex items-center px-3 py-2 text-sm font-medium rounded-button transition-colors duration-200
|
| 71 |
+
${isActive
|
| 72 |
+
? 'bg-primary-50 text-primary-700 border border-primary-200'
|
| 73 |
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
| 74 |
+
}
|
| 75 |
+
`}
|
| 76 |
+
onClick={() => window.innerWidth < 1024 && onClose()}
|
| 77 |
+
>
|
| 78 |
+
<item.icon className={`
|
| 79 |
+
mr-3 h-5 w-5 flex-shrink-0
|
| 80 |
+
${isActive ? 'text-primary-500' : 'text-gray-400 group-hover:text-gray-500'}
|
| 81 |
+
`} />
|
| 82 |
+
{item.name}
|
| 83 |
+
</Link>
|
| 84 |
+
)
|
| 85 |
+
})}
|
| 86 |
+
</nav>
|
| 87 |
+
</div>
|
| 88 |
+
</>
|
| 89 |
+
)
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export default Sidebar
|
| 93 |
+
|
| 94 |
+
</html>
|
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { useToast } from '../contexts/ToastContext'
|
| 4 |
+
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react'
|
| 5 |
+
|
| 6 |
+
const ToastContainer: React.FC = () => {
|
| 7 |
+
const { toasts, removeToast } = useToast()
|
| 8 |
+
|
| 9 |
+
const getToastIcon = (type: string) => {
|
| 10 |
+
switch (type) {
|
| 11 |
+
case 'success':
|
| 12 |
+
return <CheckCircle className="h-5 w-5 text-accent-500" />
|
| 13 |
+
case 'error':
|
| 14 |
+
return <XCircle className="h-5 w-5 text-red-500" />
|
| 15 |
+
case 'warning':
|
| 16 |
+
return <AlertTriangle className="h-5 w-5 text-alert-500" />
|
| 17 |
+
case 'info':
|
| 18 |
+
return <Info className="h-5 w-5 text-primary-500" />
|
| 19 |
+
default:
|
| 20 |
+
return <Info className="h-5 w-5 text-primary-500" />
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const getToastStyles = (type: string) => {
|
| 25 |
+
switch (type) {
|
| 26 |
+
case 'success':
|
| 27 |
+
return 'bg-accent-50 border-accent-200'
|
| 28 |
+
case 'error':
|
| 29 |
+
return 'bg-red-50 border-red-200'
|
| 30 |
+
case 'warning':
|
| 31 |
+
return 'bg-alert-50 border-alert-200'
|
| 32 |
+
case 'info':
|
| 33 |
+
return 'bg-primary-50 border-primary-200'
|
| 34 |
+
default:
|
| 35 |
+
return 'bg-gray-50 border-gray-200'
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="fixed top-4 right-4 z-50 space-y-3 max-w-sm">
|
| 41 |
+
{toasts.map((toast) => (
|
| 42 |
+
<div
|
| 43 |
+
key={toast.id}
|
| 44 |
+
className={`
|
| 45 |
+
card p-4 transform transition-all duration-300 ease-in-out
|
| 46 |
+
${getToastStyles(toast.type)}
|
| 47 |
+
`}
|
| 48 |
+
>
|
| 49 |
+
<div className="flex items-start">
|
| 50 |
+
<div className="flex-shrink-0">
|
| 51 |
+
{getToastIcon(toast.type)}
|
| 52 |
+
</div>
|
| 53 |
+
<div className="ml-3 flex-1">
|
| 54 |
+
<p className="text-sm font-medium text-gray-900">
|
| 55 |
+
{toast.title}
|
| 56 |
+
</p>
|
| 57 |
+
{toast.message && (
|
| 58 |
+
<p className="mt-1 text-sm text-gray-500">
|
| 59 |
+
{toast.message}
|
| 60 |
+
</p>
|
| 61 |
+
</div>
|
| 62 |
+
<button
|
| 63 |
+
onClick={() => removeToast(toast.id)}
|
| 64 |
+
className="ml-4 flex-shrink-0 p-1 rounded-button text-gray-400 hover:text-gray-500 hover:bg-gray-100"
|
| 65 |
+
>
|
| 66 |
+
<X className="h-4 w-4" />
|
| 67 |
+
</button>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
))}
|
| 71 |
+
</div>
|
| 72 |
+
)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
export default ToastContainer
|
| 76 |
+
|
| 77 |
+
</html>
|
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React, { useState } from 'react'
|
| 3 |
+
import { useAuth } from '../contexts/AuthContext'
|
| 4 |
+
import { Search, Bell, Menu, User, LogOut } from 'lucide-react'
|
| 5 |
+
|
| 6 |
+
interface TopNavProps {
|
| 7 |
+
onMenuClick: () => void
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const TopNav: React.FC<TopNavProps> = ({ onMenuClick }) => {
|
| 11 |
+
const { user, logout } = useAuth()
|
| 12 |
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<header className="bg-white shadow-sm border-b border-gray-200 z-30">
|
| 16 |
+
<div className="flex items-center justify-between h-16 px-4 md:px-6">
|
| 17 |
+
<div className="flex items-center">
|
| 18 |
+
<button
|
| 19 |
+
onClick={onMenuClick}
|
| 20 |
+
className="lg:hidden p-2 rounded-button text-gray-400 hover:text-gray-500 hover:bg-gray-100"
|
| 21 |
+
>
|
| 22 |
+
<Menu className="h-5 w-5" />
|
| 23 |
+
</button>
|
| 24 |
+
|
| 25 |
+
<div className="hidden md:block ml-4">
|
| 26 |
+
<div className="relative">
|
| 27 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 28 |
+
<Search className="h-4 w-4 text-gray-400" />
|
| 29 |
+
</div>
|
| 30 |
+
<input
|
| 31 |
+
type="text"
|
| 32 |
+
placeholder="Search assets, problems..."
|
| 33 |
+
className="input-field pl-10 pr-4 w-80"
|
| 34 |
+
/>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div className="flex items-center space-x-4">
|
| 40 |
+
<button className="relative p-2 rounded-button text-gray-400 hover:text-gray-500 hover:bg-gray-100">
|
| 41 |
+
<Bell className="h-5 w-5" />
|
| 42 |
+
<span className="absolute top-1 right-1 h-2 w-2 bg-alert-500 rounded-full"></span>
|
| 43 |
+
</button>
|
| 44 |
+
|
| 45 |
+
<div className="relative">
|
| 46 |
+
<button
|
| 47 |
+
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
| 48 |
+
className="flex items-center space-x-3 p-2 rounded-button hover:bg-gray-100"
|
| 49 |
+
>
|
| 50 |
+
<div className="h-8 w-8 bg-primary-100 rounded-full flex items-center justify-center">
|
| 51 |
+
<User className="h-4 w-4 text-primary-600" />
|
| 52 |
+
</div>
|
| 53 |
+
<span className="hidden md:block text-sm font-medium text-gray-700">
|
| 54 |
+
{user?.name}
|
| 55 |
+
</span>
|
| 56 |
+
</button>
|
| 57 |
+
|
| 58 |
+
{isDropdownOpen && (
|
| 59 |
+
<div className="absolute right-0 mt-2 w-48 bg-white rounded-card shadow-lg border border-gray-200 py-1">
|
| 60 |
+
<button
|
| 61 |
+
onClick={logout}
|
| 62 |
+
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
| 63 |
+
>
|
| 64 |
+
<LogOut className="mr-3 h-4 w-4" />
|
| 65 |
+
Sign out
|
| 66 |
+
</button>
|
| 67 |
+
</div>
|
| 68 |
+
)}
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</header>
|
| 73 |
+
)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export default TopNav
|
| 77 |
+
|
| 78 |
+
</html>
|
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
|
| 3 |
+
|
| 4 |
+
interface User {
|
| 5 |
+
id: string
|
| 6 |
+
name: string
|
| 7 |
+
email: string
|
| 8 |
+
role: string
|
| 9 |
+
avatar?: string
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
interface AuthContextType {
|
| 13 |
+
user: User | null
|
| 14 |
+
login: (email: string, password: string) => Promise<void>
|
| 15 |
+
logout: () => void
|
| 16 |
+
isLoading: boolean
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
| 20 |
+
|
| 21 |
+
export const useAuth = () => {
|
| 22 |
+
const context = useContext(AuthContext)
|
| 23 |
+
if (context === undefined) {
|
| 24 |
+
throw new Error('useAuth must be used within an AuthProvider')
|
| 25 |
+
}
|
| 26 |
+
return context
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
interface AuthProviderProps {
|
| 30 |
+
children: ReactNode
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
| 34 |
+
const [user, setUser] = useState<User | null>(null)
|
| 35 |
+
const [isLoading, setIsLoading] = useState(true)
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
// Check for stored auth token on app load
|
| 39 |
+
const token = localStorage.getItem('auth_token')
|
| 40 |
+
if (token) {
|
| 41 |
+
// Mock user data - in real app, verify token with backend
|
| 42 |
+
setUser({
|
| 43 |
+
id: '1',
|
| 44 |
+
name: 'John Doe',
|
| 45 |
+
email: 'john.doe@company.com',
|
| 46 |
+
role: 'admin'
|
| 47 |
+
})
|
| 48 |
+
}
|
| 49 |
+
setIsLoading(false)
|
| 50 |
+
}, [])
|
| 51 |
+
|
| 52 |
+
const login = async (email: string, password: string): Promise<void> => {
|
| 53 |
+
setIsLoading(true)
|
| 54 |
+
try {
|
| 55 |
+
// Mock API call
|
| 56 |
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
| 57 |
+
|
| 58 |
+
if (email === 'admin@mapit.com' && password === 'password') {
|
| 59 |
+
const userData: User = {
|
| 60 |
+
id: '1',
|
| 61 |
+
name: 'John Doe',
|
| 62 |
+
email: email,
|
| 63 |
+
role: 'admin'
|
| 64 |
+
}
|
| 65 |
+
setUser(userData)
|
| 66 |
+
localStorage.setItem('auth_token', 'mock_jwt_token')
|
| 67 |
+
} else {
|
| 68 |
+
throw new Error('Invalid credentials')
|
| 69 |
+
}
|
| 70 |
+
} finally {
|
| 71 |
+
setIsLoading(false)
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const logout = () => {
|
| 76 |
+
setUser(null)
|
| 77 |
+
localStorage.removeItem('auth_token')
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return (
|
| 81 |
+
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
|
| 82 |
+
{children}
|
| 83 |
+
</AuthContext.Provider>
|
| 84 |
+
)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React, { createContext, useContext, useState, ReactNode } from 'react'
|
| 3 |
+
|
| 4 |
+
export type ToastType = 'success' | 'error' | 'warning' | 'info'
|
| 5 |
+
|
| 6 |
+
interface Toast {
|
| 7 |
+
id: string
|
| 8 |
+
type: ToastType
|
| 9 |
+
title: string
|
| 10 |
+
message?: string
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
interface ToastContextType {
|
| 14 |
+
toasts: Toast[]
|
| 15 |
+
addToast: (toast: Omit<Toast, 'id'>) => void
|
| 16 |
+
removeToast: (id: string) => void
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const ToastContext = createContext<ToastContextType | undefined>(undefined)
|
| 20 |
+
|
| 21 |
+
export const useToast = () => {
|
| 22 |
+
const context = useContext(ToastContext)
|
| 23 |
+
if (context === undefined) {
|
| 24 |
+
throw new Error('useToast must be used within a ToastProvider')
|
| 25 |
+
}
|
| 26 |
+
return context
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
interface ToastProviderProps {
|
| 30 |
+
children: ReactNode
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
| 34 |
+
const [toasts, setToasts] = useState<Toast[]>([])
|
| 35 |
+
|
| 36 |
+
const addToast = (toast: Omit<Toast, 'id'>) => {
|
| 37 |
+
const id = Math.random().toString(36).substr(2, 9)
|
| 38 |
+
setToasts(prev => [...prev, { ...toast, id }])
|
| 39 |
+
|
| 40 |
+
// Auto remove after 5 seconds
|
| 41 |
+
setTimeout(() => {
|
| 42 |
+
removeToast(id)
|
| 43 |
+
}, 5000)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const removeToast = (id: string) => {
|
| 47 |
+
setToasts(prev => prev.filter(toast => toast.id !== id))
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
|
| 52 |
+
{children}
|
| 53 |
+
</ToastContext.Provider>
|
| 54 |
+
)
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 2 |
+
@tailwind base;
|
| 3 |
+
@tailwind components;
|
| 4 |
+
@tailwind utilities;
|
| 5 |
+
|
| 6 |
+
@layer base {
|
| 7 |
+
html {
|
| 8 |
+
font-family: 'Inter', system-ui, sans-serif;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
body {
|
| 12 |
+
@apply bg-gray-50 text-gray-900 antialiased;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
h1 {
|
| 16 |
+
@apply text-3xl font-bold;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
h2 {
|
| 20 |
+
@apply text-2xl font-semibold;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
p {
|
| 24 |
+
@apply text-base;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
small {
|
| 28 |
+
@apply text-sm;
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
@layer components {
|
| 33 |
+
.btn {
|
| 34 |
+
@apply inline-flex items-center justify-center px-4 py-2 rounded-button font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.btn-primary {
|
| 38 |
+
@apply btn bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.btn-secondary {
|
| 42 |
+
@apply btn bg-secondary-200 text-secondary-800 hover:bg-secondary-300 focus:ring-secondary-500;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.btn-danger {
|
| 46 |
+
@apply btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.btn-ghost {
|
| 50 |
+
@apply btn text-secondary-600 hover:text-secondary-800 hover:bg-secondary-100 focus:ring-secondary-500;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.input-field {
|
| 54 |
+
@apply block w-full px-3 py-2 border border-gray-300 rounded-button placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.card {
|
| 58 |
+
@apply bg-white rounded-card shadow-sm border border-gray-200;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.badge {
|
| 62 |
+
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.badge-success {
|
| 66 |
+
@apply badge bg-accent-100 text-accent-800;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.badge-warning {
|
| 70 |
+
@apply badge bg-alert-100 text-alert-800;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.badge-error {
|
| 74 |
+
@apply badge bg-red-100 text-red-800;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.badge-info {
|
| 78 |
+
@apply badge bg-blue-100 text-blue-800;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Smooth page transitions */
|
| 83 |
+
.page-enter {
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transform: translateY(10px);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.page-enter-active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
transform: translateY(0);
|
| 91 |
+
transition: opacity 300ms, transform 300ms;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Loading skeleton animation */
|
| 95 |
+
@keyframes pulse {
|
| 96 |
+
0%, 100% {
|
| 97 |
+
opacity: 1;
|
| 98 |
+
}
|
| 99 |
+
50% {
|
| 100 |
+
opacity: 0.5;
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.skeleton {
|
| 105 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 106 |
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import ReactDOM from 'react-dom/client'
|
| 4 |
+
import { BrowserRouter } from 'react-router-dom'
|
| 5 |
+
import App from './App.tsx'
|
| 6 |
+
import './index.css'
|
| 7 |
+
|
| 8 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 9 |
+
<React.StrictMode>
|
| 10 |
+
<BrowserRouter>
|
| 11 |
+
<App />
|
| 12 |
+
</BrowserRouter>
|
| 13 |
+
</React.StrictMode>,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
</html>
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React from 'react'
|
| 3 |
+
import { Link } from 'react-router-dom'
|
| 4 |
+
import { TrendingUp, TrendingDown, Plus, AlertTriangle } from 'lucide-react'
|
| 5 |
+
import KPICard from '../components/KPICard'
|
| 6 |
+
import ActivityTimeline from '../components/ActivityTimeline'
|
| 7 |
+
|
| 8 |
+
const Dashboard: React.FC = () => {
|
| 9 |
+
const mockKPIs = [
|
| 10 |
+
{
|
| 11 |
+
title: 'Total Assets',
|
| 12 |
+
value: '247',
|
| 13 |
+
change: '+12',
|
| 14 |
+
trend: 'up',
|
| 15 |
+
icon: Server
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
title: 'Active Servers',
|
| 19 |
+
value: '89',
|
| 20 |
+
change: '+3',
|
| 21 |
+
trend: 'up',
|
| 22 |
+
icon: TrendingUp
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
title: 'Open Problems',
|
| 26 |
+
value: '14',
|
| 27 |
+
change: '-2',
|
| 28 |
+
trend: 'down',
|
| 29 |
+
icon: TrendingDown
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
title: 'Resolved This Month',
|
| 33 |
+
value: '42',
|
| 34 |
+
change: '+8',
|
| 35 |
+
trend: 'up',
|
| 36 |
+
icon: CheckCircle
|
| 37 |
+
}
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<div className="space-y-6">
|
| 42 |
+
{/* Page Header */}
|
| 43 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between">
|
| 44 |
+
<div>
|
| 45 |
+
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
| 46 |
+
<p className="mt-1 text-sm text-gray-600">
|
| 47 |
+
Overview of your IT infrastructure and recent activity
|
| 48 |
+
</p>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div className="mt-4 sm:mt-0 flex space-x-3">
|
| 52 |
+
<Link to="/assets/new" className="btn-primary">
|
| 53 |
+
<Plus className="h-4 w-4 mr-2" />
|
| 54 |
+
Add Asset
|
| 55 |
+
</Link>
|
| 56 |
+
<Link to="/knowledge/new" className="btn-secondary">
|
| 57 |
+
<AlertTriangle className="h-4 w-4 mr-2" />
|
| 58 |
+
Log Problem
|
| 59 |
+
</Link>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
{/* KPI Grid */}
|
| 64 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 65 |
+
{mockKPIs.map((kpi, index) => (
|
| 66 |
+
<KPICard key={index} {...kpi} />
|
| 67 |
+
))}
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
{/* Main Content Grid */}
|
| 71 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 72 |
+
{/* Infrastructure Visualization */}
|
| 73 |
+
<div className="lg:col-span-2 card p-6">
|
| 74 |
+
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
| 75 |
+
Infrastructure Topology
|
| 76 |
+
</h2>
|
| 77 |
+
<div className="h-80 bg-gray-50 rounded-card border-2 border-dashed border-gray-300 flex items-center justify-center">
|
| 78 |
+
<div className="text-center">
|
| 79 |
+
<Network className="mx-auto h-12 w-12 text-gray-400" />
|
| 80 |
+
<p className="mt-2 text-sm text-gray-500">
|
| 81 |
+
Interactive network graph visualization
|
| 82 |
+
</p>
|
| 83 |
+
<Link to="/map" className="mt-3 btn-primary">
|
| 84 |
+
View Full Map
|
| 85 |
+
</Link>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
{/* Recent Activity */}
|
| 91 |
+
<div className="card p-6">
|
| 92 |
+
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
| 93 |
+
Recent Activity
|
| 94 |
+
</h2>
|
| 95 |
+
<ActivityTimeline />
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
)
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
export default Dashboard
|
| 104 |
+
|
| 105 |
+
</html>
|
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
typescript
|
| 2 |
+
import React, { useState } from 'react'
|
| 3 |
+
import { useNavigate } from 'react-router-dom'
|
| 4 |
+
import { useAuth } from '../contexts/AuthContext'
|
| 5 |
+
import { useToast } from '../contexts/ToastContext'
|
| 6 |
+
import { Server, Mail, Lock, Loader } from 'lucide-react'
|
| 7 |
+
|
| 8 |
+
const Login: React.FC = () => {
|
| 9 |
+
const [email, setEmail] = useState('')
|
| 10 |
+
const [password, setPassword] = useState('')
|
| 11 |
+
const [rememberMe, setRememberMe] = useState(false)
|
| 12 |
+
const { login, isLoading } = useAuth()
|
| 13 |
+
const { addToast } = useToast()
|
| 14 |
+
const navigate = useNavigate()
|
| 15 |
+
|
| 16 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 17 |
+
e.preventDefault()
|
| 18 |
+
try {
|
| 19 |
+
await login(email, password)
|
| 20 |
+
addToast({
|
| 21 |
+
type: 'success',
|
| 22 |
+
title: 'Welcome back!',
|
| 23 |
+
message: 'Successfully logged in.'
|
| 24 |
+
})
|
| 25 |
+
navigate('/')
|
| 26 |
+
} catch (error) {
|
| 27 |
+
addToast({
|
| 28 |
+
type: 'error',
|
| 29 |
+
title: 'Login failed',
|
| 30 |
+
message: 'Invalid email or password.'
|
| 31 |
+
})
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-accent-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
| 37 |
+
<div className="max-w-md w-full space-y-8">
|
| 38 |
+
{/* Logo */}
|
| 39 |
+
<div className="text-center">
|
| 40 |
+
<div className="mx-auto h-16 w-16 bg-primary-600 rounded-card flex items-center justify-center">
|
| 41 |
+
<Server className="h-8 w-8 text-white" />
|
| 42 |
+
</div>
|
| 43 |
+
<h2 className="mt-6 text-3xl font-bold text-gray-900">MapIT</h2>
|
| 44 |
+
<p className="mt-2 text-sm text-gray-600">
|
| 45 |
+
IT Infrastructure Documentation Platform
|
| 46 |
+
</p>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
{/* Login Form */}
|
| 50 |
+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
| 51 |
+
<div className="card p-6 shadow-xl">
|
| 52 |
+
<div className="space-y-4">
|
| 53 |
+
{/* Email Field */}
|
| 54 |
+
<div>
|
| 55 |
+
<label htmlFor="email" className="sr-only">
|
| 56 |
+
Email address
|
| 57 |
+
</label>
|
| 58 |
+
<div className="relative">
|
| 59 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 60 |
+
<Mail className="h-5 w-5 text-gray-400" />
|
| 61 |
+
</div>
|
| 62 |
+
<input
|
| 63 |
+
id="email"
|
| 64 |
+
name="email"
|
| 65 |
+
type="email"
|
| 66 |
+
autoComplete="email"
|
| 67 |
+
required
|
| 68 |
+
value={email}
|
| 69 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 70 |
+
className="input-field pl-10"
|
| 71 |
+
placeholder="Email address"
|
| 72 |
+
/>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* Password Field */}
|
| 77 |
+
<div>
|
| 78 |
+
<label htmlFor="password" className="sr-only">
|
| 79 |
+
Password
|
| 80 |
+
</label>
|
| 81 |
+
<div className="relative">
|
| 82 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 83 |
+
<Lock className="h-5 w-5 text-gray-400" />
|
| 84 |
+
</div>
|
| 85 |
+
<input
|
| 86 |
+
id="password"
|
| 87 |
+
name="password"
|
| 88 |
+
type="password"
|
| 89 |
+
autoComplete="current-password"
|
| 90 |
+
required
|
| 91 |
+
value={password}
|
| 92 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 93 |
+
className="input-field pl-10"
|
| 94 |
+
placeholder="Password"
|
| 95 |
+
/>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
{/* Remember Me & Forgot Password */}
|
| 100 |
+
<div className="flex items-center justify-between">
|
| 101 |
+
<div className="flex items-center">
|
| 102 |
+
<input
|
| 103 |
+
id="remember-me"
|
| 104 |
+
name="remember-me"
|
| 105 |
+
type="checkbox"
|
| 106 |
+
checked={rememberMe}
|
| 107 |
+
onChange={(e) => setRememberMe(e.target.checked)}
|
| 108 |
+
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
| 109 |
+
/>
|
| 110 |
+
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
|
| 111 |
+
Remember me
|
| 112 |
+
</label>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div className="text-sm">
|
| 116 |
+
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
|
| 117 |
+
Forgot your password?
|
| 118 |
+
</a>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
{/* Submit Button */}
|
| 123 |
+
<button
|
| 124 |
+
type="submit"
|
| 125 |
+
disabled={isLoading}
|
| 126 |
+
className="group relative w-full flex justify-center py-3 px-4 btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
| 127 |
+
>
|
| 128 |
+
{isLoading ? (
|
| 129 |
+
<>
|
| 130 |
+
<Loader className="animate-spin -ml-1 mr-3 h-5 w-5" />
|
| 131 |
+
Signing in...
|
| 132 |
+
</>
|
| 133 |
+
) : (
|
| 134 |
+
'Sign in to your account'
|
| 135 |
+
)}
|
| 136 |
+
</button>
|
| 137 |
+
|
| 138 |
+
{/* Divider */}
|
| 139 |
+
<div className="relative">
|
| 140 |
+
<div className="absolute inset-0 flex items-center">
|
| 141 |
+
<div className="w-full border-t border-gray-300"></div>
|
| 142 |
+
</div>
|
| 143 |
+
<div className="relative flex justify-center text-sm">
|
| 144 |
+
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
{/* SSO Options */}
|
| 149 |
+
<div className="grid grid-cols-2 gap-3">
|
| 150 |
+
<button
|
| 151 |
+
type="button"
|
| 152 |
+
className="w-full inline-flex justify-center py-2 px-4 btn-secondary">
|
| 153 |
+
Google
|
| 154 |
+
</button>
|
| 155 |
+
<button
|
| 156 |
+
type="button"
|
| 157 |
+
className="w-full inline-flex justify-center py-2 px-4 btn-secondary">
|
| 158 |
+
Microsoft
|
| 159 |
+
</button>
|
| 160 |
+
</div>
|
| 161 |
+
</form>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
)
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
export default Login
|
| 168 |
+
|
| 169 |
+
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
primary: {
|
| 11 |
+
50: '#eff6ff',
|
| 12 |
+
100: '#dbeafe',
|
| 13 |
+
200: '#bfdbfe',
|
| 14 |
+
300: '#93c5fd',
|
| 15 |
+
400: '#60a5fa',
|
| 16 |
+
500: '#3b82f6',
|
| 17 |
+
600: '#2563eb',
|
| 18 |
+
700: '#1d4ed8',
|
| 19 |
+
800: '#1e40af',
|
| 20 |
+
900: '#1e3a8a',
|
| 21 |
+
},
|
| 22 |
+
secondary: {
|
| 23 |
+
50: '#f8fafc',
|
| 24 |
+
100: '#f1f5f9',
|
| 25 |
+
200: '#e2e8f0',
|
| 26 |
+
300: '#cbd5e1',
|
| 27 |
+
400: '#94a3b8',
|
| 28 |
+
500: '#64748b',
|
| 29 |
+
600: '#475569',
|
| 30 |
+
700: '#334155',
|
| 31 |
+
800: '#1e293b',
|
| 32 |
+
900: '#0f172a',
|
| 33 |
+
},
|
| 34 |
+
accent: {
|
| 35 |
+
50: '#ecfdf5',
|
| 36 |
+
100: '#d1fae5',
|
| 37 |
+
200: '#a7f3d0',
|
| 38 |
+
300: '#6ee7b7',
|
| 39 |
+
400: '#34d399',
|
| 40 |
+
500: '#10b981',
|
| 41 |
+
600: '#059669',
|
| 42 |
+
700: '#047857',
|
| 43 |
+
800: '#065f46',
|
| 44 |
+
900: '#064e3b',
|
| 45 |
+
},
|
| 46 |
+
alert: {
|
| 47 |
+
50: '#fffbeb',
|
| 48 |
+
100: '#fef3c7',
|
| 49 |
+
200: '#fde68a',
|
| 50 |
+
300: '#fcd34d',
|
| 51 |
+
400: '#fbbf24',
|
| 52 |
+
500: '#f59e0b',
|
| 53 |
+
600: '#d97706',
|
| 54 |
+
700: '#b45309',
|
| 55 |
+
800: '#92400e',
|
| 56 |
+
900: '#78350f',
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
fontFamily: {
|
| 60 |
+
sans: ['Inter', 'system-ui', 'sans-serif'],
|
| 61 |
+
},
|
| 62 |
+
borderRadius: {
|
| 63 |
+
'card': '8px',
|
| 64 |
+
'button': '6px',
|
| 65 |
+
},
|
| 66 |
+
spacing: {
|
| 67 |
+
'18': '4.5rem',
|
| 68 |
+
'88': '22rem',
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
plugins: [],
|
| 73 |
+
}
|