Skip to content

Commit 324bd2a

Browse files
add share button (#8)
1 parent 35f67ce commit 324bd2a

File tree

2 files changed

+58
-2
lines changed

2 files changed

+58
-2
lines changed

src/components/App.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
1+
import { useEffect } from 'react';
12
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
23
import { HexViewer } from './HexViewer/HexViewer';
34
import { AstTree } from './AstTree/AstTree';
4-
import { QueryInput } from './QueryInput';
5+
import { QueryInput, decodeBase64Url } from './QueryInput';
6+
import { useStore } from '../store/store';
7+
import { ClickHouseFormat } from '../core/types/formats';
58
import logo from '../assets/clickhouse-yellow-badge.svg';
69
import '../styles/app.css';
710

811
function App() {
12+
const setQuery = useStore((s) => s.setQuery);
13+
const setFormat = useStore((s) => s.setFormat);
14+
15+
useEffect(() => {
16+
const params = new URLSearchParams(window.location.search);
17+
const q = params.get('q');
18+
const f = params.get('f');
19+
20+
if (q) {
21+
try {
22+
setQuery(decodeBase64Url(q));
23+
} catch {
24+
// ignore malformed base64
25+
}
26+
}
27+
if (f && Object.values(ClickHouseFormat).includes(f as ClickHouseFormat)) {
28+
setFormat(f as ClickHouseFormat);
29+
}
30+
31+
if (q || f) {
32+
window.history.replaceState({}, '', window.location.pathname);
33+
}
34+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
35+
936
return (
1037
<div className="app">
1138
<header className="app-header">

src/components/QueryInput.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
import { useCallback, useRef } from 'react';
1+
import { useCallback, useRef, useState } from 'react';
22
import { useStore } from '../store/store';
33
import { DEFAULT_QUERY } from '../core/clickhouse/client';
44
import { ClickHouseFormat, FORMAT_METADATA } from '../core/types/formats';
55

6+
function encodeBase64Url(str: string): string {
7+
const bytes = new TextEncoder().encode(str);
8+
const binary = String.fromCharCode(...bytes);
9+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
10+
}
11+
12+
function decodeBase64Url(encoded: string): string {
13+
const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
14+
const binary = atob(base64);
15+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
16+
return new TextDecoder().decode(bytes);
17+
}
18+
19+
export { encodeBase64Url, decodeBase64Url };
20+
621
export function QueryInput() {
722
const query = useStore((s) => s.query);
823
const setQuery = useStore((s) => s.setQuery);
@@ -15,6 +30,17 @@ export function QueryInput() {
1530
const queryTiming = useStore((s) => s.queryTiming);
1631

1732
const fileInputRef = useRef<HTMLInputElement>(null);
33+
const [shareLabel, setShareLabel] = useState('Share');
34+
35+
const handleShare = useCallback(() => {
36+
const url = new URL(window.location.href);
37+
url.search = '';
38+
url.searchParams.set('q', encodeBase64Url(query));
39+
url.searchParams.set('f', format);
40+
navigator.clipboard.writeText(url.toString());
41+
setShareLabel('Copied!');
42+
setTimeout(() => setShareLabel('Share'), 2000);
43+
}, [query, format]);
1844

1945
const handleExecute = useCallback(() => {
2046
executeQuery();
@@ -94,6 +120,9 @@ export function QueryInput() {
94120
>
95121
Upload
96122
</button>
123+
<button className="query-btn secondary" onClick={handleShare} title="Copy shareable URL to clipboard">
124+
{shareLabel}
125+
</button>
97126
<button className="query-btn secondary" onClick={handleReset} disabled={isLoading}>
98127
Reset
99128
</button>

0 commit comments

Comments
 (0)