shiny-url-input-box 1.1.0 → 1.3.0
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/ShinyUrlApiKeys.d.ts +8 -0
- package/dist/ShinyUrlApiKeys.d.ts.map +1 -0
- package/dist/ShinyUrlApiKeys.js +111 -0
- package/dist/ShinyUrlDashboard.d.ts +9 -0
- package/dist/ShinyUrlDashboard.d.ts.map +1 -0
- package/dist/ShinyUrlDashboard.js +111 -0
- package/dist/ShinyUrlDetailsModal.d.ts +10 -0
- package/dist/ShinyUrlDetailsModal.d.ts.map +1 -0
- package/dist/ShinyUrlDetailsModal.js +83 -0
- package/dist/ShinyUrlUrls.d.ts +8 -0
- package/dist/ShinyUrlUrls.d.ts.map +1 -0
- package/dist/ShinyUrlUrls.js +155 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/package.json +20 -8
- package/src/ShinyUrlApiKeys.tsx +305 -0
- package/src/ShinyUrlDashboard.tsx +261 -0
- package/src/ShinyUrlDetailsModal.tsx +397 -0
- package/src/ShinyUrlUrls.tsx +239 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShinyUrlApiKeys.d.ts","sourceRoot":"","sources":["../src/ShinyUrlApiKeys.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AA2BhE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAsQ1D,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Box, Paper, Typography, Button, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, TextField, Dialog, DialogTitle, DialogContent, DialogActions, Alert, Chip, Snackbar, CircularProgress } from '@mui/material';
|
|
4
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
5
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
6
|
+
import { getDefaultApiBaseUrl } from './shortener';
|
|
7
|
+
export const ShinyUrlApiKeys = ({ apiKey, apiBaseUrl, className = '' }) => {
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const [keys, setKeys] = useState([]);
|
|
10
|
+
const [openDialog, setOpenDialog] = useState(false);
|
|
11
|
+
const [newKeyName, setNewKeyName] = useState('');
|
|
12
|
+
const [createdKey, setCreatedKey] = useState(null);
|
|
13
|
+
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
14
|
+
const [snackbarMessage, setSnackbarMessage] = useState('');
|
|
15
|
+
const [creating, setCreating] = useState(false);
|
|
16
|
+
const baseUrl = apiBaseUrl || getDefaultApiBaseUrl();
|
|
17
|
+
const fetchKeys = useCallback(async () => {
|
|
18
|
+
if (!apiKey)
|
|
19
|
+
return;
|
|
20
|
+
setLoading(true);
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${baseUrl}/api/admin/api-keys`, {
|
|
23
|
+
headers: {
|
|
24
|
+
'Authorization': `Bearer ${apiKey}`
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if (response.ok) {
|
|
28
|
+
const result = await response.json();
|
|
29
|
+
setKeys(result.data || []);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error('Failed to fetch keys', error);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, [apiKey, baseUrl]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
fetchKeys();
|
|
41
|
+
}, [fetchKeys]);
|
|
42
|
+
const handleCreate = async () => {
|
|
43
|
+
setCreating(true);
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(`${baseUrl}/api/admin/api-keys`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
49
|
+
'Content-Type': 'application/json'
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ name: newKeyName || 'Default Key' })
|
|
52
|
+
});
|
|
53
|
+
const result = await response.json();
|
|
54
|
+
if (result.success && result.data) {
|
|
55
|
+
setCreatedKey(result.data.key);
|
|
56
|
+
setOpenDialog(false);
|
|
57
|
+
setNewKeyName('');
|
|
58
|
+
setSnackbarMessage('API key created successfully! Copy it now - you won\'t be able to see it again.');
|
|
59
|
+
setSnackbarOpen(true);
|
|
60
|
+
fetchKeys(); // Refresh list
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
setSnackbarMessage('Failed to create key');
|
|
65
|
+
setSnackbarOpen(true);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
setCreating(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const handleToggle = async (id, currentStatus) => {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`${baseUrl}/api/admin/api-keys/${id}`, {
|
|
74
|
+
method: 'PATCH',
|
|
75
|
+
headers: {
|
|
76
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
77
|
+
'Content-Type': 'application/json'
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ isActive: !currentStatus })
|
|
80
|
+
});
|
|
81
|
+
if (response.ok) {
|
|
82
|
+
setSnackbarMessage('API key updated successfully');
|
|
83
|
+
setSnackbarOpen(true);
|
|
84
|
+
fetchKeys(); // Refresh list
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
setSnackbarMessage('Failed to update key');
|
|
89
|
+
setSnackbarOpen(true);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const handleCopy = (text) => {
|
|
93
|
+
navigator.clipboard.writeText(text);
|
|
94
|
+
setSnackbarMessage('Copied to clipboard!');
|
|
95
|
+
setSnackbarOpen(true);
|
|
96
|
+
};
|
|
97
|
+
const formatKey = (key) => {
|
|
98
|
+
if (key.length > 20) {
|
|
99
|
+
return key.substring(0, 10) + '...' + key.substring(key.length - 4);
|
|
100
|
+
}
|
|
101
|
+
return key;
|
|
102
|
+
};
|
|
103
|
+
if (!apiKey) {
|
|
104
|
+
return _jsx(Typography, { color: "error", children: "Authentication required" });
|
|
105
|
+
}
|
|
106
|
+
return (_jsxs(Box, { className: className, children: [_jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }, children: [_jsx(Typography, { variant: "h4", children: "API Keys" }), _jsx(Button, { variant: "contained", startIcon: _jsx(AddIcon, {}), onClick: () => setOpenDialog(true), children: "Create New Key" })] }), _jsx(Paper, { children: _jsx(TableContainer, { children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: "Name" }), _jsx(TableCell, { children: "Key" }), _jsx(TableCell, { children: "Status" }), _jsx(TableCell, { children: "Created" }), _jsx(TableCell, { children: "Last Used" }), _jsx(TableCell, { children: "Actions" })] }) }), _jsx(TableBody, { children: loading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, align: "center", children: _jsx(CircularProgress, { size: 24 }) }) })) : keys.length > 0 ? (keys.map((key) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: key.name || 'Default Key' }), _jsx(TableCell, { children: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "body2", sx: { fontFamily: 'monospace' }, children: formatKey(key.key) }), _jsx(IconButton, { size: "small", onClick: () => handleCopy(key.key), title: "Copy full key", children: _jsx(ContentCopyIcon, { fontSize: "small" }) })] }) }), _jsx(TableCell, { children: _jsx(Chip, { label: key.isActive ? 'Active' : 'Disabled', color: key.isActive ? 'success' : 'default', size: "small" }) }), _jsx(TableCell, { children: new Date(key.createdAt).toLocaleDateString() }), _jsx(TableCell, { children: key.lastUsedAt
|
|
107
|
+
? new Date(key.lastUsedAt).toLocaleDateString()
|
|
108
|
+
: 'Never' }), _jsx(TableCell, { children: _jsx(Button, { size: "small", variant: "outlined", color: key.isActive ? 'error' : 'success', onClick: () => handleToggle(key._id, key.isActive), children: key.isActive ? 'Disable' : 'Enable' }) })] }, key._id)))) : (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, align: "center", children: "No API keys found. Create your first key to start using the service." }) })) })] }) }) }), _jsxs(Dialog, { open: openDialog, onClose: () => setOpenDialog(false), children: [_jsx(DialogTitle, { children: "Create New API Key" }), _jsx(DialogContent, { children: _jsx(TextField, { autoFocus: true, margin: "dense", label: "Key Name (optional)", fullWidth: true, variant: "outlined", value: newKeyName, onChange: (e) => setNewKeyName(e.target.value), placeholder: "e.g., Production Key, Development Key", sx: { mt: 2 } }) }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setOpenDialog(false), children: "Cancel" }), _jsx(Button, { onClick: handleCreate, variant: "contained", disabled: creating, children: creating ? 'Creating...' : 'Create' })] })] }), _jsxs(Dialog, { open: !!createdKey, onClose: () => setCreatedKey(null), children: [_jsx(DialogTitle, { children: "API Key Created" }), _jsxs(DialogContent, { children: [_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: "Copy this key now! You won't be able to see it again." }), _jsx(TextField, { fullWidth: true, value: createdKey || '', InputProps: {
|
|
109
|
+
readOnly: true,
|
|
110
|
+
}, sx: { mb: 2 } }), _jsx(Button, { fullWidth: true, variant: "contained", startIcon: _jsx(ContentCopyIcon, {}), onClick: () => createdKey && handleCopy(createdKey), children: "Copy Key" })] }), _jsx(DialogActions, { children: _jsx(Button, { onClick: () => setCreatedKey(null), children: "Close" }) })] }), _jsx(Snackbar, { open: snackbarOpen, autoHideDuration: 3000, onClose: () => setSnackbarOpen(false), message: snackbarMessage })] }));
|
|
111
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface ShinyUrlDetailsModalProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
urlId: string | null;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
apiBaseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const ShinyUrlDetailsModal: React.FC<ShinyUrlDetailsModalProps>;
|
|
10
|
+
//# sourceMappingURL=ShinyUrlDetailsModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShinyUrlDetailsModal.d.ts","sourceRoot":"","sources":["../src/ShinyUrlDetailsModal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAyCnD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA0BD,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAmUpE,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Box, Typography, Tabs, Tab, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Chip, Grid, Card, CardContent, CircularProgress } from '@mui/material';
|
|
4
|
+
import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts';
|
|
5
|
+
import { getDefaultApiBaseUrl } from './shortener';
|
|
6
|
+
function TabPanel(props) {
|
|
7
|
+
const { children, value, index, ...other } = props;
|
|
8
|
+
return (_jsx("div", { role: "tabpanel", hidden: value !== index, id: `url-tabpanel-${index}`, "aria-labelledby": `url-tab-${index}`, ...other, children: value === index && _jsx(Box, { sx: { p: 3 }, children: children }) }));
|
|
9
|
+
}
|
|
10
|
+
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
|
|
11
|
+
export const ShinyUrlDetailsModal = ({ open, urlId, onClose, apiKey, apiBaseUrl }) => {
|
|
12
|
+
const [tabValue, setTabValue] = useState(0);
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
const [urlData, setUrlData] = useState(null);
|
|
15
|
+
const [clicksData, setClicksData] = useState([]);
|
|
16
|
+
const [analyticsData, setAnalyticsData] = useState(null);
|
|
17
|
+
const baseUrl = apiBaseUrl || getDefaultApiBaseUrl();
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (open && urlId && apiKey) {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
Promise.all([
|
|
22
|
+
fetch(`${baseUrl}/api/urls/${urlId}`, {
|
|
23
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
24
|
+
}).then(res => res.json()),
|
|
25
|
+
fetch(`${baseUrl}/api/analytics/url/${urlId}?limit=1000`, {
|
|
26
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
27
|
+
}).then(res => res.json()), // Clicks
|
|
28
|
+
fetch(`${baseUrl}/api/analytics/url/${urlId}/stats?days=30`, {
|
|
29
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
30
|
+
}).then(res => res.json()) // Analytics
|
|
31
|
+
]).then(([urlRes, clicksRes, analyticsRes]) => {
|
|
32
|
+
if (urlRes.success)
|
|
33
|
+
setUrlData(urlRes.data);
|
|
34
|
+
if (clicksRes.success)
|
|
35
|
+
setClicksData(clicksRes.data || []);
|
|
36
|
+
if (analyticsRes.success)
|
|
37
|
+
setAnalyticsData(analyticsRes.data);
|
|
38
|
+
}).catch(err => {
|
|
39
|
+
console.error("Failed to fetch url details", err);
|
|
40
|
+
}).finally(() => {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}, [open, urlId, apiKey, baseUrl]);
|
|
45
|
+
const handleTabChange = (_event, newValue) => {
|
|
46
|
+
setTabValue(newValue);
|
|
47
|
+
};
|
|
48
|
+
// Prepare chart data
|
|
49
|
+
const clicksOverTime = analyticsData?.clicksOverTime?.map((item) => ({
|
|
50
|
+
date: item._id,
|
|
51
|
+
clicks: item.count,
|
|
52
|
+
})) || [];
|
|
53
|
+
const deviceData = analyticsData?.deviceBreakdown?.map((item) => ({
|
|
54
|
+
name: item._id || 'Unknown',
|
|
55
|
+
value: item.count,
|
|
56
|
+
})) || [];
|
|
57
|
+
const browserData = analyticsData?.browserBreakdown?.slice(0, 5).map((item) => ({
|
|
58
|
+
name: item._id || 'Unknown',
|
|
59
|
+
clicks: item.count,
|
|
60
|
+
})) || [];
|
|
61
|
+
const osData = analyticsData?.osBreakdown?.slice(0, 5).map((item) => ({
|
|
62
|
+
name: item._id || 'Unknown',
|
|
63
|
+
clicks: item.count,
|
|
64
|
+
})) || [];
|
|
65
|
+
return (_jsxs(Dialog, { open: open, onClose: onClose, maxWidth: "lg", fullWidth: true, children: [_jsx(DialogTitle, { children: _jsxs(Box, { children: [_jsx(Typography, { variant: "h6", children: "URL Details & Analytics" }), urlData && (_jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mt: 1 }, children: [_jsx("strong", { children: "Short Code:" }), ' ', _jsx(Typography, { component: "a", href: `${baseUrl}/${urlData.shortCode}`, target: "_blank", rel: "noopener", sx: {
|
|
66
|
+
color: 'primary.main',
|
|
67
|
+
textDecoration: 'none',
|
|
68
|
+
fontFamily: 'monospace',
|
|
69
|
+
'&:hover': {
|
|
70
|
+
textDecoration: 'underline',
|
|
71
|
+
},
|
|
72
|
+
}, children: urlData.shortCode }), ' ', "| ", _jsx("strong", { children: "Total Clicks:" }), " ", urlData.clicks || 0] }))] }) }), _jsx(DialogContent, { children: loading ? (_jsxs(Box, { sx: { p: 3, textAlign: 'center' }, children: [_jsx(CircularProgress, {}), _jsx(Typography, { sx: { mt: 2 }, children: "Loading..." })] })) : (_jsxs(_Fragment, { children: [urlData && (_jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: _jsx("strong", { children: "Original URL:" }) }), _jsx(Typography, { variant: "body2", sx: {
|
|
73
|
+
wordBreak: 'break-all',
|
|
74
|
+
bgcolor: 'action.hover',
|
|
75
|
+
p: 1,
|
|
76
|
+
borderRadius: 1,
|
|
77
|
+
}, children: urlData.originalUrl })] })), _jsx(Box, { sx: { borderBottom: 1, borderColor: 'divider', mb: 2 }, children: _jsxs(Tabs, { value: tabValue, onChange: handleTabChange, children: [_jsx(Tab, { label: "Analytics" }), _jsx(Tab, { label: "Click History" })] }) }), _jsx(TabPanel, { value: tabValue, index: 0, children: analyticsData ? (_jsxs(Grid, { container: true, spacing: 3, children: [_jsx(Grid, { item: true, xs: 12, md: 6, children: _jsx(Card, { children: _jsxs(CardContent, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Total Clicks" }), _jsx(Typography, { variant: "h3", color: "primary", children: analyticsData.totalClicks })] }) }) }), _jsx(Grid, { item: true, xs: 12, md: 6, children: _jsx(Card, { children: _jsxs(CardContent, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Unique Users" }), _jsx(Typography, { variant: "h3", color: "secondary", children: analyticsData.uniqueUsers })] }) }) }), _jsx(Grid, { item: true, xs: 12, children: _jsxs(Paper, { sx: { p: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Clicks Over Time (Last 30 Days)" }), _jsx(ResponsiveContainer, { width: "100%", height: 300, children: _jsxs(LineChart, { data: clicksOverTime, 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: 6, children: _jsxs(Paper, { sx: { p: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Device Breakdown" }), _jsx(ResponsiveContainer, { width: "100%", height: 250, children: _jsxs(PieChart, { children: [_jsx(Pie, { data: deviceData, cx: "50%", cy: "50%", labelLine: false, label: ({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`, outerRadius: 80, fill: "#8884d8", dataKey: "value", children: deviceData.map((_entry, index) => (_jsx(Cell, { fill: COLORS[index % COLORS.length] }, `cell-${index}`))) }), _jsx(Tooltip, {})] }) })] }) }), _jsx(Grid, { item: true, xs: 12, md: 6, children: _jsxs(Paper, { sx: { p: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Top Browsers" }), _jsx(ResponsiveContainer, { width: "100%", height: 250, children: _jsxs(BarChart, { data: browserData, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "name" }), _jsx(YAxis, {}), _jsx(Tooltip, {}), _jsx(Bar, { dataKey: "clicks", fill: "#1976d2" })] }) })] }) }), _jsx(Grid, { item: true, xs: 12, children: _jsxs(Paper, { sx: { p: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Top Operating Systems" }), _jsx(ResponsiveContainer, { width: "100%", height: 250, children: _jsxs(BarChart, { data: osData, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "name" }), _jsx(YAxis, {}), _jsx(Tooltip, {}), _jsx(Bar, { dataKey: "clicks", fill: "#00C49F" })] }) })] }) })] })) : (_jsx(Typography, { children: "No analytics data available" })) }), _jsx(TabPanel, { value: tabValue, index: 1, children: _jsx(TableContainer, { component: Paper, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: "Date & Time" }), _jsx(TableCell, { children: "IP Address" }), _jsx(TableCell, { children: "Device" }), _jsx(TableCell, { children: "Browser" }), _jsx(TableCell, { children: "OS" }), _jsx(TableCell, { children: "Referer" })] }) }), _jsx(TableBody, { children: clicksData.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, align: "center", children: "No clicks recorded yet" }) })) : (clicksData.map((click, index) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: new Date(click.clickedAt).toLocaleString() }), _jsx(TableCell, { children: _jsx(Chip, { label: click.ipAddress, size: "small" }) }), _jsx(TableCell, { children: _jsx(Chip, { label: click.device || 'Unknown', size: "small", color: "primary", variant: "outlined" }) }), _jsx(TableCell, { children: click.browser || 'Unknown' }), _jsx(TableCell, { children: click.os || 'Unknown' }), _jsx(TableCell, { children: click.referer ? (_jsx(Typography, { variant: "body2", sx: {
|
|
78
|
+
maxWidth: 200,
|
|
79
|
+
overflow: 'hidden',
|
|
80
|
+
textOverflow: 'ellipsis',
|
|
81
|
+
whiteSpace: 'nowrap',
|
|
82
|
+
}, title: click.referer, children: click.referer })) : (_jsx(Typography, { variant: "body2", color: "text.secondary", children: "Direct" })) })] }, index)))) })] }) }) })] })) }), _jsx(DialogActions, { children: _jsx(Button, { onClick: onClose, children: "Close" }) })] }));
|
|
83
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShinyUrlUrls.d.ts","sourceRoot":"","sources":["../src/ShinyUrlUrls.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAoBhE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAoNpD,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Box, Typography, TextField, Paper } from '@mui/material';
|
|
4
|
+
import { DataGrid, GridActionsCellItem, GridToolbar } from '@mui/x-data-grid';
|
|
5
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
6
|
+
import { ShinyUrlDetailsModal } from './ShinyUrlDetailsModal';
|
|
7
|
+
import { getDefaultApiBaseUrl } from './shortener';
|
|
8
|
+
export const ShinyUrlUrls = ({ apiKey, apiBaseUrl, className = '' }) => {
|
|
9
|
+
const [page, setPage] = useState(0);
|
|
10
|
+
const [pageSize, setPageSize] = useState(10);
|
|
11
|
+
const [search, setSearch] = useState('');
|
|
12
|
+
const [selectedUrlId, setSelectedUrlId] = useState(null);
|
|
13
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const [rows, setRows] = useState([]);
|
|
16
|
+
const [total, setTotal] = useState(0);
|
|
17
|
+
const baseUrl = apiBaseUrl || getDefaultApiBaseUrl();
|
|
18
|
+
const fetchUrls = useCallback(async () => {
|
|
19
|
+
if (!apiKey)
|
|
20
|
+
return;
|
|
21
|
+
setLoading(true);
|
|
22
|
+
try {
|
|
23
|
+
const queryParams = new URLSearchParams({
|
|
24
|
+
page: (page + 1).toString(),
|
|
25
|
+
limit: pageSize.toString(),
|
|
26
|
+
search: search
|
|
27
|
+
});
|
|
28
|
+
const response = await fetch(`${baseUrl}/api/admin/urls?${queryParams}`, {
|
|
29
|
+
headers: {
|
|
30
|
+
'Authorization': `Bearer ${apiKey}`
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const result = await response.json();
|
|
34
|
+
if (result.success) {
|
|
35
|
+
setRows(result.data || []);
|
|
36
|
+
setTotal(result.pagination?.total || 0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('Failed to fetch URLs', error);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}, [apiKey, baseUrl, page, pageSize, search]);
|
|
46
|
+
// Debounce search
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
fetchUrls();
|
|
50
|
+
}, 500);
|
|
51
|
+
return () => clearTimeout(timer);
|
|
52
|
+
}, [fetchUrls]);
|
|
53
|
+
const handleDelete = async (id) => {
|
|
54
|
+
if (window.confirm('Are you sure you want to delete this URL?')) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`${baseUrl}/api/urls/${id}`, {
|
|
57
|
+
method: 'DELETE',
|
|
58
|
+
headers: {
|
|
59
|
+
'Authorization': `Bearer ${apiKey}`
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
fetchUrls(); // Refresh
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error('Failed to delete URL', error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const handleRowClick = (params) => {
|
|
72
|
+
setSelectedUrlId(params.id);
|
|
73
|
+
setModalOpen(true);
|
|
74
|
+
};
|
|
75
|
+
const handleCloseModal = () => {
|
|
76
|
+
setModalOpen(false);
|
|
77
|
+
setSelectedUrlId(null);
|
|
78
|
+
};
|
|
79
|
+
const columns = [
|
|
80
|
+
{
|
|
81
|
+
field: 'shortCode',
|
|
82
|
+
headerName: 'Short Code',
|
|
83
|
+
width: 150,
|
|
84
|
+
renderCell: (params) => {
|
|
85
|
+
// Add referer as query parameter as fallback if browser doesn't send referer header
|
|
86
|
+
// For component usage, we might not know the exact "admin panel url", but we can use window.location
|
|
87
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
88
|
+
const shortUrl = `${baseUrl}/${params.value}?ref=${encodeURIComponent(currentUrl)}`;
|
|
89
|
+
return (_jsx(Typography, { component: "a", href: shortUrl, target: "_blank", rel: "noopener", variant: "body2", sx: {
|
|
90
|
+
fontFamily: 'monospace',
|
|
91
|
+
bgcolor: 'action.hover',
|
|
92
|
+
px: 1,
|
|
93
|
+
py: 0.5,
|
|
94
|
+
borderRadius: 1,
|
|
95
|
+
color: 'primary.main',
|
|
96
|
+
textDecoration: 'none',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
'&:hover': {
|
|
99
|
+
textDecoration: 'underline',
|
|
100
|
+
bgcolor: 'action.selected',
|
|
101
|
+
},
|
|
102
|
+
}, onClick: (e) => {
|
|
103
|
+
e.stopPropagation(); // Prevent row click when clicking the link
|
|
104
|
+
}, children: params.value }));
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
field: 'originalUrl',
|
|
109
|
+
headerName: 'Original URL',
|
|
110
|
+
flex: 1,
|
|
111
|
+
minWidth: 300,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
field: 'clicks',
|
|
115
|
+
headerName: 'Clicks',
|
|
116
|
+
width: 100,
|
|
117
|
+
type: 'number',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
field: 'createdAt',
|
|
121
|
+
headerName: 'Created At',
|
|
122
|
+
width: 180,
|
|
123
|
+
valueFormatter: (params) => {
|
|
124
|
+
if (!params.value)
|
|
125
|
+
return '';
|
|
126
|
+
return new Date(params.value).toLocaleString();
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
field: 'actions',
|
|
131
|
+
type: 'actions',
|
|
132
|
+
headerName: 'Actions',
|
|
133
|
+
width: 100,
|
|
134
|
+
getActions: (params) => [
|
|
135
|
+
_jsx(GridActionsCellItem, { icon: _jsx(DeleteIcon, {}), label: "Delete", onClick: () => handleDelete(params.id), color: "error" }),
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
if (!apiKey) {
|
|
140
|
+
return _jsx(Typography, { color: "error", children: "Authentication required" });
|
|
141
|
+
}
|
|
142
|
+
return (_jsxs(Box, { className: className, children: [_jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', mb: 3 }, children: [_jsx(Typography, { variant: "h4", children: "URLs Management" }), _jsx(Box, { sx: { display: 'flex', gap: 2 }, children: _jsx(TextField, { size: "small", placeholder: "Search URLs...", value: search, onChange: (e) => {
|
|
143
|
+
setSearch(e.target.value);
|
|
144
|
+
setPage(0); // Reset page on search
|
|
145
|
+
}, sx: { width: 300 } }) })] }), _jsx(Paper, { sx: { height: 600, width: '100%' }, children: _jsx(DataGrid, { rows: rows, columns: columns, loading: loading, pagination: true, paginationModel: { page, pageSize }, onPaginationModelChange: (model) => {
|
|
146
|
+
setPage(model.page);
|
|
147
|
+
setPageSize(model.pageSize);
|
|
148
|
+
}, paginationMode: "server", rowCount: total, slots: {
|
|
149
|
+
toolbar: GridToolbar,
|
|
150
|
+
}, getRowId: (row) => row._id, onRowClick: handleRowClick, sx: {
|
|
151
|
+
'& .MuiDataGrid-row': {
|
|
152
|
+
cursor: 'pointer',
|
|
153
|
+
},
|
|
154
|
+
} }) }), _jsx(ShinyUrlDetailsModal, { open: modalOpen, urlId: selectedUrlId, onClose: handleCloseModal, apiKey: apiKey, apiBaseUrl: baseUrl })] }));
|
|
155
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
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';
|
|
6
|
+
export { ShinyUrlApiKeys } from './ShinyUrlApiKeys';
|
|
7
|
+
export type { ShinyUrlApiKeysProps } from './ShinyUrlApiKeys';
|
|
8
|
+
export { ShinyUrlUrls } from './ShinyUrlUrls';
|
|
9
|
+
export type { ShinyUrlUrlsProps } from './ShinyUrlUrls';
|
|
10
|
+
export { ShinyUrlDetailsModal } from './ShinyUrlDetailsModal';
|
|
11
|
+
export type { ShinyUrlDetailsModalProps } from './ShinyUrlDetailsModal';
|
|
4
12
|
//# 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;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { default as ShinyUrlInput } from './ShinyUrlInput';
|
|
2
2
|
export * from './shortener';
|
|
3
|
+
export { ShinyUrlDashboard } from './ShinyUrlDashboard';
|
|
4
|
+
export { ShinyUrlApiKeys } from './ShinyUrlApiKeys';
|
|
5
|
+
export { ShinyUrlUrls } from './ShinyUrlUrls';
|
|
6
|
+
export { ShinyUrlDetailsModal } from './ShinyUrlDetailsModal';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shiny-url-input-box",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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,26 @@
|
|
|
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.0",
|
|
34
|
+
"@mui/x-data-grid": "^6.18.2",
|
|
35
|
+
"recharts": "^2.10.3"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=16.8.0",
|
|
39
|
+
"react-dom": ">=16.8.0",
|
|
40
|
+
"@mui/material": "^5.0.0",
|
|
41
|
+
"@emotion/react": "^11.0.0",
|
|
42
|
+
"@emotion/styled": "^11.0.0",
|
|
43
|
+
"@mui/icons-material": "^5.0.0",
|
|
44
|
+
"@mui/x-data-grid": "^6.0.0",
|
|
45
|
+
"recharts": "^2.0.0"
|
|
33
46
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
47
|
+
}
|