@liorandb/studio 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +19 -0
- package/package.json +10 -0
- package/template/README.md +36 -0
- package/template/app/dashboard/page.tsx +240 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +73 -0
- package/template/app/layout.tsx +37 -0
- package/template/app/login/page.tsx +233 -0
- package/template/app/page.tsx +32 -0
- package/template/eslint.config.mjs +18 -0
- package/template/next.config.ts +7 -0
- package/template/package-lock.json +6765 -0
- package/template/package.json +31 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/src/app/dashboard/page.tsx +240 -0
- package/template/src/app/login/page.tsx +233 -0
- package/template/src/components/DocumentViewer.tsx +313 -0
- package/template/src/components/JsonViewer.tsx +93 -0
- package/template/src/components/Modal.tsx +192 -0
- package/template/src/components/Navbar.tsx +76 -0
- package/template/src/components/QueryEditor.tsx +189 -0
- package/template/src/components/Sidebar.tsx +196 -0
- package/template/src/components/Toast.tsx +91 -0
- package/template/src/lib/lioran.ts +252 -0
- package/template/src/lib/utils.ts +66 -0
- package/template/src/store/auth.ts +66 -0
- package/template/src/store/index.ts +125 -0
- package/template/src/types/index.ts +63 -0
- package/template/tsconfig.json +34 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { Plus, Edit2, Trash2, Copy } from 'lucide-react';
|
|
5
|
+
import { useAppStore } from '@/store';
|
|
6
|
+
import { LioranDBService } from '@/lib/lioran';
|
|
7
|
+
import { Document } from '@/types';
|
|
8
|
+
import { formatJSON, copyToClipboard } from '@/lib/utils';
|
|
9
|
+
import { useToast } from './Toast';
|
|
10
|
+
import { JsonViewer } from './JsonViewer';
|
|
11
|
+
|
|
12
|
+
interface DocumentViewerProps {
|
|
13
|
+
onAddDocument?: () => void;
|
|
14
|
+
onEditDocument?: (doc: Document) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function DocumentViewer({ onAddDocument, onEditDocument }: DocumentViewerProps) {
|
|
18
|
+
const { currentDatabase, selectedCollection, documents, isLoading } = useAppStore();
|
|
19
|
+
const [viewMode, setViewMode] = useState<'table' | 'json'>('table');
|
|
20
|
+
const { addToast } = useToast();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!currentDatabase || !selectedCollection) return;
|
|
24
|
+
|
|
25
|
+
loadDocuments();
|
|
26
|
+
}, [currentDatabase, selectedCollection]);
|
|
27
|
+
|
|
28
|
+
async function loadDocuments() {
|
|
29
|
+
if (!currentDatabase || !selectedCollection) return;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
useAppStore.setState({ isLoading: true });
|
|
33
|
+
const { documents: docs } = await LioranDBService.find(
|
|
34
|
+
currentDatabase,
|
|
35
|
+
selectedCollection,
|
|
36
|
+
{},
|
|
37
|
+
100
|
|
38
|
+
);
|
|
39
|
+
useAppStore.setState({ documents: docs });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
addToast(`Error loading documents: ${error}`, 'error');
|
|
42
|
+
} finally {
|
|
43
|
+
useAppStore.setState({ isLoading: false });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function handleDelete(doc: Document) {
|
|
48
|
+
if (!currentDatabase || !selectedCollection) return;
|
|
49
|
+
if (!confirm('Delete this document? This action cannot be undone.')) return;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
useAppStore.setState({ isLoading: true });
|
|
53
|
+
const _id = doc._id;
|
|
54
|
+
await LioranDBService.deleteMany(currentDatabase, selectedCollection, { _id });
|
|
55
|
+
await loadDocuments();
|
|
56
|
+
addToast('Document deleted', 'success');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
addToast(`Error deleting document: ${error}`, 'error');
|
|
59
|
+
} finally {
|
|
60
|
+
useAppStore.setState({ isLoading: false });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleCopy(doc: Document) {
|
|
65
|
+
try {
|
|
66
|
+
await copyToClipboard(formatJSON(doc));
|
|
67
|
+
addToast('Copied to clipboard', 'success');
|
|
68
|
+
} catch (error) {
|
|
69
|
+
addToast('Failed to copy', 'error');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!currentDatabase || !selectedCollection) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex items-center justify-center h-full text-slate-400">
|
|
76
|
+
<p>Select a collection to view documents</p>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col h-full">
|
|
83
|
+
{/* Toolbar */}
|
|
84
|
+
<div className="p-4 border-b border-slate-800 flex items-center justify-between">
|
|
85
|
+
<h3 className="text-lg font-semibold text-slate-100">
|
|
86
|
+
Documents ({documents.length})
|
|
87
|
+
</h3>
|
|
88
|
+
|
|
89
|
+
<div className="flex items-center gap-3">
|
|
90
|
+
<div className="flex gap-1 bg-slate-900 rounded p-1">
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => setViewMode('table')}
|
|
93
|
+
className={`px-3 py-1 rounded text-sm transition ${
|
|
94
|
+
viewMode === 'table'
|
|
95
|
+
? 'bg-emerald-600 text-white'
|
|
96
|
+
: 'text-slate-400 hover:text-slate-200'
|
|
97
|
+
}`}
|
|
98
|
+
>
|
|
99
|
+
Table
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => setViewMode('json')}
|
|
103
|
+
className={`px-3 py-1 rounded text-sm transition ${
|
|
104
|
+
viewMode === 'json'
|
|
105
|
+
? 'bg-emerald-600 text-white'
|
|
106
|
+
: 'text-slate-400 hover:text-slate-200'
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
JSON
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<button
|
|
114
|
+
onClick={onAddDocument}
|
|
115
|
+
disabled={isLoading}
|
|
116
|
+
className="flex items-center gap-2 px-3 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded transition disabled:opacity-50"
|
|
117
|
+
>
|
|
118
|
+
<Plus size={16} />
|
|
119
|
+
<span className="text-sm">Add Document</span>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Content */}
|
|
125
|
+
<div className="flex-1 overflow-auto">
|
|
126
|
+
{isLoading ? (
|
|
127
|
+
<div className="flex items-center justify-center h-full text-slate-400">
|
|
128
|
+
<p>Loading documents...</p>
|
|
129
|
+
</div>
|
|
130
|
+
) : documents.length === 0 ? (
|
|
131
|
+
<div className="flex items-center justify-center h-full text-slate-400">
|
|
132
|
+
<p>No documents in this collection</p>
|
|
133
|
+
</div>
|
|
134
|
+
) : viewMode === 'table' ? (
|
|
135
|
+
<DocumentTable
|
|
136
|
+
documents={documents}
|
|
137
|
+
onEdit={onEditDocument}
|
|
138
|
+
onDelete={handleDelete}
|
|
139
|
+
onCopy={handleCopy}
|
|
140
|
+
/>
|
|
141
|
+
) : (
|
|
142
|
+
<JsonViewMode
|
|
143
|
+
documents={documents}
|
|
144
|
+
onEdit={onEditDocument}
|
|
145
|
+
onDelete={handleDelete}
|
|
146
|
+
onCopy={handleCopy}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function DocumentTable({
|
|
155
|
+
documents,
|
|
156
|
+
onEdit,
|
|
157
|
+
onDelete,
|
|
158
|
+
onCopy,
|
|
159
|
+
}: {
|
|
160
|
+
documents: Document[];
|
|
161
|
+
onEdit?: (doc: Document) => void;
|
|
162
|
+
onDelete: (doc: Document) => void;
|
|
163
|
+
onCopy: (doc: Document) => void;
|
|
164
|
+
}) {
|
|
165
|
+
if (documents.length === 0) return null;
|
|
166
|
+
|
|
167
|
+
const keys = Array.from(
|
|
168
|
+
new Set(documents.flatMap((doc) => Object.keys(doc)))
|
|
169
|
+
).slice(0, 5); // Show first 5 columns
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="overflow-x-auto">
|
|
173
|
+
<table className="w-full border-collapse">
|
|
174
|
+
<thead>
|
|
175
|
+
<tr className="border-b border-slate-800 bg-slate-900">
|
|
176
|
+
{keys.map((key) => (
|
|
177
|
+
<th
|
|
178
|
+
key={key}
|
|
179
|
+
className="px-4 py-3 text-left text-sm font-medium text-slate-300 truncate"
|
|
180
|
+
>
|
|
181
|
+
{key}
|
|
182
|
+
</th>
|
|
183
|
+
))}
|
|
184
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-slate-300 w-24">
|
|
185
|
+
Actions
|
|
186
|
+
</th>
|
|
187
|
+
</tr>
|
|
188
|
+
</thead>
|
|
189
|
+
<tbody>
|
|
190
|
+
{documents.map((doc, idx) => (
|
|
191
|
+
<tr
|
|
192
|
+
key={idx}
|
|
193
|
+
className="border-b border-slate-800 hover:bg-slate-900/50 transition"
|
|
194
|
+
>
|
|
195
|
+
{keys.map((key) => (
|
|
196
|
+
<td
|
|
197
|
+
key={key}
|
|
198
|
+
className="px-4 py-3 text-sm text-slate-300 truncate"
|
|
199
|
+
>
|
|
200
|
+
{formatCellValue(doc[key])}
|
|
201
|
+
</td>
|
|
202
|
+
))}
|
|
203
|
+
<td className="px-4 py-3">
|
|
204
|
+
<div className="flex items-center gap-2">
|
|
205
|
+
{onEdit && (
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => onEdit(doc)}
|
|
208
|
+
className="p-1 hover:bg-slate-800 rounded transition text-slate-400 hover:text-amber-400"
|
|
209
|
+
title="Edit"
|
|
210
|
+
>
|
|
211
|
+
<Edit2 size={14} />
|
|
212
|
+
</button>
|
|
213
|
+
)}
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => onCopy(doc)}
|
|
216
|
+
className="p-1 hover:bg-slate-800 rounded transition text-slate-400 hover:text-cyan-400"
|
|
217
|
+
title="Copy"
|
|
218
|
+
>
|
|
219
|
+
<Copy size={14} />
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => onDelete(doc)}
|
|
223
|
+
className="p-1 hover:bg-slate-800 rounded transition text-slate-400 hover:text-red-400"
|
|
224
|
+
title="Delete"
|
|
225
|
+
>
|
|
226
|
+
<Trash2 size={14} />
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</td>
|
|
230
|
+
</tr>
|
|
231
|
+
))}
|
|
232
|
+
</tbody>
|
|
233
|
+
</table>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function JsonViewMode({
|
|
239
|
+
documents,
|
|
240
|
+
onEdit,
|
|
241
|
+
onDelete,
|
|
242
|
+
onCopy,
|
|
243
|
+
}: {
|
|
244
|
+
documents: Document[];
|
|
245
|
+
onEdit?: (doc: Document) => void;
|
|
246
|
+
onDelete: (doc: Document) => void;
|
|
247
|
+
onCopy: (doc: Document) => void;
|
|
248
|
+
}) {
|
|
249
|
+
const [expandedDocs, setExpandedDocs] = useState<Set<number>>(new Set());
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="space-y-2 p-4">
|
|
253
|
+
{documents.map((doc, idx) => (
|
|
254
|
+
<div
|
|
255
|
+
key={idx}
|
|
256
|
+
className="bg-slate-900 rounded border border-slate-800 overflow-hidden"
|
|
257
|
+
>
|
|
258
|
+
<div className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-slate-800/50 transition">
|
|
259
|
+
<button
|
|
260
|
+
onClick={() => {
|
|
261
|
+
const next = new Set(expandedDocs);
|
|
262
|
+
next.has(idx) ? next.delete(idx) : next.add(idx);
|
|
263
|
+
setExpandedDocs(next);
|
|
264
|
+
}}
|
|
265
|
+
className="text-slate-400 hover:text-slate-200"
|
|
266
|
+
>
|
|
267
|
+
{expandedDocs.has(idx) ? '▼' : '▶'}
|
|
268
|
+
</button>
|
|
269
|
+
<span className="flex-1 text-sm text-slate-300 ml-2 font-mono">
|
|
270
|
+
Document {idx + 1}
|
|
271
|
+
</span>
|
|
272
|
+
<div className="flex items-center gap-2">
|
|
273
|
+
{onEdit && (
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => onEdit(doc)}
|
|
276
|
+
className="p-1 hover:bg-slate-700 rounded transition text-slate-400 hover:text-amber-400"
|
|
277
|
+
title="Edit"
|
|
278
|
+
>
|
|
279
|
+
<Edit2 size={14} />
|
|
280
|
+
</button>
|
|
281
|
+
)}
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => onCopy(doc)}
|
|
284
|
+
className="p-1 hover:bg-slate-700 rounded transition text-slate-400 hover:text-cyan-400"
|
|
285
|
+
title="Copy"
|
|
286
|
+
>
|
|
287
|
+
<Copy size={14} />
|
|
288
|
+
</button>
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => onDelete(doc)}
|
|
291
|
+
className="p-1 hover:bg-slate-700 rounded transition text-slate-400 hover:text-red-400"
|
|
292
|
+
title="Delete"
|
|
293
|
+
>
|
|
294
|
+
<Trash2 size={14} />
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
{expandedDocs.has(idx) && (
|
|
299
|
+
<div className="px-4 py-3 bg-slate-950 border-t border-slate-800">
|
|
300
|
+
<JsonViewer data={doc} />
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function formatCellValue(value: any): string {
|
|
310
|
+
if (value === null || value === undefined) return '';
|
|
311
|
+
if (typeof value === 'object') return JSON.stringify(value).slice(0, 50) + '...';
|
|
312
|
+
return String(value).slice(0, 50);
|
|
313
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
interface JsonViewerProps {
|
|
6
|
+
data: any;
|
|
7
|
+
collapsed?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function JsonViewer({ data, collapsed = false }: JsonViewerProps) {
|
|
11
|
+
const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
|
|
12
|
+
|
|
13
|
+
if (data === null) {
|
|
14
|
+
return <span className="text-slate-400">null</span>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (data === undefined) {
|
|
18
|
+
return <span className="text-slate-400">undefined</span>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof data !== 'object') {
|
|
22
|
+
return <span className={getTypeColor(typeof data)}>{JSON.stringify(data)}</span>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(data)) {
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<span
|
|
29
|
+
className="cursor-pointer text-emerald-400 select-none"
|
|
30
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
31
|
+
>
|
|
32
|
+
{isCollapsed ? '▶' : '▼'}
|
|
33
|
+
</span>
|
|
34
|
+
<span className="text-slate-400">[</span>
|
|
35
|
+
{!isCollapsed && (
|
|
36
|
+
<div className="ml-4 space-y-1">
|
|
37
|
+
{data.map((item, idx) => (
|
|
38
|
+
<div key={idx}>
|
|
39
|
+
<span className="text-slate-500">{idx}:</span>
|
|
40
|
+
<span className="ml-2">
|
|
41
|
+
<JsonViewer data={item} />
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
<span className="text-slate-400">]</span>
|
|
48
|
+
{isCollapsed && <span className="text-slate-500 ml-2">... ({data.length} items)</span>}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Object
|
|
54
|
+
const keys = Object.keys(data);
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<span
|
|
58
|
+
className="cursor-pointer text-emerald-400 select-none"
|
|
59
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
60
|
+
>
|
|
61
|
+
{isCollapsed ? '▶' : '▼'}
|
|
62
|
+
</span>
|
|
63
|
+
<span className="text-slate-400">{'{}'}</span>
|
|
64
|
+
{!isCollapsed && (
|
|
65
|
+
<div className="ml-4 space-y-1">
|
|
66
|
+
{keys.map((key) => (
|
|
67
|
+
<div key={key}>
|
|
68
|
+
<span className="text-cyan-400">"{key}"</span>
|
|
69
|
+
<span className="text-slate-400">: </span>
|
|
70
|
+
<span className="ml-2">
|
|
71
|
+
<JsonViewer data={data[key]} />
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
{isCollapsed && <span className="text-slate-500 ml-2">... ({keys.length} fields)</span>}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getTypeColor(type: string): string {
|
|
83
|
+
switch (type) {
|
|
84
|
+
case 'string':
|
|
85
|
+
return 'text-amber-400';
|
|
86
|
+
case 'number':
|
|
87
|
+
return 'text-blue-400';
|
|
88
|
+
case 'boolean':
|
|
89
|
+
return 'text-pink-400';
|
|
90
|
+
default:
|
|
91
|
+
return 'text-slate-400';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
title: string;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onConfirm?: () => Promise<void> | void;
|
|
12
|
+
confirmText?: string;
|
|
13
|
+
isLoading?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Modal({
|
|
17
|
+
isOpen,
|
|
18
|
+
title,
|
|
19
|
+
children,
|
|
20
|
+
onClose,
|
|
21
|
+
onConfirm,
|
|
22
|
+
confirmText = 'Confirm',
|
|
23
|
+
isLoading = false,
|
|
24
|
+
}: ModalProps) {
|
|
25
|
+
if (!isOpen) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
29
|
+
<div className="bg-slate-900 rounded-lg border border-slate-800 w-full max-w-md max-h-[90vh] flex flex-col">
|
|
30
|
+
{/* Header */}
|
|
31
|
+
<div className="flex items-center justify-between p-4 border-b border-slate-800">
|
|
32
|
+
<h2 className="text-lg font-semibold text-slate-100">{title}</h2>
|
|
33
|
+
<button
|
|
34
|
+
onClick={onClose}
|
|
35
|
+
className="p-1 hover:bg-slate-800 rounded transition text-slate-400"
|
|
36
|
+
>
|
|
37
|
+
<X size={20} />
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Content */}
|
|
42
|
+
<div className="flex-1 overflow-y-auto p-4">{children}</div>
|
|
43
|
+
|
|
44
|
+
{/* Footer */}
|
|
45
|
+
<div className="flex gap-2 p-4 border-t border-slate-800">
|
|
46
|
+
<button
|
|
47
|
+
onClick={onClose}
|
|
48
|
+
disabled={isLoading}
|
|
49
|
+
className="flex-1 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-100 rounded transition disabled:opacity-50"
|
|
50
|
+
>
|
|
51
|
+
Cancel
|
|
52
|
+
</button>
|
|
53
|
+
{onConfirm && (
|
|
54
|
+
<button
|
|
55
|
+
onClick={onConfirm}
|
|
56
|
+
disabled={isLoading}
|
|
57
|
+
className="flex-1 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded transition disabled:opacity-50 font-medium"
|
|
58
|
+
>
|
|
59
|
+
{isLoading ? 'Loading...' : confirmText}
|
|
60
|
+
</button>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface InputModalProps {
|
|
69
|
+
isOpen: boolean;
|
|
70
|
+
title: string;
|
|
71
|
+
label: string;
|
|
72
|
+
placeholder?: string;
|
|
73
|
+
defaultValue?: string;
|
|
74
|
+
onClose: () => void;
|
|
75
|
+
onConfirm: (value: string) => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function InputModal({
|
|
79
|
+
isOpen,
|
|
80
|
+
title,
|
|
81
|
+
label,
|
|
82
|
+
placeholder,
|
|
83
|
+
defaultValue = '',
|
|
84
|
+
onClose,
|
|
85
|
+
onConfirm,
|
|
86
|
+
}: InputModalProps) {
|
|
87
|
+
const [value, setValue] = useState(defaultValue);
|
|
88
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
setValue(defaultValue);
|
|
92
|
+
}, [defaultValue, isOpen]);
|
|
93
|
+
|
|
94
|
+
const handleConfirm = async () => {
|
|
95
|
+
if (!value.trim()) return;
|
|
96
|
+
try {
|
|
97
|
+
setIsLoading(true);
|
|
98
|
+
await onConfirm(value.trim());
|
|
99
|
+
setValue('');
|
|
100
|
+
onClose();
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Modal
|
|
108
|
+
isOpen={isOpen}
|
|
109
|
+
title={title}
|
|
110
|
+
onClose={onClose}
|
|
111
|
+
onConfirm={handleConfirm}
|
|
112
|
+
isLoading={isLoading}
|
|
113
|
+
>
|
|
114
|
+
<div className="space-y-4">
|
|
115
|
+
<label className="block">
|
|
116
|
+
<span className="block text-sm font-medium text-slate-300 mb-2">{label}</span>
|
|
117
|
+
<input
|
|
118
|
+
type="text"
|
|
119
|
+
value={value}
|
|
120
|
+
onChange={(e) => setValue(e.target.value)}
|
|
121
|
+
placeholder={placeholder}
|
|
122
|
+
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-slate-100 placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500"
|
|
123
|
+
disabled={isLoading}
|
|
124
|
+
onKeyDown={(e) => {
|
|
125
|
+
if (e.key === 'Enter') handleConfirm();
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</label>
|
|
129
|
+
</div>
|
|
130
|
+
</Modal>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface JsonInputModalProps {
|
|
135
|
+
isOpen: boolean;
|
|
136
|
+
title: string;
|
|
137
|
+
defaultValue?: string;
|
|
138
|
+
onClose: () => void;
|
|
139
|
+
onConfirm: (value: Record<string, any>) => Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function JsonInputModal({
|
|
143
|
+
isOpen,
|
|
144
|
+
title,
|
|
145
|
+
defaultValue = '{}',
|
|
146
|
+
onClose,
|
|
147
|
+
onConfirm,
|
|
148
|
+
}: JsonInputModalProps) {
|
|
149
|
+
const [value, setValue] = useState(defaultValue);
|
|
150
|
+
const [error, setError] = useState('');
|
|
151
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
setValue(defaultValue);
|
|
155
|
+
setError('');
|
|
156
|
+
}, [defaultValue, isOpen]);
|
|
157
|
+
|
|
158
|
+
const handleConfirm = async () => {
|
|
159
|
+
try {
|
|
160
|
+
setError('');
|
|
161
|
+
const parsed = JSON.parse(value);
|
|
162
|
+
setIsLoading(true);
|
|
163
|
+
await onConfirm(parsed);
|
|
164
|
+
setValue('{}');
|
|
165
|
+
onClose();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
setError(`Invalid JSON: ${err}`);
|
|
168
|
+
} finally {
|
|
169
|
+
setIsLoading(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Modal
|
|
175
|
+
isOpen={isOpen}
|
|
176
|
+
title={title}
|
|
177
|
+
onClose={onClose}
|
|
178
|
+
onConfirm={handleConfirm}
|
|
179
|
+
isLoading={isLoading}
|
|
180
|
+
>
|
|
181
|
+
<div className="space-y-3">
|
|
182
|
+
<textarea
|
|
183
|
+
value={value}
|
|
184
|
+
onChange={(e) => setValue(e.target.value)}
|
|
185
|
+
className="w-full h-64 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-slate-100 font-mono text-sm placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 resize-none"
|
|
186
|
+
disabled={isLoading}
|
|
187
|
+
/>
|
|
188
|
+
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
189
|
+
</div>
|
|
190
|
+
</Modal>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { LogOut, Settings, Zap } from 'lucide-react';
|
|
5
|
+
import { useAppStore } from '@/store';
|
|
6
|
+
|
|
7
|
+
interface NavbarProps {
|
|
8
|
+
onLogout: () => void;
|
|
9
|
+
onSettings?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Navbar({ onLogout, onSettings }: NavbarProps) {
|
|
13
|
+
const { currentDatabase, selectedCollection, isLoading } = useAppStore();
|
|
14
|
+
const [connectionStatus] = useState(true);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="h-16 bg-slate-950 border-b border-slate-800 flex items-center justify-between px-6">
|
|
18
|
+
{/* Left: Breadcrumb */}
|
|
19
|
+
<div className="flex items-center gap-3">
|
|
20
|
+
<div className="flex items-center gap-2">
|
|
21
|
+
<div
|
|
22
|
+
className={`w-2.5 h-2.5 rounded-full transition ${
|
|
23
|
+
connectionStatus ? 'bg-emerald-500' : 'bg-red-500'
|
|
24
|
+
}`}
|
|
25
|
+
/>
|
|
26
|
+
<span className="text-slate-400 text-sm">
|
|
27
|
+
{connectionStatus ? 'Connected' : 'Disconnected'}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{currentDatabase && (
|
|
32
|
+
<>
|
|
33
|
+
<span className="text-slate-600">/</span>
|
|
34
|
+
<span className="text-slate-300 font-medium">{currentDatabase}</span>
|
|
35
|
+
|
|
36
|
+
{selectedCollection && (
|
|
37
|
+
<>
|
|
38
|
+
<span className="text-slate-600">/</span>
|
|
39
|
+
<span className="text-slate-300 font-medium">{selectedCollection}</span>
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Right: Actions */}
|
|
47
|
+
<div className="flex items-center gap-3">
|
|
48
|
+
{isLoading && (
|
|
49
|
+
<div className="flex items-center gap-2 text-slate-400">
|
|
50
|
+
<Zap size={16} className="animate-spin" />
|
|
51
|
+
<span className="text-sm">Loading...</span>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{onSettings && (
|
|
56
|
+
<button
|
|
57
|
+
onClick={onSettings}
|
|
58
|
+
className="p-2 hover:bg-slate-800 rounded transition text-slate-400 hover:text-slate-200"
|
|
59
|
+
title="Settings"
|
|
60
|
+
>
|
|
61
|
+
<Settings size={18} />
|
|
62
|
+
</button>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<button
|
|
66
|
+
onClick={onLogout}
|
|
67
|
+
className="flex items-center gap-2 px-3 py-2 text-slate-400 hover:text-red-400 hover:bg-slate-800 rounded transition"
|
|
68
|
+
title="Logout"
|
|
69
|
+
>
|
|
70
|
+
<LogOut size={16} />
|
|
71
|
+
<span className="text-sm">Logout</span>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|