shiny-url-input-box 1.1.1 → 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/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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/package.json +7 -4
- package/src/ShinyUrlApiKeys.tsx +305 -0
- package/src/ShinyUrlDetailsModal.tsx +397 -0
- package/src/ShinyUrlUrls.tsx +239 -0
- package/src/index.ts +6 -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,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
|
@@ -3,4 +3,10 @@ export type { ShinyUrlInputProps } from './ShinyUrlInput';
|
|
|
3
3
|
export * from './shortener';
|
|
4
4
|
export { ShinyUrlDashboard } from './ShinyUrlDashboard';
|
|
5
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';
|
|
6
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;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,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,3 +1,6 @@
|
|
|
1
1
|
export { default as ShinyUrlInput } from './ShinyUrlInput';
|
|
2
2
|
export * from './shortener';
|
|
3
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",
|
|
@@ -30,15 +30,18 @@
|
|
|
30
30
|
"@mui/material": "^5.15.2",
|
|
31
31
|
"@emotion/react": "^11.11.3",
|
|
32
32
|
"@emotion/styled": "^11.11.0",
|
|
33
|
-
"@mui/icons-material": "^5.15.
|
|
33
|
+
"@mui/icons-material": "^5.15.0",
|
|
34
|
+
"@mui/x-data-grid": "^6.18.2",
|
|
34
35
|
"recharts": "^2.10.3"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"react": "
|
|
38
|
-
"react-dom": "
|
|
38
|
+
"react": ">=16.8.0",
|
|
39
|
+
"react-dom": ">=16.8.0",
|
|
39
40
|
"@mui/material": "^5.0.0",
|
|
40
41
|
"@emotion/react": "^11.0.0",
|
|
41
42
|
"@emotion/styled": "^11.0.0",
|
|
43
|
+
"@mui/icons-material": "^5.0.0",
|
|
44
|
+
"@mui/x-data-grid": "^6.0.0",
|
|
42
45
|
"recharts": "^2.0.0"
|
|
43
46
|
}
|
|
44
47
|
}
|