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,239 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Typography,
|
|
5
|
+
TextField,
|
|
6
|
+
Paper,
|
|
7
|
+
CircularProgress
|
|
8
|
+
} from '@mui/material';
|
|
9
|
+
import {
|
|
10
|
+
DataGrid,
|
|
11
|
+
GridColDef,
|
|
12
|
+
GridActionsCellItem,
|
|
13
|
+
GridToolbar,
|
|
14
|
+
GridRowParams,
|
|
15
|
+
GridPaginationModel
|
|
16
|
+
} from '@mui/x-data-grid';
|
|
17
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
18
|
+
import { ShinyUrlDetailsModal } from './ShinyUrlDetailsModal';
|
|
19
|
+
import { getDefaultApiBaseUrl } from './shortener';
|
|
20
|
+
|
|
21
|
+
export interface ShinyUrlUrlsProps {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
apiBaseUrl?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ShinyUrlUrls: React.FC<ShinyUrlUrlsProps> = ({
|
|
28
|
+
apiKey,
|
|
29
|
+
apiBaseUrl,
|
|
30
|
+
className = ''
|
|
31
|
+
}) => {
|
|
32
|
+
const [page, setPage] = useState(0);
|
|
33
|
+
const [pageSize, setPageSize] = useState(10);
|
|
34
|
+
const [search, setSearch] = useState('');
|
|
35
|
+
const [selectedUrlId, setSelectedUrlId] = useState<string | null>(null);
|
|
36
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const [rows, setRows] = useState<any[]>([]);
|
|
39
|
+
const [total, setTotal] = useState(0);
|
|
40
|
+
|
|
41
|
+
const baseUrl = apiBaseUrl || getDefaultApiBaseUrl();
|
|
42
|
+
|
|
43
|
+
const fetchUrls = useCallback(async () => {
|
|
44
|
+
if (!apiKey) return;
|
|
45
|
+
setLoading(true);
|
|
46
|
+
try {
|
|
47
|
+
const queryParams = new URLSearchParams({
|
|
48
|
+
page: (page + 1).toString(),
|
|
49
|
+
limit: pageSize.toString(),
|
|
50
|
+
search: search
|
|
51
|
+
});
|
|
52
|
+
const response = await fetch(`${baseUrl}/api/admin/urls?${queryParams}`, {
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `Bearer ${apiKey}`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const result = await response.json();
|
|
58
|
+
if (result.success) {
|
|
59
|
+
setRows(result.data || []);
|
|
60
|
+
setTotal(result.pagination?.total || 0);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to fetch URLs', error);
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
}, [apiKey, baseUrl, page, pageSize, search]);
|
|
68
|
+
|
|
69
|
+
// Debounce search
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
fetchUrls();
|
|
73
|
+
}, 500);
|
|
74
|
+
return () => clearTimeout(timer);
|
|
75
|
+
}, [fetchUrls]);
|
|
76
|
+
|
|
77
|
+
const handleDelete = async (id: string) => {
|
|
78
|
+
if (window.confirm('Are you sure you want to delete this URL?')) {
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(`${baseUrl}/api/urls/${id}`, {
|
|
81
|
+
method: 'DELETE',
|
|
82
|
+
headers: {
|
|
83
|
+
'Authorization': `Bearer ${apiKey}`
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (response.ok) {
|
|
87
|
+
fetchUrls(); // Refresh
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Failed to delete URL', error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleRowClick = (params: GridRowParams) => {
|
|
96
|
+
setSelectedUrlId(params.id as string);
|
|
97
|
+
setModalOpen(true);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleCloseModal = () => {
|
|
101
|
+
setModalOpen(false);
|
|
102
|
+
setSelectedUrlId(null);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const columns: GridColDef[] = [
|
|
106
|
+
{
|
|
107
|
+
field: 'shortCode',
|
|
108
|
+
headerName: 'Short Code',
|
|
109
|
+
width: 150,
|
|
110
|
+
renderCell: (params: any) => {
|
|
111
|
+
// Add referer as query parameter as fallback if browser doesn't send referer header
|
|
112
|
+
// For component usage, we might not know the exact "admin panel url", but we can use window.location
|
|
113
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
114
|
+
const shortUrl = `${baseUrl}/${params.value}?ref=${encodeURIComponent(currentUrl)}`;
|
|
115
|
+
return (
|
|
116
|
+
<Typography
|
|
117
|
+
component="a"
|
|
118
|
+
href={shortUrl}
|
|
119
|
+
target="_blank"
|
|
120
|
+
rel="noopener"
|
|
121
|
+
variant="body2"
|
|
122
|
+
sx={{
|
|
123
|
+
fontFamily: 'monospace',
|
|
124
|
+
bgcolor: 'action.hover',
|
|
125
|
+
px: 1,
|
|
126
|
+
py: 0.5,
|
|
127
|
+
borderRadius: 1,
|
|
128
|
+
color: 'primary.main',
|
|
129
|
+
textDecoration: 'none',
|
|
130
|
+
cursor: 'pointer',
|
|
131
|
+
'&:hover': {
|
|
132
|
+
textDecoration: 'underline',
|
|
133
|
+
bgcolor: 'action.selected',
|
|
134
|
+
},
|
|
135
|
+
}}
|
|
136
|
+
onClick={(e: React.MouseEvent) => {
|
|
137
|
+
e.stopPropagation(); // Prevent row click when clicking the link
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{params.value}
|
|
141
|
+
</Typography>
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
field: 'originalUrl',
|
|
147
|
+
headerName: 'Original URL',
|
|
148
|
+
flex: 1,
|
|
149
|
+
minWidth: 300,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
field: 'clicks',
|
|
153
|
+
headerName: 'Clicks',
|
|
154
|
+
width: 100,
|
|
155
|
+
type: 'number',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
field: 'createdAt',
|
|
159
|
+
headerName: 'Created At',
|
|
160
|
+
width: 180,
|
|
161
|
+
valueFormatter: (params: any) => {
|
|
162
|
+
if (!params.value) return '';
|
|
163
|
+
return new Date(params.value).toLocaleString();
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
field: 'actions',
|
|
168
|
+
type: 'actions',
|
|
169
|
+
headerName: 'Actions',
|
|
170
|
+
width: 100,
|
|
171
|
+
getActions: (params: GridRowParams) => [
|
|
172
|
+
<GridActionsCellItem
|
|
173
|
+
icon={<DeleteIcon />}
|
|
174
|
+
label="Delete"
|
|
175
|
+
onClick={() => handleDelete(params.id as string)}
|
|
176
|
+
color="error"
|
|
177
|
+
/>,
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
if (!apiKey) {
|
|
183
|
+
return <Typography color="error">Authentication required</Typography>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Box className={className}>
|
|
188
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
|
189
|
+
<Typography variant="h4">URLs Management</Typography>
|
|
190
|
+
<Box sx={{ display: 'flex', gap: 2 }}>
|
|
191
|
+
<TextField
|
|
192
|
+
size="small"
|
|
193
|
+
placeholder="Search URLs..."
|
|
194
|
+
value={search}
|
|
195
|
+
onChange={(e) => {
|
|
196
|
+
setSearch(e.target.value);
|
|
197
|
+
setPage(0); // Reset page on search
|
|
198
|
+
}}
|
|
199
|
+
sx={{ width: 300 }}
|
|
200
|
+
/>
|
|
201
|
+
</Box>
|
|
202
|
+
</Box>
|
|
203
|
+
|
|
204
|
+
<Paper sx={{ height: 600, width: '100%' }}>
|
|
205
|
+
<DataGrid
|
|
206
|
+
rows={rows}
|
|
207
|
+
columns={columns}
|
|
208
|
+
loading={loading}
|
|
209
|
+
pagination
|
|
210
|
+
paginationModel={{ page, pageSize }}
|
|
211
|
+
onPaginationModelChange={(model: GridPaginationModel) => {
|
|
212
|
+
setPage(model.page);
|
|
213
|
+
setPageSize(model.pageSize);
|
|
214
|
+
}}
|
|
215
|
+
paginationMode="server"
|
|
216
|
+
rowCount={total}
|
|
217
|
+
slots={{
|
|
218
|
+
toolbar: GridToolbar,
|
|
219
|
+
}}
|
|
220
|
+
getRowId={(row: any) => row._id}
|
|
221
|
+
onRowClick={handleRowClick}
|
|
222
|
+
sx={{
|
|
223
|
+
'& .MuiDataGrid-row': {
|
|
224
|
+
cursor: 'pointer',
|
|
225
|
+
},
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
</Paper>
|
|
229
|
+
|
|
230
|
+
<ShinyUrlDetailsModal
|
|
231
|
+
open={modalOpen}
|
|
232
|
+
urlId={selectedUrlId}
|
|
233
|
+
onClose={handleCloseModal}
|
|
234
|
+
apiKey={apiKey}
|
|
235
|
+
apiBaseUrl={baseUrl}
|
|
236
|
+
/>
|
|
237
|
+
</Box>
|
|
238
|
+
);
|
|
239
|
+
};
|
package/src/index.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
|
|