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.
@@ -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
@@ -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
@@ -1,2 +1,3 @@
1
1
  export { default as ShinyUrlInput } from './ShinyUrlInput';
2
2
  export * from './shortener';
3
+ export { ShinyUrlDashboard } from './ShinyUrlDashboard';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiny-url-input-box",
3
- "version": "1.1.0",
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