shiny-url-input-box 1.1.0 → 1.1.1
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/ShinyUrlDashboard.d.ts +9 -0
- package/dist/ShinyUrlDashboard.d.ts.map +1 -0
- package/dist/ShinyUrlDashboard.js +111 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +17 -8
- package/src/ShinyUrlDashboard.tsx +261 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface ShinyUrlDashboardProps {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiBaseUrl?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const ShinyUrlDashboard: React.FC<ShinyUrlDashboardProps>;
|
|
9
|
+
//# sourceMappingURL=ShinyUrlDashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShinyUrlDashboard.d.ts","sourceRoot":"","sources":["../src/ShinyUrlDashboard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAsBnD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2BD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA8M9D,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Grid, Paper, Typography, Box, Card, CardContent, CircularProgress } from '@mui/material';
|
|
4
|
+
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts';
|
|
5
|
+
import { getDefaultApiBaseUrl } from './shortener';
|
|
6
|
+
export const ShinyUrlDashboard = ({ apiKey, apiBaseUrl, className = '', title = 'Analytics Dashboard' }) => {
|
|
7
|
+
const [loading, setLoading] = useState(true);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
const [stats, setStats] = useState(null);
|
|
10
|
+
const [clicks, setClicks] = useState(null);
|
|
11
|
+
const [topUrls, setTopUrls] = useState(null);
|
|
12
|
+
const baseUrl = apiBaseUrl || getDefaultApiBaseUrl();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const fetchData = async () => {
|
|
15
|
+
if (!apiKey)
|
|
16
|
+
return;
|
|
17
|
+
setLoading(true);
|
|
18
|
+
setError(null);
|
|
19
|
+
try {
|
|
20
|
+
const headers = {
|
|
21
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
};
|
|
24
|
+
// Try standard X-API-Key as well if Bearer fails?
|
|
25
|
+
// Admin panel uses Bearer for JWT.
|
|
26
|
+
// If user passes an API Key (sk_...), the backend might need 'X-API-Key'.
|
|
27
|
+
// For now, let's assume if it looks like a JWT (3 distinct parts), use Bearer, else X-API-Key?
|
|
28
|
+
// Or simpler: try to fetch with headers that match the backend's expectation for the key type.
|
|
29
|
+
// The prompt says "admin panel components... user can import this dashboard creds".
|
|
30
|
+
// If it's an "Admin Dashboard", it likely needs admin 'Bearer' token.
|
|
31
|
+
// If it's a "User Dashboard" (showing user's URLs), it might use 'X-API-Key'.
|
|
32
|
+
// The endpoints used in Admin Dashboard (/admin/...) are protected by JWT.
|
|
33
|
+
// Endpoints for users might be different or same.
|
|
34
|
+
// I will implement a fallback or accept a 'keyType' prop later if needed.
|
|
35
|
+
// For now, I'll follow the admin pattern (Bearer) as requested by "dashboard creds in other admin panels".
|
|
36
|
+
const fetchOptions = { headers };
|
|
37
|
+
const [statsRes, clicksRes, topUrlsRes] = await Promise.all([
|
|
38
|
+
fetch(`${baseUrl}/api/admin/dashboard/stats`, fetchOptions),
|
|
39
|
+
fetch(`${baseUrl}/api/admin/analytics/clicks?days=7`, fetchOptions),
|
|
40
|
+
fetch(`${baseUrl}/api/admin/analytics/top-urls?limit=10`, fetchOptions)
|
|
41
|
+
]);
|
|
42
|
+
if (!statsRes.ok || !clicksRes.ok || !topUrlsRes.ok) {
|
|
43
|
+
// Check if 401
|
|
44
|
+
if (statsRes.status === 401 || clicksRes.status === 401 || topUrlsRes.status === 401) {
|
|
45
|
+
throw new Error("Unauthorized: Invalid API Key or Token");
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Failed to fetch dashboard data");
|
|
48
|
+
}
|
|
49
|
+
const statsData = await statsRes.json();
|
|
50
|
+
const clicksData = await clicksRes.json();
|
|
51
|
+
const topUrlsData = await topUrlsRes.json();
|
|
52
|
+
setStats(statsData);
|
|
53
|
+
setClicks(clicksData);
|
|
54
|
+
setTopUrls(topUrlsData);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof Error) {
|
|
58
|
+
setError(err.message);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
setError("An unknown error occurred");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
fetchData();
|
|
69
|
+
}, [apiKey, baseUrl]);
|
|
70
|
+
if (!apiKey) {
|
|
71
|
+
return (_jsx(Box, { p: 3, className: className, children: _jsx(Typography, { color: "error", children: "Please provide an API Key / Token to view the dashboard." }) }));
|
|
72
|
+
}
|
|
73
|
+
if (loading) {
|
|
74
|
+
return (_jsx(Box, { display: "flex", justifyContent: "center", alignItems: "center", minHeight: "400px", className: className, children: _jsx(CircularProgress, {}) }));
|
|
75
|
+
}
|
|
76
|
+
if (error) {
|
|
77
|
+
return (_jsx(Box, { p: 3, className: className, children: _jsxs(Typography, { color: "error", children: ["Error: ", error] }) }));
|
|
78
|
+
}
|
|
79
|
+
const statCards = [
|
|
80
|
+
{
|
|
81
|
+
title: 'Total URLs',
|
|
82
|
+
value: stats?.stats?.totalUrls || 0,
|
|
83
|
+
color: '#1976d2',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: 'Total Clicks',
|
|
87
|
+
value: stats?.stats?.totalClicks || 0,
|
|
88
|
+
color: '#2e7d32',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
title: 'Unique IPs',
|
|
92
|
+
value: stats?.stats?.uniqueIps || 0,
|
|
93
|
+
color: '#ed6c02',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: 'Clicks Today',
|
|
97
|
+
value: stats?.stats?.clicksToday || 0,
|
|
98
|
+
color: '#d32f2f',
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
const chartData = clicks?.data?.map((item) => ({
|
|
102
|
+
date: item._id,
|
|
103
|
+
clicks: item.count,
|
|
104
|
+
})) || [];
|
|
105
|
+
return (_jsxs(Box, { className: className, children: [_jsx(Typography, { variant: "h4", gutterBottom: true, children: title }), _jsx(Grid, { container: true, spacing: 3, sx: { mb: 3 }, children: statCards.map((card) => (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 3, children: _jsx(Card, { children: _jsxs(CardContent, { children: [_jsx(Typography, { color: "text.secondary", gutterBottom: true, children: card.title }), _jsx(Typography, { variant: "h4", sx: { color: card.color }, children: card.value.toLocaleString() })] }) }) }, card.title))) }), _jsxs(Grid, { container: true, spacing: 3, children: [_jsx(Grid, { item: true, xs: 12, md: 8, children: _jsxs(Paper, { sx: { p: 3 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Clicks Over Time (Last 7 Days)" }), _jsx(ResponsiveContainer, { width: "100%", height: 300, children: _jsxs(LineChart, { data: chartData, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date" }), _jsx(YAxis, {}), _jsx(Tooltip, {}), _jsx(Legend, {}), _jsx(Line, { type: "monotone", dataKey: "clicks", stroke: "#1976d2", strokeWidth: 2 })] }) })] }) }), _jsx(Grid, { item: true, xs: 12, md: 4, children: _jsxs(Paper, { sx: { p: 3 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Top URLs" }), _jsx(Box, { sx: { maxHeight: 300, overflow: 'auto' }, children: topUrls?.data?.map((url, index) => (_jsxs(Box, { sx: {
|
|
106
|
+
p: 1,
|
|
107
|
+
mb: 1,
|
|
108
|
+
bgcolor: 'background.default',
|
|
109
|
+
borderRadius: 1,
|
|
110
|
+
}, children: [_jsxs(Typography, { variant: "body2", noWrap: true, children: [index + 1, ". ", url.originalUrl] }), _jsxs(Typography, { variant: "caption", color: "text.secondary", children: [url.clickCount, " clicks"] })] }, url._id))) })] }) })] })] }));
|
|
111
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as ShinyUrlInput } from './ShinyUrlInput';
|
|
2
2
|
export type { ShinyUrlInputProps } from './ShinyUrlInput';
|
|
3
3
|
export * from './shortener';
|
|
4
|
+
export { ShinyUrlDashboard } from './ShinyUrlDashboard';
|
|
5
|
+
export type { ShinyUrlDashboardProps } from './ShinyUrlDashboard';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiny-url-input-box",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A reusable React component for generating ShinyURLs",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -22,14 +22,23 @@
|
|
|
22
22
|
],
|
|
23
23
|
"author": "",
|
|
24
24
|
"license": "ISC",
|
|
25
|
-
"peerDependencies": {
|
|
26
|
-
"react": "^18.0.0",
|
|
27
|
-
"react-dom": "^18.0.0"
|
|
28
|
-
},
|
|
29
25
|
"devDependencies": {
|
|
30
26
|
"@types/react": "^18.2.45",
|
|
31
27
|
"@types/react-dom": "^18.2.18",
|
|
32
|
-
"typescript": "^5.3.3"
|
|
28
|
+
"typescript": "^5.3.3",
|
|
29
|
+
"@types/recharts": "^1.8.29",
|
|
30
|
+
"@mui/material": "^5.15.2",
|
|
31
|
+
"@emotion/react": "^11.11.3",
|
|
32
|
+
"@emotion/styled": "^11.11.0",
|
|
33
|
+
"@mui/icons-material": "^5.15.2",
|
|
34
|
+
"recharts": "^2.10.3"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^18.0.0",
|
|
38
|
+
"react-dom": "^18.0.0",
|
|
39
|
+
"@mui/material": "^5.0.0",
|
|
40
|
+
"@emotion/react": "^11.0.0",
|
|
41
|
+
"@emotion/styled": "^11.0.0",
|
|
42
|
+
"recharts": "^2.0.0"
|
|
33
43
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
44
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Grid,
|
|
4
|
+
Paper,
|
|
5
|
+
Typography,
|
|
6
|
+
Box,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CircularProgress
|
|
10
|
+
} from '@mui/material';
|
|
11
|
+
import {
|
|
12
|
+
LineChart,
|
|
13
|
+
Line,
|
|
14
|
+
XAxis,
|
|
15
|
+
YAxis,
|
|
16
|
+
CartesianGrid,
|
|
17
|
+
Tooltip,
|
|
18
|
+
Legend,
|
|
19
|
+
ResponsiveContainer,
|
|
20
|
+
} from 'recharts';
|
|
21
|
+
import { getDefaultApiBaseUrl } from './shortener';
|
|
22
|
+
|
|
23
|
+
export interface ShinyUrlDashboardProps {
|
|
24
|
+
apiKey: string;
|
|
25
|
+
apiBaseUrl?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StatsData {
|
|
31
|
+
stats: {
|
|
32
|
+
totalUrls: number;
|
|
33
|
+
totalClicks: number;
|
|
34
|
+
uniqueIps: number;
|
|
35
|
+
clicksToday: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ClickAnalyticsData {
|
|
40
|
+
data: Array<{
|
|
41
|
+
_id: string; // date
|
|
42
|
+
count: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface TopUrlData {
|
|
47
|
+
data: Array<{
|
|
48
|
+
_id: string;
|
|
49
|
+
originalUrl: string;
|
|
50
|
+
shortCode: string;
|
|
51
|
+
clickCount: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const ShinyUrlDashboard: React.FC<ShinyUrlDashboardProps> = ({
|
|
56
|
+
apiKey,
|
|
57
|
+
apiBaseUrl,
|
|
58
|
+
className = '',
|
|
59
|
+
title = 'Analytics Dashboard'
|
|
60
|
+
}) => {
|
|
61
|
+
const [loading, setLoading] = useState(true);
|
|
62
|
+
const [error, setError] = useState<string | null>(null);
|
|
63
|
+
const [stats, setStats] = useState<StatsData | null>(null);
|
|
64
|
+
const [clicks, setClicks] = useState<ClickAnalyticsData | null>(null);
|
|
65
|
+
const [topUrls, setTopUrls] = useState<TopUrlData | null>(null);
|
|
66
|
+
|
|
67
|
+
const baseUrl = apiBaseUrl || getDefaultApiBaseUrl();
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const fetchData = async () => {
|
|
71
|
+
if (!apiKey) return;
|
|
72
|
+
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const headers = {
|
|
78
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
79
|
+
'Content-Type': 'application/json'
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Try standard X-API-Key as well if Bearer fails?
|
|
83
|
+
// Admin panel uses Bearer for JWT.
|
|
84
|
+
// If user passes an API Key (sk_...), the backend might need 'X-API-Key'.
|
|
85
|
+
// For now, let's assume if it looks like a JWT (3 distinct parts), use Bearer, else X-API-Key?
|
|
86
|
+
// Or simpler: try to fetch with headers that match the backend's expectation for the key type.
|
|
87
|
+
// The prompt says "admin panel components... user can import this dashboard creds".
|
|
88
|
+
// If it's an "Admin Dashboard", it likely needs admin 'Bearer' token.
|
|
89
|
+
// If it's a "User Dashboard" (showing user's URLs), it might use 'X-API-Key'.
|
|
90
|
+
// The endpoints used in Admin Dashboard (/admin/...) are protected by JWT.
|
|
91
|
+
// Endpoints for users might be different or same.
|
|
92
|
+
// I will implement a fallback or accept a 'keyType' prop later if needed.
|
|
93
|
+
// For now, I'll follow the admin pattern (Bearer) as requested by "dashboard creds in other admin panels".
|
|
94
|
+
|
|
95
|
+
const fetchOptions = { headers };
|
|
96
|
+
|
|
97
|
+
const [statsRes, clicksRes, topUrlsRes] = await Promise.all([
|
|
98
|
+
fetch(`${baseUrl}/api/admin/dashboard/stats`, fetchOptions),
|
|
99
|
+
fetch(`${baseUrl}/api/admin/analytics/clicks?days=7`, fetchOptions),
|
|
100
|
+
fetch(`${baseUrl}/api/admin/analytics/top-urls?limit=10`, fetchOptions)
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
if (!statsRes.ok || !clicksRes.ok || !topUrlsRes.ok) {
|
|
104
|
+
// Check if 401
|
|
105
|
+
if (statsRes.status === 401 || clicksRes.status === 401 || topUrlsRes.status === 401) {
|
|
106
|
+
throw new Error("Unauthorized: Invalid API Key or Token");
|
|
107
|
+
}
|
|
108
|
+
throw new Error("Failed to fetch dashboard data");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const statsData = await statsRes.json();
|
|
112
|
+
const clicksData = await clicksRes.json();
|
|
113
|
+
const topUrlsData = await topUrlsRes.json();
|
|
114
|
+
|
|
115
|
+
setStats(statsData);
|
|
116
|
+
setClicks(clicksData);
|
|
117
|
+
setTopUrls(topUrlsData);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err instanceof Error) {
|
|
120
|
+
setError(err.message);
|
|
121
|
+
} else {
|
|
122
|
+
setError("An unknown error occurred");
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
setLoading(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
fetchData();
|
|
130
|
+
}, [apiKey, baseUrl]);
|
|
131
|
+
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
return (
|
|
134
|
+
<Box p={3} className={className}>
|
|
135
|
+
<Typography color="error">Please provide an API Key / Token to view the dashboard.</Typography>
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (loading) {
|
|
141
|
+
return (
|
|
142
|
+
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px" className={className}>
|
|
143
|
+
<CircularProgress />
|
|
144
|
+
</Box>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (error) {
|
|
149
|
+
return (
|
|
150
|
+
<Box p={3} className={className}>
|
|
151
|
+
<Typography color="error">Error: {error}</Typography>
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const statCards = [
|
|
157
|
+
{
|
|
158
|
+
title: 'Total URLs',
|
|
159
|
+
value: stats?.stats?.totalUrls || 0,
|
|
160
|
+
color: '#1976d2',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
title: 'Total Clicks',
|
|
164
|
+
value: stats?.stats?.totalClicks || 0,
|
|
165
|
+
color: '#2e7d32',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
title: 'Unique IPs',
|
|
169
|
+
value: stats?.stats?.uniqueIps || 0,
|
|
170
|
+
color: '#ed6c02',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
title: 'Clicks Today',
|
|
174
|
+
value: stats?.stats?.clicksToday || 0,
|
|
175
|
+
color: '#d32f2f',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const chartData = clicks?.data?.map((item: any) => ({
|
|
180
|
+
date: item._id,
|
|
181
|
+
clicks: item.count,
|
|
182
|
+
})) || [];
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Box className={className}>
|
|
186
|
+
<Typography variant="h4" gutterBottom>
|
|
187
|
+
{title}
|
|
188
|
+
</Typography>
|
|
189
|
+
|
|
190
|
+
<Grid container spacing={3} sx={{ mb: 3 }}>
|
|
191
|
+
{statCards.map((card) => (
|
|
192
|
+
<Grid item xs={12} sm={6} md={3} key={card.title}>
|
|
193
|
+
<Card>
|
|
194
|
+
<CardContent>
|
|
195
|
+
<Typography color="text.secondary" gutterBottom>
|
|
196
|
+
{card.title}
|
|
197
|
+
</Typography>
|
|
198
|
+
<Typography variant="h4" sx={{ color: card.color }}>
|
|
199
|
+
{card.value.toLocaleString()}
|
|
200
|
+
</Typography>
|
|
201
|
+
</CardContent>
|
|
202
|
+
</Card>
|
|
203
|
+
</Grid>
|
|
204
|
+
))}
|
|
205
|
+
</Grid>
|
|
206
|
+
|
|
207
|
+
<Grid container spacing={3}>
|
|
208
|
+
<Grid item xs={12} md={8}>
|
|
209
|
+
<Paper sx={{ p: 3 }}>
|
|
210
|
+
<Typography variant="h6" gutterBottom>
|
|
211
|
+
Clicks Over Time (Last 7 Days)
|
|
212
|
+
</Typography>
|
|
213
|
+
<ResponsiveContainer width="100%" height={300}>
|
|
214
|
+
<LineChart data={chartData}>
|
|
215
|
+
<CartesianGrid strokeDasharray="3 3" />
|
|
216
|
+
<XAxis dataKey="date" />
|
|
217
|
+
<YAxis />
|
|
218
|
+
<Tooltip />
|
|
219
|
+
<Legend />
|
|
220
|
+
<Line
|
|
221
|
+
type="monotone"
|
|
222
|
+
dataKey="clicks"
|
|
223
|
+
stroke="#1976d2"
|
|
224
|
+
strokeWidth={2}
|
|
225
|
+
/>
|
|
226
|
+
</LineChart>
|
|
227
|
+
</ResponsiveContainer>
|
|
228
|
+
</Paper>
|
|
229
|
+
</Grid>
|
|
230
|
+
|
|
231
|
+
<Grid item xs={12} md={4}>
|
|
232
|
+
<Paper sx={{ p: 3 }}>
|
|
233
|
+
<Typography variant="h6" gutterBottom>
|
|
234
|
+
Top URLs
|
|
235
|
+
</Typography>
|
|
236
|
+
<Box sx={{ maxHeight: 300, overflow: 'auto' }}>
|
|
237
|
+
{topUrls?.data?.map((url: any, index: number) => (
|
|
238
|
+
<Box
|
|
239
|
+
key={url._id}
|
|
240
|
+
sx={{
|
|
241
|
+
p: 1,
|
|
242
|
+
mb: 1,
|
|
243
|
+
bgcolor: 'background.default',
|
|
244
|
+
borderRadius: 1,
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<Typography variant="body2" noWrap>
|
|
248
|
+
{index + 1}. {url.originalUrl}
|
|
249
|
+
</Typography>
|
|
250
|
+
<Typography variant="caption" color="text.secondary">
|
|
251
|
+
{url.clickCount} clicks
|
|
252
|
+
</Typography>
|
|
253
|
+
</Box>
|
|
254
|
+
))}
|
|
255
|
+
</Box>
|
|
256
|
+
</Paper>
|
|
257
|
+
</Grid>
|
|
258
|
+
</Grid>
|
|
259
|
+
</Box>
|
|
260
|
+
);
|
|
261
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as ShinyUrlInput } from './ShinyUrlInput';
|
|
2
2
|
export type { ShinyUrlInputProps } from './ShinyUrlInput';
|
|
3
3
|
export * from './shortener';
|
|
4
|
+
export { ShinyUrlDashboard } from './ShinyUrlDashboard';
|
|
5
|
+
export type { ShinyUrlDashboardProps } from './ShinyUrlDashboard';
|
|
4
6
|
|