primus-saas-react 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +568 -0
- package/package.json +15 -9
- package/DEMO.md +0 -68
- package/INTEGRATION.md +0 -702
- package/build_log.txt +0 -0
- package/postcss.config.js +0 -6
- package/src/components/ai/AICopilot.tsx +0 -88
- package/src/components/auth/PrimusLogin.tsx +0 -298
- package/src/components/auth/UserProfile.tsx +0 -26
- package/src/components/banking/accounts/AccountDashboard.tsx +0 -67
- package/src/components/banking/cards/CreditCardVisual.tsx +0 -67
- package/src/components/banking/credit/CreditScoreCard.tsx +0 -80
- package/src/components/banking/kyc/KYCVerification.tsx +0 -76
- package/src/components/banking/loans/LoanCalculator.tsx +0 -106
- package/src/components/banking/transactions/TransactionHistory.tsx +0 -74
- package/src/components/crud/PrimusDataTable.tsx +0 -220
- package/src/components/crud/PrimusModal.tsx +0 -68
- package/src/components/dashboard/PrimusDashboard.tsx +0 -145
- package/src/components/documents/DocumentViewer.tsx +0 -107
- package/src/components/featureflags/FeatureFlagToggle.tsx +0 -64
- package/src/components/insurance/agents/AgentDirectory.tsx +0 -72
- package/src/components/insurance/claims/ClaimStatusTracker.tsx +0 -78
- package/src/components/insurance/fraud/FraudDetectionDashboard.tsx +0 -68
- package/src/components/insurance/policies/PolicyCard.tsx +0 -77
- package/src/components/insurance/premium/PremiumCalculator.tsx +0 -104
- package/src/components/insurance/quotes/QuoteComparison.tsx +0 -75
- package/src/components/layout/PrimusHeader.tsx +0 -75
- package/src/components/layout/PrimusLayout.tsx +0 -47
- package/src/components/layout/PrimusSidebar.tsx +0 -102
- package/src/components/logging/LogViewer.tsx +0 -90
- package/src/components/notifications/NotificationFeed.tsx +0 -106
- package/src/components/notifications/PrimusNotificationCenter.tsx +0 -282
- package/src/components/payments/CheckoutForm.tsx +0 -167
- package/src/components/security/SecurityDashboard.tsx +0 -83
- package/src/components/shared/Button.tsx +0 -36
- package/src/components/shared/Input.tsx +0 -36
- package/src/components/storage/FileUploader.tsx +0 -79
- package/src/context/PrimusProvider.tsx +0 -156
- package/src/context/PrimusThemeProvider.tsx +0 -160
- package/src/hooks/useNotifications.ts +0 -58
- package/src/hooks/usePrimusAuth.ts +0 -3
- package/src/hooks/useRealtimeNotifications.ts +0 -114
- package/src/index.ts +0 -42
- package/tailwind.config.js +0 -18
- package/tsconfig.json +0 -28
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const PremiumCalculator: React.FC<{ apiUrl?: string }> = ({
|
|
4
|
-
apiUrl = 'http://localhost:5221'
|
|
5
|
-
}) => {
|
|
6
|
-
const [coverageAmount, setCoverageAmount] = useState(100000);
|
|
7
|
-
const [age, setAge] = useState(35);
|
|
8
|
-
const [riskScore, setRiskScore] = useState(30);
|
|
9
|
-
const [result, setResult] = useState<any>(null);
|
|
10
|
-
const [loading, setLoading] = useState(false);
|
|
11
|
-
|
|
12
|
-
const calculate = async () => {
|
|
13
|
-
setLoading(true);
|
|
14
|
-
try {
|
|
15
|
-
const response = await fetch(`${apiUrl}/api/insurance/premium/calculate`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: { 'Content-Type': 'application/json' },
|
|
18
|
-
body: JSON.stringify({ coverageAmount, age, riskScore })
|
|
19
|
-
});
|
|
20
|
-
if (response.ok) {
|
|
21
|
-
const data = await response.json();
|
|
22
|
-
setResult(data);
|
|
23
|
-
}
|
|
24
|
-
} catch (error) {
|
|
25
|
-
console.error('Failed to calculate premium:', error);
|
|
26
|
-
} finally {
|
|
27
|
-
setLoading(false);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
33
|
-
<h3 className="text-lg font-medium text-gray-900 mb-4">Premium Calculator</h3>
|
|
34
|
-
|
|
35
|
-
<div className="space-y-4 mb-6">
|
|
36
|
-
<div>
|
|
37
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
38
|
-
Coverage Amount: ${coverageAmount.toLocaleString()}
|
|
39
|
-
</label>
|
|
40
|
-
<input
|
|
41
|
-
type="range"
|
|
42
|
-
min="10000"
|
|
43
|
-
max="1000000"
|
|
44
|
-
step="10000"
|
|
45
|
-
value={coverageAmount}
|
|
46
|
-
onChange={(e) => setCoverageAmount(Number(e.target.value))}
|
|
47
|
-
className="w-full"
|
|
48
|
-
/>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<div>
|
|
52
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
53
|
-
Age: {age}
|
|
54
|
-
</label>
|
|
55
|
-
<input
|
|
56
|
-
type="range"
|
|
57
|
-
min="18"
|
|
58
|
-
max="80"
|
|
59
|
-
value={age}
|
|
60
|
-
onChange={(e) => setAge(Number(e.target.value))}
|
|
61
|
-
className="w-full"
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div>
|
|
66
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
67
|
-
Risk Score: {riskScore}
|
|
68
|
-
</label>
|
|
69
|
-
<input
|
|
70
|
-
type="range"
|
|
71
|
-
min="0"
|
|
72
|
-
max="100"
|
|
73
|
-
value={riskScore}
|
|
74
|
-
onChange={(e) => setRiskScore(Number(e.target.value))}
|
|
75
|
-
className="w-full"
|
|
76
|
-
/>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<button
|
|
81
|
-
onClick={calculate}
|
|
82
|
-
disabled={loading}
|
|
83
|
-
className="w-full bg-primus-600 text-white py-2 px-4 rounded hover:bg-primus-700 disabled:opacity-50"
|
|
84
|
-
>
|
|
85
|
-
{loading ? 'Calculating...' : 'Calculate Premium'}
|
|
86
|
-
</button>
|
|
87
|
-
|
|
88
|
-
{result && (
|
|
89
|
-
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
|
90
|
-
<div className="grid grid-cols-2 gap-4">
|
|
91
|
-
<div>
|
|
92
|
-
<p className="text-sm text-gray-600">Monthly Premium</p>
|
|
93
|
-
<p className="text-2xl font-bold text-gray-900">${result.monthlyPremium}</p>
|
|
94
|
-
</div>
|
|
95
|
-
<div>
|
|
96
|
-
<p className="text-sm text-gray-600">Annual Premium</p>
|
|
97
|
-
<p className="text-2xl font-bold text-gray-900">${result.annualPremium}</p>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
);
|
|
104
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface Quote {
|
|
4
|
-
id: string;
|
|
5
|
-
type: string;
|
|
6
|
-
monthlyPremium: number;
|
|
7
|
-
coverageAmount: number;
|
|
8
|
-
status: string;
|
|
9
|
-
validUntil: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const QuoteComparison: React.FC<{ apiUrl?: string }> = ({
|
|
13
|
-
apiUrl = 'http://localhost:5221'
|
|
14
|
-
}) => {
|
|
15
|
-
const [quotes, setQuotes] = useState<Quote[]>([]);
|
|
16
|
-
const [loading, setLoading] = useState(true);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
fetchQuotes();
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
|
-
const fetchQuotes = async () => {
|
|
23
|
-
try {
|
|
24
|
-
const response = await fetch(`${apiUrl}/api/insurance/quotes`);
|
|
25
|
-
if (response.ok) {
|
|
26
|
-
const data = await response.json();
|
|
27
|
-
setQuotes(data);
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('Failed to fetch quotes:', error);
|
|
31
|
-
} finally {
|
|
32
|
-
setLoading(false);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (loading) return <div className="p-4">Loading quotes...</div>;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
40
|
-
<h3 className="text-lg font-medium text-gray-900 mb-4">Insurance Quotes</h3>
|
|
41
|
-
|
|
42
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
43
|
-
{quotes.map((quote) => (
|
|
44
|
-
<div key={quote.id} className="p-4 border-2 border-gray-200 rounded-lg hover:border-primus-500 transition-colors">
|
|
45
|
-
<div className="flex justify-between items-start mb-3">
|
|
46
|
-
<h4 className="font-medium text-gray-900">{quote.type}</h4>
|
|
47
|
-
<span className={`px-2 py-1 rounded text-xs font-medium ${quote.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'
|
|
48
|
-
}`}>
|
|
49
|
-
{quote.status}
|
|
50
|
-
</span>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div className="mb-3">
|
|
54
|
-
<div className="text-3xl font-bold text-gray-900">
|
|
55
|
-
${quote.monthlyPremium}
|
|
56
|
-
<span className="text-sm font-normal text-gray-500">/mo</span>
|
|
57
|
-
</div>
|
|
58
|
-
<div className="text-sm text-gray-600">
|
|
59
|
-
Coverage: ${quote.coverageAmount.toLocaleString()}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<div className="text-xs text-gray-500">
|
|
64
|
-
Valid until: {new Date(quote.validUntil).toLocaleDateString()}
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<button className="mt-3 w-full px-4 py-2 bg-primus-600 text-white rounded hover:bg-primus-700">
|
|
68
|
-
Accept Quote
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
))}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface PrimusHeaderProps {
|
|
4
|
-
/** Page title */
|
|
5
|
-
title?: string;
|
|
6
|
-
/** Breadcrumb items */
|
|
7
|
-
breadcrumbs?: { label: string; href?: string }[];
|
|
8
|
-
/** Right side actions */
|
|
9
|
-
actions?: React.ReactNode;
|
|
10
|
-
/** User info/avatar */
|
|
11
|
-
user?: {
|
|
12
|
-
name: string;
|
|
13
|
-
email?: string;
|
|
14
|
-
avatar?: string;
|
|
15
|
-
};
|
|
16
|
-
/** On user click */
|
|
17
|
-
onUserClick?: () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const PrimusHeader: React.FC<PrimusHeaderProps> = ({
|
|
21
|
-
title,
|
|
22
|
-
breadcrumbs,
|
|
23
|
-
actions,
|
|
24
|
-
user,
|
|
25
|
-
onUserClick,
|
|
26
|
-
}) => {
|
|
27
|
-
return (
|
|
28
|
-
<div className="flex items-center justify-between w-full">
|
|
29
|
-
{/* Left side */}
|
|
30
|
-
<div className="flex items-center gap-4">
|
|
31
|
-
{breadcrumbs && breadcrumbs.length > 0 && (
|
|
32
|
-
<nav className="flex items-center gap-2 text-sm">
|
|
33
|
-
{breadcrumbs.map((crumb, index) => (
|
|
34
|
-
<React.Fragment key={index}>
|
|
35
|
-
{index > 0 && <span className="text-gray-500">/</span>}
|
|
36
|
-
<span className={index === breadcrumbs.length - 1 ? 'text-white' : 'text-gray-400 hover:text-white cursor-pointer'}>
|
|
37
|
-
{crumb.label}
|
|
38
|
-
</span>
|
|
39
|
-
</React.Fragment>
|
|
40
|
-
))}
|
|
41
|
-
</nav>
|
|
42
|
-
)}
|
|
43
|
-
{title && !breadcrumbs && (
|
|
44
|
-
<h1 className="text-xl font-semibold">{title}</h1>
|
|
45
|
-
)}
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
{/* Right side */}
|
|
49
|
-
<div className="flex items-center gap-4">
|
|
50
|
-
{actions}
|
|
51
|
-
|
|
52
|
-
{user && (
|
|
53
|
-
<button
|
|
54
|
-
onClick={onUserClick}
|
|
55
|
-
className="flex items-center gap-3 px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-colors"
|
|
56
|
-
>
|
|
57
|
-
<div className="text-right hidden sm:block">
|
|
58
|
-
<p className="text-sm font-medium">{user.name}</p>
|
|
59
|
-
{user.email && (
|
|
60
|
-
<p className="text-xs text-gray-400">{user.email}</p>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
{user.avatar ? (
|
|
64
|
-
<img src={user.avatar} alt={user.name} className="w-8 h-8 rounded-full" />
|
|
65
|
-
) : (
|
|
66
|
-
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-sm font-medium">
|
|
67
|
-
{user.name.charAt(0).toUpperCase()}
|
|
68
|
-
</div>
|
|
69
|
-
)}
|
|
70
|
-
</button>
|
|
71
|
-
)}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface PrimusLayoutProps {
|
|
4
|
-
children: React.ReactNode;
|
|
5
|
-
sidebar?: React.ReactNode;
|
|
6
|
-
header?: React.ReactNode;
|
|
7
|
-
/** Dark mode */
|
|
8
|
-
darkMode?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const PrimusLayout: React.FC<PrimusLayoutProps> = ({
|
|
12
|
-
children,
|
|
13
|
-
sidebar,
|
|
14
|
-
header,
|
|
15
|
-
}) => {
|
|
16
|
-
// We rely on the global body background (set in App.css) for the main texture/color.
|
|
17
|
-
// This layout provides structure and glass panels.
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div className={`min-h-screen flex text-white relative`}>
|
|
21
|
-
{/* Sidebar Container */}
|
|
22
|
-
{sidebar && (
|
|
23
|
-
<aside className="w-72 flex-shrink-0 flex flex-col fixed inset-y-0 left-0 z-50">
|
|
24
|
-
{sidebar}
|
|
25
|
-
</aside>
|
|
26
|
-
)}
|
|
27
|
-
|
|
28
|
-
{/* Main Content Area */}
|
|
29
|
-
<div className={`flex-1 flex flex-col min-h-screen ${sidebar ? 'pl-72' : ''}`}>
|
|
30
|
-
|
|
31
|
-
{/* Header - Floating Glass Bar */}
|
|
32
|
-
{header && (
|
|
33
|
-
<header className="h-20 flex items-center px-8 fixed top-0 right-0 left-72 z-40 bg-slate-900/10 backdrop-blur-md border-b border-white/5 transition-all duration-300">
|
|
34
|
-
{header}
|
|
35
|
-
</header>
|
|
36
|
-
)}
|
|
37
|
-
|
|
38
|
-
{/* Content Scrollable Area */}
|
|
39
|
-
<main className="flex-1 p-8 pt-28 overflow-x-hidden">
|
|
40
|
-
<div className="max-w-7xl mx-auto space-y-8 animate-enter">
|
|
41
|
-
{children}
|
|
42
|
-
</div>
|
|
43
|
-
</main>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface SidebarItem {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
icon?: React.ReactNode;
|
|
7
|
-
href?: string;
|
|
8
|
-
onClick?: () => void;
|
|
9
|
-
active?: boolean;
|
|
10
|
-
badge?: string | number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface PrimusSidebarProps {
|
|
14
|
-
/** App logo or title */
|
|
15
|
-
logo?: React.ReactNode;
|
|
16
|
-
/** Navigation items */
|
|
17
|
-
items: SidebarItem[];
|
|
18
|
-
/** Footer content */
|
|
19
|
-
footer?: React.ReactNode;
|
|
20
|
-
/** Callback when item clicked */
|
|
21
|
-
onItemClick?: (item: SidebarItem) => void;
|
|
22
|
-
/** Currently active item id */
|
|
23
|
-
activeId?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const PrimusSidebar: React.FC<PrimusSidebarProps> = ({
|
|
27
|
-
logo,
|
|
28
|
-
items,
|
|
29
|
-
footer,
|
|
30
|
-
onItemClick,
|
|
31
|
-
activeId,
|
|
32
|
-
}) => {
|
|
33
|
-
return (
|
|
34
|
-
<div className="flex flex-col h-full bg-slate-900/50 backdrop-blur-xl border-r border-white/10 relative overflow-hidden">
|
|
35
|
-
{/* Ambient Background Glow */}
|
|
36
|
-
<div className="absolute top-0 left-0 w-full h-64 bg-purple-500/10 blur-[60px] pointer-events-none" />
|
|
37
|
-
|
|
38
|
-
{/* Logo */}
|
|
39
|
-
{logo && (
|
|
40
|
-
<div className="h-20 flex items-center px-6 relative z-10 border-b border-white/5">
|
|
41
|
-
{logo}
|
|
42
|
-
</div>
|
|
43
|
-
)}
|
|
44
|
-
|
|
45
|
-
{/* Navigation */}
|
|
46
|
-
<nav className="flex-1 py-6 px-3 overflow-y-auto relative z-10">
|
|
47
|
-
<ul className="space-y-1">
|
|
48
|
-
{items.map((item) => {
|
|
49
|
-
const isActive = activeId === item.id || item.active;
|
|
50
|
-
return (
|
|
51
|
-
<li key={item.id}>
|
|
52
|
-
<button
|
|
53
|
-
onClick={() => {
|
|
54
|
-
item.onClick?.();
|
|
55
|
-
onItemClick?.(item);
|
|
56
|
-
}}
|
|
57
|
-
className={`group relative w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${isActive
|
|
58
|
-
? 'text-white shadow-lg shadow-purple-500/20 ring-1 ring-white/10'
|
|
59
|
-
: 'text-gray-400 hover:text-white hover:bg-white/5'
|
|
60
|
-
}`}
|
|
61
|
-
>
|
|
62
|
-
{/* Active State Background Gradient */}
|
|
63
|
-
{isActive && (
|
|
64
|
-
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-purple-600/90 to-pink-600/90 opacity-100 transition-opacity" />
|
|
65
|
-
)}
|
|
66
|
-
|
|
67
|
-
{/* Content */}
|
|
68
|
-
<span className="relative z-10 w-5 h-5 flex items-center justify-center transition-transform group-hover:scale-110 duration-300">
|
|
69
|
-
{item.icon}
|
|
70
|
-
</span>
|
|
71
|
-
<span className="relative z-10 flex-1 text-left">{item.label}</span>
|
|
72
|
-
|
|
73
|
-
{/* Badge */}
|
|
74
|
-
{item.badge && (
|
|
75
|
-
<span className={`relative z-10 px-2 py-0.5 text-xs font-semibold rounded-full border ${isActive
|
|
76
|
-
? 'bg-white/20 border-white/20 text-white'
|
|
77
|
-
: 'bg-gray-800 border-gray-700 text-gray-300 group-hover:bg-gray-700'
|
|
78
|
-
}`}>
|
|
79
|
-
{item.badge}
|
|
80
|
-
</span>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
{/* Active Indicator Dot */}
|
|
84
|
-
{isActive && (
|
|
85
|
-
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-white rounded-r-full shadow-[0_0_12px_rgba(255,255,255,0.6)] hidden" />
|
|
86
|
-
)}
|
|
87
|
-
</button>
|
|
88
|
-
</li>
|
|
89
|
-
);
|
|
90
|
-
})}
|
|
91
|
-
</ul>
|
|
92
|
-
</nav>
|
|
93
|
-
|
|
94
|
-
{/* Footer */}
|
|
95
|
-
{footer && (
|
|
96
|
-
<div className="p-4 border-t border-white/5 bg-black/20 backdrop-blur-md relative z-10">
|
|
97
|
-
{footer}
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface LogEntry {
|
|
4
|
-
id: string;
|
|
5
|
-
timestamp: string;
|
|
6
|
-
level: string;
|
|
7
|
-
message: string;
|
|
8
|
-
source: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const LogViewer: React.FC<{ apiUrl?: string }> = ({
|
|
12
|
-
apiUrl = 'http://localhost:5221'
|
|
13
|
-
}) => {
|
|
14
|
-
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
15
|
-
const [filter, setFilter] = useState<string>('');
|
|
16
|
-
const [loading, setLoading] = useState(true);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
fetchLogs();
|
|
20
|
-
const interval = setInterval(fetchLogs, 10000); // Refresh every 10s
|
|
21
|
-
return () => clearInterval(interval);
|
|
22
|
-
}, [filter]);
|
|
23
|
-
|
|
24
|
-
const fetchLogs = async () => {
|
|
25
|
-
try {
|
|
26
|
-
const url = filter
|
|
27
|
-
? `${apiUrl}/api/logs?level=${filter}`
|
|
28
|
-
: `${apiUrl}/api/logs`;
|
|
29
|
-
const response = await fetch(url);
|
|
30
|
-
if (response.ok) {
|
|
31
|
-
const data = await response.json();
|
|
32
|
-
setLogs(data);
|
|
33
|
-
}
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error('Failed to fetch logs:', error);
|
|
36
|
-
} finally {
|
|
37
|
-
setLoading(false);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const getLevelColor = (level: string) => {
|
|
42
|
-
switch (level.toUpperCase()) {
|
|
43
|
-
case 'ERROR': return 'text-red-600 bg-red-50';
|
|
44
|
-
case 'WARNING': return 'text-yellow-600 bg-yellow-50';
|
|
45
|
-
case 'INFO': return 'text-blue-600 bg-blue-50';
|
|
46
|
-
default: return 'text-gray-600 bg-gray-50';
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
52
|
-
<div className="flex justify-between items-center mb-4">
|
|
53
|
-
<h3 className="text-lg font-medium text-gray-900">System Logs</h3>
|
|
54
|
-
<select
|
|
55
|
-
value={filter}
|
|
56
|
-
onChange={(e) => setFilter(e.target.value)}
|
|
57
|
-
className="px-3 py-1 border border-gray-300 rounded-md text-sm"
|
|
58
|
-
>
|
|
59
|
-
<option value="">All Levels</option>
|
|
60
|
-
<option value="INFO">Info</option>
|
|
61
|
-
<option value="WARNING">Warning</option>
|
|
62
|
-
<option value="ERROR">Error</option>
|
|
63
|
-
</select>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
{loading ? (
|
|
67
|
-
<div className="text-center py-8 text-gray-500">Loading logs...</div>
|
|
68
|
-
) : (
|
|
69
|
-
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
70
|
-
{logs.map((log) => (
|
|
71
|
-
<div key={log.id} className="p-3 bg-gray-50 rounded border border-gray-200">
|
|
72
|
-
<div className="flex items-start gap-3">
|
|
73
|
-
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getLevelColor(log.level)}`}>
|
|
74
|
-
{log.level}
|
|
75
|
-
</span>
|
|
76
|
-
<div className="flex-1 min-w-0">
|
|
77
|
-
<p className="text-sm text-gray-900">{log.message}</p>
|
|
78
|
-
<div className="flex gap-4 mt-1 text-xs text-gray-500">
|
|
79
|
-
<span>{new Date(log.timestamp).toLocaleString()}</span>
|
|
80
|
-
<span>{log.source}</span>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
))}
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useNotifications, type Notification } from '../../hooks/useNotifications';
|
|
3
|
-
// import { Button } from '../shared/Button';
|
|
4
|
-
import { BellIcon, CheckCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
|
|
5
|
-
import { Popover, Transition } from '@headlessui/react';
|
|
6
|
-
import { Fragment } from 'react';
|
|
7
|
-
import { clsx } from 'clsx';
|
|
8
|
-
|
|
9
|
-
const NotificationIcon = ({ type }: { type: Notification['type'] }) => {
|
|
10
|
-
switch (type) {
|
|
11
|
-
case 'success': return <CheckCircleIcon className="h-6 w-6 text-green-500" />;
|
|
12
|
-
case 'warning': return <ExclamationTriangleIcon className="h-6 w-6 text-yellow-500" />;
|
|
13
|
-
case 'error': return <XCircleIcon className="h-6 w-6 text-red-500" />;
|
|
14
|
-
default: return <InformationCircleIcon className="h-6 w-6 text-blue-500" />;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const NotificationFeed: React.FC = () => {
|
|
19
|
-
const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications();
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<Popover className="relative">
|
|
23
|
-
{({ open }) => (
|
|
24
|
-
<>
|
|
25
|
-
<Popover.Button className={clsx(
|
|
26
|
-
"relative p-2 rounded-full hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primus-500",
|
|
27
|
-
open && "bg-gray-100"
|
|
28
|
-
)}>
|
|
29
|
-
<BellIcon className="h-6 w-6 text-gray-600" aria-hidden="true" />
|
|
30
|
-
{unreadCount > 0 && (
|
|
31
|
-
<span className="absolute top-1 right-1 block h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-white" />
|
|
32
|
-
)}
|
|
33
|
-
</Popover.Button>
|
|
34
|
-
<Transition
|
|
35
|
-
as={Fragment}
|
|
36
|
-
enter="transition ease-out duration-200"
|
|
37
|
-
enterFrom="opacity-0 translate-y-1"
|
|
38
|
-
enterTo="opacity-100 translate-y-0"
|
|
39
|
-
leave="transition ease-in duration-150"
|
|
40
|
-
leaveFrom="opacity-100 translate-y-0"
|
|
41
|
-
leaveTo="opacity-0 translate-y-1"
|
|
42
|
-
>
|
|
43
|
-
<Popover.Panel className="absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:w-96">
|
|
44
|
-
<div className="px-4 py-3 flex items-center justify-between border-b border-gray-100">
|
|
45
|
-
<h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
|
|
46
|
-
{unreadCount > 0 && (
|
|
47
|
-
<button
|
|
48
|
-
onClick={() => markAllAsRead()}
|
|
49
|
-
className="text-xs font-medium text-primus-600 hover:text-primus-500"
|
|
50
|
-
>
|
|
51
|
-
Mark all as read
|
|
52
|
-
</button>
|
|
53
|
-
)}
|
|
54
|
-
</div>
|
|
55
|
-
<div className="max-h-96 overflow-y-auto">
|
|
56
|
-
{notifications.length === 0 ? (
|
|
57
|
-
<div className="px-4 py-6 text-center text-sm text-gray-500">
|
|
58
|
-
No new notifications
|
|
59
|
-
</div>
|
|
60
|
-
) : (
|
|
61
|
-
<div className="divide-y divide-gray-100">
|
|
62
|
-
{notifications.map((notification) => (
|
|
63
|
-
<div
|
|
64
|
-
key={notification.id}
|
|
65
|
-
className={clsx(
|
|
66
|
-
"flex gap-3 px-4 py-4 hover:bg-gray-50 transition-colors cursor-pointer",
|
|
67
|
-
!notification.read && "bg-blue-50/50"
|
|
68
|
-
)}
|
|
69
|
-
onClick={() => markAsRead(notification.id)}
|
|
70
|
-
>
|
|
71
|
-
<div className="flex-shrink-0 mt-0.5">
|
|
72
|
-
<NotificationIcon type={notification.type} />
|
|
73
|
-
</div>
|
|
74
|
-
<div className="flex-1 min-w-0">
|
|
75
|
-
<p className="text-sm font-medium text-gray-900">
|
|
76
|
-
{notification.title}
|
|
77
|
-
</p>
|
|
78
|
-
<p className="text-sm text-gray-500 truncate">
|
|
79
|
-
{notification.message}
|
|
80
|
-
</p>
|
|
81
|
-
<p className="mt-1 text-xs text-gray-400">
|
|
82
|
-
{new Date(notification.timestamp).toLocaleDateString()}
|
|
83
|
-
</p>
|
|
84
|
-
</div>
|
|
85
|
-
{!notification.read && (
|
|
86
|
-
<div className="flex-shrink-0 self-center">
|
|
87
|
-
<span className="block h-2 w-2 rounded-full bg-primus-500" />
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
))}
|
|
92
|
-
</div>
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
<div className="border-t border-gray-100 px-4 py-2">
|
|
96
|
-
<a href="#" className="block text-center text-sm font-medium text-primus-600 hover:text-primus-500">
|
|
97
|
-
View all notifications
|
|
98
|
-
</a>
|
|
99
|
-
</div>
|
|
100
|
-
</Popover.Panel>
|
|
101
|
-
</Transition>
|
|
102
|
-
</>
|
|
103
|
-
)}
|
|
104
|
-
</Popover>
|
|
105
|
-
);
|
|
106
|
-
};
|