diff --git a/package-lock.json b/package-lock.json index 35453ed..5b44222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "better-sqlite3": "^9.4.1", "cors": "^2.8.5", - "express": "^4.18.2" + "express": "^4.18.2", + "xlsx": "^0.18.5" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", @@ -24,6 +25,7 @@ "@types/express": "^4.17.21", "@types/node": "^20.11.19", "@types/supertest": "^6.0.3", + "@types/xlsx": "^0.0.35", "@vitest/ui": "^3.1.3", "supertest": "^7.1.0", "svelte": "^4.2.7", @@ -1433,6 +1435,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/xlsx": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/xlsx/-/xlsx-0.0.35.tgz", + "integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vercel/nft": { "version": "0.29.2", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.2.tgz", @@ -1721,6 +1730,15 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2051,6 +2069,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -2123,6 +2154,15 @@ "periscopic": "^3.1.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2247,6 +2287,18 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2828,6 +2880,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -4335,6 +4396,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5677,6 +5750,24 @@ "node": ">=8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -5781,6 +5872,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 3f74baf..c40a418 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/express": "^4.17.21", "@types/node": "^20.11.19", "@types/supertest": "^6.0.3", + "@types/xlsx": "^0.0.35", "@vitest/ui": "^3.1.3", "supertest": "^7.1.0", "svelte": "^4.2.7", @@ -36,6 +37,7 @@ "dependencies": { "better-sqlite3": "^9.4.1", "cors": "^2.8.5", - "express": "^4.18.2" + "express": "^4.18.2", + "xlsx": "^0.18.5" } } diff --git a/src/app.css b/src/app.css index 5680de5..0361b91 100644 --- a/src/app.css +++ b/src/app.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital,wght@0,400;1,400&family=Inter:wght@400;600;700&display=swap'); + :root { --primary-green: #00c853; --primary-green-dark: #009624; @@ -15,7 +17,7 @@ html, body { background: var(--background); color: var(--text); - font-family: "Inter", "Roboto", "Segoe UI", Arial, sans-serif; + font-family: 'Inter', 'Roboto', 'Segoe UI', Arial, sans-serif; min-height: 100vh; width: 100vw; margin: 0; @@ -28,6 +30,14 @@ body { padding: 2rem; } +h1, +h2, +h3 { + font-family: 'DM Serif Display', 'Playfair Display', serif; + font-weight: 400; + letter-spacing: 0.01em; +} + h1 { font-size: 2.5rem; font-weight: 700; @@ -317,7 +327,7 @@ tr:nth-child(even) td { .json-preview { font-family: "Fira Mono", monospace; - font-size: 0.9rem; + font-size: 0.8rem; margin: 1rem 0; max-height: 350px; padding: 1.1rem; @@ -528,6 +538,31 @@ p { margin-bottom: 0.3rem; } +.app-summary-card, +.app-summary-card * { + font-family: 'DM Serif Display', 'Playfair Display', serif !important; + font-weight: 400; + letter-spacing: 0.01em; +} + +/* Keep Inter for code, buttons, and monospace areas */ +button, +.clear-btn, +.json-preview, +.history-item .query, +.query-section textarea, +.raw-json-section textarea { + font-family: 'Inter', 'Roboto', 'Segoe UI', Arial, sans-serif !important; +} + +.sample-suggestions, +.sample-suggestions *, +.sample-suggestions button { + font-family: 'DM Serif Display', 'Playfair Display', serif !important; + font-weight: 400; + letter-spacing: 0.01em; +} + @media (max-width: 600px) { .container { padding: 0.7rem; diff --git a/src/app.css.save b/src/app.css.save new file mode 100644 index 0000000..0b3168a --- /dev/null +++ b/src/app.css.save @@ -0,0 +1,578 @@ +:root { + --primary-green: #00c853; + --primary-green-dark: #009624; + --background: #003d1f; + --card-bg: #fff; + --border: #1b5e20; + --text: #fff; + --text-light: #bdbdbd; + --error-bg: #ffebee; + --error-text: #c62828; + --success-text: #00c853; +} + +html, +body { + background: var(--background); + color: var(--text); + font-family: "Inter", "Roboto", "Segoe UI", Arial, sans-serif; + min-height: 100vh; + width: 100vw; + margin: 0; + padding: 0; +} + +.container { + max-width: 900px; + margin: 0 auto; + padding: 2rem; +} + +h1 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 2rem; +} + +.query-section { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + padding: 1.2rem 1.5rem 2rem; + margin-bottom: 2rem; + color: #222; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.query-section h2 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +.query-section textarea, +.raw-json-section textarea { + width: 100%; + min-width: 100%; + max-width: 100%; + box-sizing: border-box; + font-family: "Fira Mono", monospace; + margin-left: 0; + margin-right: 0; +} + +.query-section textarea { + margin-bottom: 1.5rem; + margin-top: -1.5rem; + font-size: 1.15rem; + padding: 1.1rem; + background: #f7f7f7; + color: #067800; + transition: border 0.2s; + min-height: 160px; + max-height: 350px; + resize: vertical; + overflow-y: auto; + border: 1.5px solid #bdbdbd; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + overflow-x: hidden; + white-space: pre-wrap; +} + +/* --- Unified custom scrollbar styles for textarea and JSON preview --- */ +.query-section textarea, +.json-preview { + scrollbar-width: thin; + /* Firefox */ + scrollbar-color: #009624 #e0e0e0; + /* Firefox */ + overflow-x: hidden; + white-space: pre-wrap; +} + +.query-section textarea::-webkit-scrollbar, +.json-preview::-webkit-scrollbar { + width: 10px; + background: #e0e0e0; + border-radius: 6px; +} + +.query-section textarea::-webkit-scrollbar-thumb, +.json-preview::-webkit-scrollbar-thumb { + background: #009624; + border-radius: 6px; +} + +.query-section textarea:focus, +.json-preview:focus { + border-color: #009624; + outline: none; +} + +.query-actions { + display: flex; + gap: 0.7rem; + margin-bottom: 0; + justify-content: flex-start; +} + +button, +.clear-btn { + padding: 0.65rem 1.5rem; + background-color: var(--primary-green); + color: #fff; + border: none; + border-radius: 6px; + font-size: 1.05rem; + font-weight: 600; + cursor: pointer; + transition: + background 0.2s, + box-shadow 0.2s; + box-shadow: 0 1px 4px rgba(0, 200, 83, 0.07); + min-width: 140px; +} + +button:disabled, +.clear-btn:disabled { + background-color: #e0e0e0; + color: #aaa; + cursor: not-allowed; +} + +button:hover:not(:disabled), +.clear-btn:hover:not(:disabled) { + background-color: var(--primary-green-dark); +} + +.clear-btn { + background: #e0e0e0; + color: #222; + border: none; + border-radius: 6px; + font-size: 1.05rem; + font-weight: 500; + transition: background 0.2s; +} + +.clear-btn:disabled { + background: #f3f3f3; + color: #bbb; +} + +.result-section { + margin-top: 5rem; + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + padding: 1rem 5rem 2rem 2rem; + margin-bottom: 2rem; + color: #222; +} + +.result-section h2 { + color: var(--primary-green-dark); + margin-top: 0.2rem; + margin-bottom: 0.3rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 0; + background: var(--card-bg); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); +} + +th, +td { + border: 1px solid var(--border); + padding: 0.75rem 1rem; + text-align: left; +} + +th { + background: #f5f5f5; + color: var(--primary-green-dark); + font-weight: 700; +} + +tr:nth-child(even) td { + background: #fafafa; +} + +.error { + color: var(--error-text); + background: var(--error-bg); + padding: 1rem; + border-radius: 6px; + margin-top: 1rem; + font-weight: 500; +} + +.history-section { + margin-top: 2rem; +} + +.history-item { + border: 1px solid var(--border); + background: var(--card-bg); + border-radius: 8px; + padding: 1rem 1.5rem; + margin-bottom: 1.2rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03); + color: #222; +} + +.history-item .query { + font-family: "Fira Mono", monospace; + margin-bottom: 0.5rem; + color: var(--primary-green-dark); + font-size: 1.05rem; +} + +.history-item .timestamp { + color: var(--text-light); + font-size: 0.95rem; + margin-bottom: 0.1rem; +} + +.history-item .success { + color: var(--success-text); + font-weight: 600; + margin-top: 0.2rem; +} + +.history-item .error { + color: var(--error-text); + background: var(--error-bg); + border-radius: 4px; + padding: 0.5rem 1rem; + margin-top: 0.5rem; + font-weight: 500; +} + +.json-input-section { + margin-bottom: 2.2rem; +} + +.upload-zone { + border: 2px dashed var(--border); + border-radius: 12px; + padding: 2rem; + text-align: center; + background: rgba(255, 255, 255, 0.05); + transition: all 0.3s ease; + cursor: pointer; +} + +.upload-zone.dragging { + border-color: var(--primary-green); + background: rgba(0, 200, 83, 0.1); +} + +.upload-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.upload-icon { + color: var(--primary-green); + margin-bottom: 1rem; +} + +.upload-button { + background: var(--primary-green); + color: white; + padding: 0.75rem 1.5rem; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + transition: background 0.2s; +} + +.upload-button:hover { + background: var(--primary-green-dark); +} + +.hidden { + display: none; +} + +.preview-section { + margin-top: 1.5rem; + background: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + color: #222; +} + +.json-preview { + font-family: "Fira Mono", monospace; + font-size: 0.9rem; + margin: 1rem 0; + max-height: 350px; + padding: 1.1rem; + background: #f7f7f7; + color: #067800; + border: 1.5px solid #bdbdbd; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + overflow-x: hidden; + white-space: pre-wrap; +} + +.preview-note { + color: #666; + font-size: 0.9rem; + margin: 0; +} + +.result-meta { + margin: 0; + color: #222; + font-size: 1.05rem; +} + +.table-container { + overflow-x: auto; + margin-top: 0; +} + +h2 { + color: var(--text); + margin-bottom: 1rem; + font-size: 1.5rem; +} + +h3 { + color: var(--text); + margin: 0; + font-size: 1.2rem; +} + +p { + margin: 0.5rem 0; + color: var(--text-light); +} + +.input-mode-toggle { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.toggle-btn { + flex: 1; + background: rgba(255, 255, 255, 0.1); + color: var(--text); + border: 1px solid var(--border); + padding: 0.75rem; + border-radius: 6px; + font-weight: 500; + transition: all 0.2s; +} + +.toggle-btn.active { + background: var(--primary-green); + border-color: var(--primary-green); +} + +.toggle-btn:hover:not(.active) { + background: rgba(255, 255, 255, 0.15); +} + +.raw-json-section { + background: var(--card-bg); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.raw-json-section textarea { + font-size: 1.15rem; + line-height: 1.5; + background: #f5f5f5; + border: 1px solid var(--border); + border-radius: 6px; + padding: 1rem; + color: #222; + height: 200px; + min-height: 200px; + max-height: 200px; + resize: none; +} + +.raw-json-section textarea:focus { + border-color: var(--primary-green); + outline: none; +} + +/* Ensure Preview title is green */ +.preview-section h2 { + color: #067800; + margin-bottom: 1rem; + margin-top: -2px; +} + +.table-container { + overflow-x: auto; + margin-top: 1rem; +} + +h2 { + color: var(--text); + margin-bottom: 1rem; + font-size: 1.5rem; +} + +h3 { + color: var(--text); + margin: 0; + font-size: 1.2rem; +} + +p { + margin: 0.5rem 0; + color: var(--text-light); +} + +.sample-suggestions { + margin: 1.2rem 0 2rem 0; + display: flex; + align-items: center; + gap: 0.7rem; + flex-wrap: wrap; + padding: 1rem 1.5rem; + background: rgba(0, 150, 36, 0.07); + border: 1.5px solid #b9f6ca; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 200, 83, 0.06); + justify-content: flex-start; +} + +.sample-suggestions span { + font-weight: 600; + font-size: 1.08rem; + margin-right: 0.5rem; +} + +.sample-suggestions button { + background: #e0e0e0; + color: #067800; + border: 2px solid #009624; + border-radius: 20px; + padding: 0.5rem 1.4rem; + font-size: 1.08rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s, box-shadow 0.2s, border-color 0.2s; + box-shadow: 0 1px 4px rgba(0, 200, 83, 0.09); + outline: none; +} + +.sample-suggestions button:hover { + background: #b9f6ca; + border-color: #00c853; + box-shadow: 0 2px 8px rgba(0, 200, 83, 0.18); +} + +.json-input-section .sample-suggestions span { + color: #fff; +} + +.sample-suggestions span { + color: #222; +} + +.raw-json-section h2 { + margin-top: -2px; + color: #067800; +} + +.app-summary-card { + background: #f7f7f7; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + padding: 1.2rem 2rem 1.2rem 2rem; + margin-bottom: 2.2rem; + border: 1.5px solid #b9f6ca; + width: 100%; + box-sizing: border-box; +} + +.app-summary { + color: #067800; + font-size: 1.13rem; + margin-bottom: 0.7rem; + text-align: center; +} + +.app-guide { + color: #222; + font-size: 1.05rem; + margin: 0; + padding-left: 1.2rem; + text-align: left; +} + +.app-guide li { + margin-bottom: 0.3rem; +} + +@media (max-width: 600px) { + .container { + padding: 0.7rem; + } + + .app-summary-card { + padding: 0.7rem 0.7rem 0.7rem 0.7rem; + font-size: 0.98rem; + } + + .app-summary { + font-size: 1rem; + } + + .app-guide { + font-size: 0.97rem; + padding-left: 0.7rem; + } + + .sample-suggestions { + padding: 0.7rem; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .sample-suggestions button { + width: 100%; + min-width: 0; + text-align: left; + font-size: 1rem; + padding: 0.5rem 0.7rem; + } + + .query-actions { + flex-direction: column; + align-items: stretch; + gap: 0.7rem; + } + + .query-actions button, + .query-actions .clear-btn { + width: 100%; + min-width: 0; + box-sizing: border-box; + margin: 0; + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ea51fb7..3bc27c5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -11,11 +11,17 @@ let queryTime: number | null = null; let nextId = 1; let jsonInput = ""; - let data: Row[] = []; + let tables: Record = {}; + let currentTable: string | null = null; let jsonError: string | null = null; let isDragging = false; let previewData: string = ""; let inputMode: "file" | "raw" = "file"; + let copyMessage = ""; + let copyTimeout: ReturnType | null = null; + let copySqlMessage = ""; + let copySqlTimeout: ReturnType | null = null; + let selectedHistory: QueryHistory | null = null; // Sample data for suggestions const sampleJsons = [ @@ -27,25 +33,68 @@ { "state": "Florida", "region": "South", "pop": 21538187, "pop_male": 10470577, "pop_female": 11067610 } ]`, }, - // Add more samples if you want - ]; - - const sampleSqls = [ { - label: "All States", - value: "SELECT * FROM table;", - }, - { - label: "States with pop > 20M", - value: "SELECT state, pop FROM table WHERE pop > 20000000;", + label: "World Cities", + value: `[ + { "city": "Tokyo", "country": "Japan", "population": 37400068, "area_km2": 2191 }, + { "city": "Delhi", "country": "India", "population": 28514000, "area_km2": 1484 }, + { "city": "Shanghai", "country": "China", "population": 25582000, "area_km2": 6340 } +]`, }, { - label: "States in the South", - value: "SELECT state FROM table WHERE region = 'South';", + label: "Books", + value: `[ + { "title": "To Kill a Mockingbird", "author": "Harper Lee", "year": 1960, "genre": "Fiction" }, + { "title": "1984", "author": "George Orwell", "year": 1949, "genre": "Dystopian" }, + { "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "genre": "Classic" } +]`, }, // Add more samples if you want ]; + const sampleSqlMap: Record = { + "US States Population": [ + { label: "All States", value: "SELECT * FROM table;" }, + { + label: "States with pop > 20M", + value: "SELECT state, pop FROM table WHERE pop > 20000000;", + }, + { + label: "States in the South", + value: "SELECT state FROM table WHERE region = 'South';", + }, + ], + "World Cities": [ + { label: "All Cities", value: "SELECT * FROM table;" }, + { + label: "Cities with pop > 25M", + value: "SELECT city, population FROM table WHERE population > 25000000;", + }, + { + label: "Cities in China", + value: "SELECT city FROM table WHERE country = 'China';", + }, + ], + Books: [ + { label: "All Books", value: "SELECT * FROM table;" }, + { + label: "Books before 1950", + value: "SELECT title, year FROM table WHERE year < 1950;", + }, + { + label: "Fiction Books", + value: "SELECT title FROM table WHERE genre = 'Fiction';", + }, + ], + }; + + let sampleSqls = sampleSqlMap["US States Population"]; + + function setSampleSqlsFor(label: string) { + sampleSqls = + sampleSqlMap[label] || sampleSqlMap["US States Population"]; + } + function saveHistory() { sessionStorage.setItem("queryHistory", JSON.stringify(history)); sessionStorage.setItem("queryHistoryNextId", String(nextId)); @@ -61,27 +110,40 @@ function processJsonInput(input: string) { jsonInput = input; try { - data = JSON.parse(input); - jsonError = null; - previewData = JSON.stringify(data, null, 2); + const rows = JSON.parse(input); + if (Array.isArray(rows)) { + addTable("RawInput", rows); + jsonError = null; + } else { + jsonError = "Input is not a JSON array."; + } } catch (err) { jsonError = "Invalid JSON input."; - data = []; - previewData = ""; } } function handleFileChange(event: Event) { - const file = (event.target as HTMLInputElement).files?.[0]; - if (file) { - processFile(file); + const files = (event.target as HTMLInputElement).files; + if (files) { + for (const file of Array.from(files)) { + processFile(file); + } } } function processFile(file: File) { const reader = new FileReader(); reader.onload = (e) => { - processJsonInput(e.target?.result as string); + try { + const rows = JSON.parse(e.target?.result as string); + if (Array.isArray(rows)) { + addTable(file.name.replace(/\.[^/.]+$/, ""), rows); + } else { + jsonError = `File ${file.name} does not contain a JSON array.`; + } + } catch (err) { + jsonError = `Invalid JSON in file ${file.name}.`; + } }; reader.readAsText(file); } @@ -108,7 +170,7 @@ function executeQuery() { if (!query.trim()) return; - if (!data.length) { + if (!currentTable || !tables[currentTable]) { result = { success: false, error: "No JSON data loaded." }; return; } @@ -116,7 +178,7 @@ queryTime = null; const start = performance.now(); try { - const parser = new SQLParser(data); + const parser = new SQLParser(tables[currentTable]); const parsed = parser.parse(query); const res = parser.execute(parsed); result = { success: true, data: res }; @@ -151,6 +213,105 @@ saveHistory(); } + function copyToClipboard(text: string) { + navigator.clipboard.writeText(text).then(() => { + copyMessage = "Copied!"; + if (copyTimeout) clearTimeout(copyTimeout); + copyTimeout = setTimeout(() => (copyMessage = ""), 1200); + }); + } + + function copySqlToClipboard() { + navigator.clipboard.writeText(query).then(() => { + copySqlMessage = "Copied!"; + if (copySqlTimeout) clearTimeout(copySqlTimeout); + copySqlTimeout = setTimeout(() => (copySqlMessage = ""), 1200); + }); + } + + // Helper to add a new table + function addTable(name: string, rows: Row[]) { + tables = { ...tables, [name]: rows }; + if (!currentTable) currentTable = name; + } + + // Helper to remove a table + function removeTable(name: string) { + const { [name]: _, ...rest } = tables; + tables = rest; + if (currentTable === name) { + currentTable = Object.keys(tables)[0] || null; + } + } + + // Preview logic for current table + $: previewData = + currentTable && tables[currentTable] + ? JSON.stringify(tables[currentTable], null, 2) + : ""; + + // Export helpers + function exportResultAsJSON() { + if (result && result.data) { + const blob = new Blob([JSON.stringify(result.data, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "query_result.json"; + a.click(); + URL.revokeObjectURL(url); + } + } + + function exportResultAsCSV() { + if (result && result.data && result.data.length > 0) { + const headers = Object.keys(result.data[0]); + const csvRows = [headers.join(",")]; + for (const row of result.data) { + csvRows.push( + headers.map((h) => JSON.stringify(row[h] ?? "")).join(","), + ); + } + const csv = csvRows.join("\n"); + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "query_result.csv"; + a.click(); + URL.revokeObjectURL(url); + } + } + + // Excel export (SheetJS) + async function exportResultAsExcel() { + if (result && result.data && result.data.length > 0) { + try { + const XLSX = await import("xlsx"); + const ws = XLSX.utils.json_to_sheet(result.data); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "Results"); + const wbout = XLSX.write(wb, { + bookType: "xlsx", + type: "array", + }); + const blob = new Blob([wbout], { + type: "application/octet-stream", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "query_result.xlsx"; + a.click(); + URL.revokeObjectURL(url); + } catch (e) { + alert("Excel export requires SheetJS (xlsx) to be installed."); + } + } + } + onMount(() => { loadHistoryFromSession(); }); @@ -207,6 +368,7 @@ on:click={() => { jsonInput = sample.value; processJsonInput(sample.value); + setSampleSqlsFor(sample.label); }} > {sample.label} @@ -264,14 +426,33 @@ {/if} {#if previewData} -
+

Preview

{previewData}
-

- Showing {data.length} total record{data.length === 1 - ? "" - : "s"} -

+
+

+ Showing {(currentTable && + tables[currentTable]?.length) || + 0} total record{(currentTable && + tables[currentTable]?.length) === 1 + ? "" + : "s"} +

+ +
+ {#if copyMessage} +
+ {copyMessage} +
+ {/if}
{/if}
@@ -291,31 +472,72 @@ bind:value={query} placeholder="Enter your SQL query here..." rows="4" + style="width:100%;" > -
- + + +
+ - - + {#if copySqlMessage} +
+ {copySqlMessage} +
+ {/if} {#if result}

Result

- {#if result.success} + {#if result && result.success} {#if result.data && result.data.length > 0} +
+ + + +
+
{result.data.length} row{result.data.length === 1 @@ -346,10 +568,15 @@
+ {#if copyMessage} +
+ {copyMessage} +
+ {/if} {:else}

No results found

{/if} - {:else} + {:else if result && result.error}
{result.error}
@@ -361,7 +588,11 @@

Query History ({history.length})

{#each history as item} -
+
(selectedHistory = item)} + >
{item.query}
{new Date(item.timestamp).toLocaleString()} @@ -377,4 +608,106 @@ {/each}
{/if} + + {#if selectedHistory} + + {/if} + + {#if Object.keys(tables).length > 0} +
+

Loaded Tables

+
    + {#each Object.keys(tables) as tableName} +
  • + + +
  • + {/each} +
+
+ {/if} + +