@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
package/bin/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
|
|
6
|
+
const target = process.cwd();
|
|
7
|
+
const templateDir = new URL("../template", import.meta.url).pathname;
|
|
8
|
+
|
|
9
|
+
console.log("š Creating LioranDB Studio...\n");
|
|
10
|
+
|
|
11
|
+
fs.cpSync(templateDir, target, { recursive: true });
|
|
12
|
+
|
|
13
|
+
console.log("š¦ Installing dependencies...\n");
|
|
14
|
+
|
|
15
|
+
execSync("npm install", { stdio: "inherit" });
|
|
16
|
+
|
|
17
|
+
console.log("\nš„ Starting dev server...\n");
|
|
18
|
+
|
|
19
|
+
execSync("npm run dev", { stdio: "inherit" });
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
+
|
|
3
|
+
## Getting Started
|
|
4
|
+
|
|
5
|
+
First, run the development server:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
# or
|
|
10
|
+
yarn dev
|
|
11
|
+
# or
|
|
12
|
+
pnpm dev
|
|
13
|
+
# or
|
|
14
|
+
bun dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
+
|
|
19
|
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
20
|
+
|
|
21
|
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
+
|
|
23
|
+
## Learn More
|
|
24
|
+
|
|
25
|
+
To learn more about Next.js, take a look at the following resources:
|
|
26
|
+
|
|
27
|
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
+
|
|
30
|
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
+
|
|
32
|
+
## Deploy on Vercel
|
|
33
|
+
|
|
34
|
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
+
|
|
36
|
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useAppStore } from '@/store';
|
|
6
|
+
import { LioranDBService } from '@/lib/lioran';
|
|
7
|
+
import { Sidebar } from '@/components/Sidebar';
|
|
8
|
+
import { Navbar } from '@/components/Navbar';
|
|
9
|
+
import { DocumentViewer } from '@/components/DocumentViewer';
|
|
10
|
+
import { QueryEditor } from '@/components/QueryEditor';
|
|
11
|
+
import { InputModal, JsonInputModal } from '@/components/Modal';
|
|
12
|
+
import { useToast } from '@/components/Toast';
|
|
13
|
+
import { Document } from '@/types';
|
|
14
|
+
|
|
15
|
+
export default function DashboardPage() {
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const { addToast } = useToast();
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
isLoggedIn,
|
|
21
|
+
currentDatabase,
|
|
22
|
+
selectedCollection,
|
|
23
|
+
logout,
|
|
24
|
+
setCurrentDatabase,
|
|
25
|
+
setSelectedCollection,
|
|
26
|
+
setDatabases,
|
|
27
|
+
setCollections,
|
|
28
|
+
} = useAppStore();
|
|
29
|
+
|
|
30
|
+
// Modal states
|
|
31
|
+
const [createDbModal, setCreateDbModal] = useState(false);
|
|
32
|
+
const [createColModal, setCreateColModal] = useState(false);
|
|
33
|
+
const [addDocModal, setAddDocModal] = useState(false);
|
|
34
|
+
const [editDocModal, setEditDocModal] = useState(false);
|
|
35
|
+
const [editingDoc, setEditingDoc] = useState<Document | null>(null);
|
|
36
|
+
|
|
37
|
+
// Check authentication
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isLoggedIn) {
|
|
40
|
+
router.push('/login');
|
|
41
|
+
}
|
|
42
|
+
}, [isLoggedIn, router]);
|
|
43
|
+
|
|
44
|
+
// Load databases on mount
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (isLoggedIn) {
|
|
47
|
+
loadDatabases();
|
|
48
|
+
}
|
|
49
|
+
}, [isLoggedIn]);
|
|
50
|
+
|
|
51
|
+
// Load collections when database changes
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (currentDatabase) {
|
|
54
|
+
loadCollections(currentDatabase);
|
|
55
|
+
}
|
|
56
|
+
}, [currentDatabase]);
|
|
57
|
+
|
|
58
|
+
async function loadDatabases() {
|
|
59
|
+
try {
|
|
60
|
+
const databases = await LioranDBService.listDatabases();
|
|
61
|
+
setDatabases(databases);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
addToast(`Error loading databases: ${error}`, 'error');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadCollections(dbName: string) {
|
|
68
|
+
try {
|
|
69
|
+
const collections = await LioranDBService.listCollections(dbName);
|
|
70
|
+
setCollections(dbName, collections);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
addToast(`Error loading collections: ${error}`, 'error');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handleCreateDatabase(name: string) {
|
|
77
|
+
try {
|
|
78
|
+
await LioranDBService.createDatabase(name);
|
|
79
|
+
await loadDatabases();
|
|
80
|
+
setCurrentDatabase(name);
|
|
81
|
+
addToast(`Database "${name}" created`, 'success');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
addToast(`Error creating database: ${error}`, 'error');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleCreateCollection(name: string) {
|
|
88
|
+
if (!currentDatabase) return;
|
|
89
|
+
try {
|
|
90
|
+
await LioranDBService.createCollection(currentDatabase, name);
|
|
91
|
+
await loadCollections(currentDatabase);
|
|
92
|
+
setSelectedCollection(name);
|
|
93
|
+
addToast(`Collection "${name}" created`, 'success');
|
|
94
|
+
} catch (error) {
|
|
95
|
+
addToast(`Error creating collection: ${error}`, 'error');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleAddDocument(doc: Record<string, any>) {
|
|
100
|
+
if (!currentDatabase || !selectedCollection) return;
|
|
101
|
+
try {
|
|
102
|
+
await LioranDBService.insertOne(currentDatabase, selectedCollection, doc);
|
|
103
|
+
addToast('Document added', 'success');
|
|
104
|
+
setAddDocModal(false);
|
|
105
|
+
// Reload documents
|
|
106
|
+
const docViewer = document.querySelector('[data-reload-documents]');
|
|
107
|
+
if (docViewer) {
|
|
108
|
+
const event = new CustomEvent('reload');
|
|
109
|
+
docViewer.dispatchEvent(event);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
addToast(`Error adding document: ${error}`, 'error');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleLogout() {
|
|
117
|
+
if (confirm('Are you sure you want to logout?')) {
|
|
118
|
+
logout();
|
|
119
|
+
LioranDBService.disconnect();
|
|
120
|
+
router.push('/login');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="h-screen flex flex-col bg-slate-950">
|
|
126
|
+
{/* Navbar */}
|
|
127
|
+
<Navbar onLogout={handleLogout} />
|
|
128
|
+
|
|
129
|
+
{/* Main Content */}
|
|
130
|
+
<div className="flex-1 flex overflow-hidden">
|
|
131
|
+
{/* Sidebar */}
|
|
132
|
+
<Sidebar
|
|
133
|
+
onDatabaseSelect={setCurrentDatabase}
|
|
134
|
+
onCollectionSelect={(db, col) => {
|
|
135
|
+
setCurrentDatabase(db);
|
|
136
|
+
setSelectedCollection(col);
|
|
137
|
+
}}
|
|
138
|
+
onCreateDatabase={() => setCreateDbModal(true)}
|
|
139
|
+
onCreateCollection={() => setCreateColModal(true)}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
{/* Workspace */}
|
|
143
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
144
|
+
{!currentDatabase ? (
|
|
145
|
+
// Empty State
|
|
146
|
+
<div className="flex-1 flex items-center justify-center text-slate-400">
|
|
147
|
+
<div className="text-center">
|
|
148
|
+
<div className="text-4xl mb-4">š¦</div>
|
|
149
|
+
<p className="text-lg">No database selected</p>
|
|
150
|
+
<p className="text-sm text-slate-500">Create or select a database to get started</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
) : !selectedCollection ? (
|
|
154
|
+
// Collection Selection State
|
|
155
|
+
<div className="flex-1 flex items-center justify-center text-slate-400">
|
|
156
|
+
<div className="text-center">
|
|
157
|
+
<div className="text-4xl mb-4">š</div>
|
|
158
|
+
<p className="text-lg">No collection selected</p>
|
|
159
|
+
<p className="text-sm text-slate-500">Select or create a collection to view documents</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
// Two Panel Layout
|
|
164
|
+
<div className="flex-1 flex overflow-hidden gap-4 p-4 bg-slate-900">
|
|
165
|
+
{/* Left: Document Viewer */}
|
|
166
|
+
<div className="flex-1 bg-slate-950 rounded-lg border border-slate-800 overflow-hidden flex flex-col">
|
|
167
|
+
<DocumentViewer
|
|
168
|
+
onAddDocument={() => setAddDocModal(true)}
|
|
169
|
+
onEditDocument={(doc) => {
|
|
170
|
+
setEditingDoc(doc);
|
|
171
|
+
setEditDocModal(true);
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Right: Query Editor */}
|
|
177
|
+
<div className="w-96 bg-slate-950 rounded-lg border border-slate-800 overflow-hidden">
|
|
178
|
+
<QueryEditor />
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Modals */}
|
|
186
|
+
<InputModal
|
|
187
|
+
isOpen={createDbModal}
|
|
188
|
+
title="Create Database"
|
|
189
|
+
label="Database Name"
|
|
190
|
+
placeholder="mydb"
|
|
191
|
+
onClose={() => setCreateDbModal(false)}
|
|
192
|
+
onConfirm={handleCreateDatabase}
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
<InputModal
|
|
196
|
+
isOpen={createColModal}
|
|
197
|
+
title="Create Collection"
|
|
198
|
+
label="Collection Name"
|
|
199
|
+
placeholder="users"
|
|
200
|
+
onClose={() => setCreateColModal(false)}
|
|
201
|
+
onConfirm={handleCreateCollection}
|
|
202
|
+
/>
|
|
203
|
+
|
|
204
|
+
<JsonInputModal
|
|
205
|
+
isOpen={addDocModal}
|
|
206
|
+
title="Add Document"
|
|
207
|
+
defaultValue='{}'
|
|
208
|
+
onClose={() => setAddDocModal(false)}
|
|
209
|
+
onConfirm={handleAddDocument}
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
<JsonInputModal
|
|
213
|
+
isOpen={editDocModal}
|
|
214
|
+
title="Edit Document"
|
|
215
|
+
defaultValue={editingDoc ? JSON.stringify(editingDoc, null, 2) : '{}'}
|
|
216
|
+
onClose={() => {
|
|
217
|
+
setEditDocModal(false);
|
|
218
|
+
setEditingDoc(null);
|
|
219
|
+
}}
|
|
220
|
+
onConfirm={async (doc) => {
|
|
221
|
+
if (!currentDatabase || !selectedCollection || !editingDoc) return;
|
|
222
|
+
try {
|
|
223
|
+
const _id = editingDoc._id;
|
|
224
|
+
await LioranDBService.updateMany(
|
|
225
|
+
currentDatabase,
|
|
226
|
+
selectedCollection,
|
|
227
|
+
{ _id },
|
|
228
|
+
{ $set: doc }
|
|
229
|
+
);
|
|
230
|
+
addToast('Document updated', 'success');
|
|
231
|
+
setEditDocModal(false);
|
|
232
|
+
setEditingDoc(null);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
addToast(`Error updating document: ${error}`, 'error');
|
|
235
|
+
}
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #0f172a;
|
|
5
|
+
--foreground: #f1f5f9;
|
|
6
|
+
--primary: #10b981;
|
|
7
|
+
--primary-dark: #059669;
|
|
8
|
+
--secondary: #06b6d4;
|
|
9
|
+
--destructive: #ef4444;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@theme inline {
|
|
13
|
+
--color-background: var(--background);
|
|
14
|
+
--color-foreground: var(--foreground);
|
|
15
|
+
--color-primary: var(--primary);
|
|
16
|
+
--font-sans: var(--font-geist-sans);
|
|
17
|
+
--font-mono: var(--font-geist-mono);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
html, body {
|
|
27
|
+
height: 100%;
|
|
28
|
+
background: var(--background);
|
|
29
|
+
color: var(--foreground);
|
|
30
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
html {
|
|
34
|
+
color-scheme: dark;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
background-color: #0f172a;
|
|
39
|
+
color: #f1f5f9;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Scrollbar Styling */
|
|
44
|
+
::-webkit-scrollbar {
|
|
45
|
+
width: 8px;
|
|
46
|
+
height: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
::-webkit-scrollbar-track {
|
|
50
|
+
background: transparent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
::-webkit-scrollbar-thumb {
|
|
54
|
+
background: #64748b;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
::-webkit-scrollbar-thumb:hover {
|
|
59
|
+
background: #94a3b8;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Selection */
|
|
63
|
+
::selection {
|
|
64
|
+
background-color: #10b98140;
|
|
65
|
+
color: #f1f5f9;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Smooth transitions */
|
|
69
|
+
@supports (supports: selector(:has(*))) {
|
|
70
|
+
html {
|
|
71
|
+
scroll-behavior: smooth;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
import { ToastProvider } from "@/components/Toast";
|
|
5
|
+
|
|
6
|
+
const geistSans = Geist({
|
|
7
|
+
variable: "--font-geist-sans",
|
|
8
|
+
subsets: ["latin"],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const geistMono = Geist_Mono({
|
|
12
|
+
variable: "--font-geist-mono",
|
|
13
|
+
subsets: ["latin"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
title: "LioranDB Studio",
|
|
18
|
+
description: "MongoDB Compass-like UI for LioranDB databases",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function RootLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: Readonly<{
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}>) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<body
|
|
29
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
30
|
+
>
|
|
31
|
+
<ToastProvider>
|
|
32
|
+
{children}
|
|
33
|
+
</ToastProvider>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useAppStore } from '@/store';
|
|
6
|
+
import { LioranDBService } from '@/lib/lioran';
|
|
7
|
+
import { parseConnectionUri } from '@/lib/utils';
|
|
8
|
+
import { useToast } from '@/components/Toast';
|
|
9
|
+
|
|
10
|
+
export default function LoginPage() {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const { addToast } = useToast();
|
|
13
|
+
const { setLoggedIn } = useAppStore();
|
|
14
|
+
|
|
15
|
+
const [uri, setUri] = useState('');
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
18
|
+
|
|
19
|
+
const [username, setUsername] = useState('admin');
|
|
20
|
+
const [password, setPassword] = useState('');
|
|
21
|
+
const [host, setHost] = useState('localhost');
|
|
22
|
+
const [port, setPort] = useState('4000');
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Load saved URI from localStorage
|
|
26
|
+
const savedUri = localStorage.getItem('liorandb_uri');
|
|
27
|
+
const savedToken = localStorage.getItem('liorandb_token');
|
|
28
|
+
|
|
29
|
+
if (savedUri && savedToken) {
|
|
30
|
+
// Try to auto-login
|
|
31
|
+
attemptAutoLogin(savedUri, savedToken);
|
|
32
|
+
}
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
async function attemptAutoLogin(uri: string, token: string) {
|
|
36
|
+
try {
|
|
37
|
+
setIsLoading(true);
|
|
38
|
+
LioranDBService.initialize(uri);
|
|
39
|
+
|
|
40
|
+
// Test connection
|
|
41
|
+
const databases = await LioranDBService.listDatabases();
|
|
42
|
+
|
|
43
|
+
setLoggedIn(true, token, uri);
|
|
44
|
+
useAppStore.setState({ databases });
|
|
45
|
+
addToast('Connected successfully', 'success');
|
|
46
|
+
router.push('/dashboard');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
localStorage.removeItem('liorandb_token');
|
|
49
|
+
localStorage.removeItem('liorandb_uri');
|
|
50
|
+
addToast(`Connection failed: ${error}`, 'error');
|
|
51
|
+
} finally {
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function handleLogin(e: React.FormEvent) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
|
|
59
|
+
const loginUri = showAdvanced
|
|
60
|
+
? `lioran://${username}:${password}@${host}:${port}`
|
|
61
|
+
: uri;
|
|
62
|
+
|
|
63
|
+
if (!loginUri) {
|
|
64
|
+
addToast('Please enter a connection URI', 'warning');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
setIsLoading(true);
|
|
70
|
+
|
|
71
|
+
// Validate URI format
|
|
72
|
+
try {
|
|
73
|
+
parseConnectionUri(loginUri);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
addToast(String(err), 'error');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Initialize client
|
|
80
|
+
await LioranDBService.initialize(loginUri);
|
|
81
|
+
|
|
82
|
+
// List databases to verify connection
|
|
83
|
+
const databases = await LioranDBService.listDatabases();
|
|
84
|
+
|
|
85
|
+
// Store session (in a real app, you'd get a token from the server)
|
|
86
|
+
const token = `token_${Date.now()}`;
|
|
87
|
+
|
|
88
|
+
setLoggedIn(true, token, loginUri);
|
|
89
|
+
useAppStore.setState({ databases });
|
|
90
|
+
|
|
91
|
+
addToast('Connected successfully', 'success');
|
|
92
|
+
router.push('/dashboard');
|
|
93
|
+
} catch (error) {
|
|
94
|
+
addToast(`Login failed: ${error}`, 'error');
|
|
95
|
+
} finally {
|
|
96
|
+
setIsLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-800 flex items-center justify-center p-4">
|
|
102
|
+
{/* Background Effect */}
|
|
103
|
+
<div className="absolute inset-0 overflow-hidden">
|
|
104
|
+
<div className="absolute -top-40 -right-40 w-80 h-80 bg-emerald-500/5 rounded-full blur-3xl" />
|
|
105
|
+
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-cyan-500/5 rounded-full blur-3xl" />
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Login Card */}
|
|
109
|
+
<div className="relative w-full max-w-md">
|
|
110
|
+
<div className="bg-slate-900/80 backdrop-blur border border-slate-800 rounded-xl shadow-2xl overflow-hidden">
|
|
111
|
+
{/* Header */}
|
|
112
|
+
<div className="px-8 pt-8 pb-6 border-b border-slate-800">
|
|
113
|
+
<div className="flex items-center gap-3 mb-2">
|
|
114
|
+
<div className="w-10 h-10 bg-gradient-to-br from-emerald-400 to-cyan-400 rounded-lg flex items-center justify-center">
|
|
115
|
+
<span className="text-slate-900 font-bold text-lg">ā”</span>
|
|
116
|
+
</div>
|
|
117
|
+
<h1 className="text-2xl font-bold text-slate-100">LioranDB</h1>
|
|
118
|
+
</div>
|
|
119
|
+
<p className="text-slate-400 text-sm">Database Studio</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Form */}
|
|
123
|
+
<form onSubmit={handleLogin} className="px-8 py-8 space-y-6">
|
|
124
|
+
{!showAdvanced ? (
|
|
125
|
+
// Quick Connect
|
|
126
|
+
<div>
|
|
127
|
+
<label className="block text-sm font-medium text-slate-300 mb-2">
|
|
128
|
+
Connection URI
|
|
129
|
+
</label>
|
|
130
|
+
<input
|
|
131
|
+
type="text"
|
|
132
|
+
value={uri}
|
|
133
|
+
onChange={(e) => setUri(e.target.value)}
|
|
134
|
+
placeholder="lioran://admin:password@localhost:4000"
|
|
135
|
+
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
|
|
136
|
+
disabled={isLoading}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
) : (
|
|
140
|
+
// Advanced Connect
|
|
141
|
+
<div className="space-y-4">
|
|
142
|
+
<div>
|
|
143
|
+
<label className="block text-sm font-medium text-slate-300 mb-2">
|
|
144
|
+
Username
|
|
145
|
+
</label>
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
value={username}
|
|
149
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
150
|
+
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
|
|
151
|
+
disabled={isLoading}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div>
|
|
156
|
+
<label className="block text-sm font-medium text-slate-300 mb-2">
|
|
157
|
+
Password
|
|
158
|
+
</label>
|
|
159
|
+
<input
|
|
160
|
+
type="password"
|
|
161
|
+
value={password}
|
|
162
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
163
|
+
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
|
|
164
|
+
disabled={isLoading}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="grid grid-cols-2 gap-4">
|
|
169
|
+
<div>
|
|
170
|
+
<label className="block text-sm font-medium text-slate-300 mb-2">
|
|
171
|
+
Host
|
|
172
|
+
</label>
|
|
173
|
+
<input
|
|
174
|
+
type="text"
|
|
175
|
+
value={host}
|
|
176
|
+
onChange={(e) => setHost(e.target.value)}
|
|
177
|
+
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
|
|
178
|
+
disabled={isLoading}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div>
|
|
183
|
+
<label className="block text-sm font-medium text-slate-300 mb-2">
|
|
184
|
+
Port
|
|
185
|
+
</label>
|
|
186
|
+
<input
|
|
187
|
+
type="text"
|
|
188
|
+
value={port}
|
|
189
|
+
onChange={(e) => setPort(e.target.value)}
|
|
190
|
+
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
|
|
191
|
+
disabled={isLoading}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{/* Toggle Advanced */}
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
202
|
+
className="text-sm text-emerald-400 hover:text-emerald-300 transition"
|
|
203
|
+
disabled={isLoading}
|
|
204
|
+
>
|
|
205
|
+
{showAdvanced ? 'ā Back to Quick Connect' : 'Advanced Options ā'}
|
|
206
|
+
</button>
|
|
207
|
+
|
|
208
|
+
{/* Submit Button */}
|
|
209
|
+
<button
|
|
210
|
+
type="submit"
|
|
211
|
+
disabled={isLoading || (!showAdvanced && !uri) || (showAdvanced && !password)}
|
|
212
|
+
className="w-full bg-gradient-to-r from-emerald-600 to-emerald-500 hover:from-emerald-700 hover:to-emerald-600 text-white font-semibold py-3 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
213
|
+
>
|
|
214
|
+
{isLoading ? 'Connecting...' : 'Connect to LioranDB'}
|
|
215
|
+
</button>
|
|
216
|
+
</form>
|
|
217
|
+
|
|
218
|
+
{/* Footer */}
|
|
219
|
+
<div className="px-8 py-6 bg-slate-800/30 border-t border-slate-800">
|
|
220
|
+
<p className="text-xs text-slate-400 text-center">
|
|
221
|
+
Default: <code className="bg-slate-900 px-2 py-1 rounded">lioran://admin:password@localhost:4000</code>
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Beta Badge */}
|
|
227
|
+
<div className="absolute -top-3 right-4 bg-amber-500 text-slate-900 px-3 py-1 rounded-full text-xs font-semibold">
|
|
228
|
+
BETA
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useAppStore } from '@/store';
|
|
6
|
+
|
|
7
|
+
export default function Home() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const { loadFromStorage, isLoggedIn } = useAppStore();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
loadFromStorage();
|
|
13
|
+
|
|
14
|
+
// Check localStorage for existing session
|
|
15
|
+
const hasToken = typeof window !== 'undefined' && !!localStorage.getItem('liorandb_token');
|
|
16
|
+
|
|
17
|
+
if (hasToken) {
|
|
18
|
+
router.push('/dashboard');
|
|
19
|
+
} else {
|
|
20
|
+
router.push('/login');
|
|
21
|
+
}
|
|
22
|
+
}, [router, loadFromStorage]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="h-screen bg-slate-950 flex items-center justify-center">
|
|
26
|
+
<div className="text-center space-y-4">
|
|
27
|
+
<div className="text-4xl">ā”</div>
|
|
28
|
+
<p className="text-slate-400">Connecting to LioranDB...</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|