380 lines
15 KiB
TypeScript
380 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Key,
|
|
Smartphone,
|
|
Bookmark,
|
|
Terminal,
|
|
Plus,
|
|
Trash2,
|
|
Copy,
|
|
Check,
|
|
Download,
|
|
X,
|
|
} from 'lucide-react';
|
|
|
|
interface ApiKeyRecord {
|
|
id: string;
|
|
name: string;
|
|
keyPrefix: string;
|
|
lastUsedAt: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface HooksModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function HooksModal({ open, onClose }: HooksModalProps) {
|
|
const [keys, setKeys] = useState<ApiKeyRecord[]>([]);
|
|
const [newKeyName, setNewKeyName] = useState('Android & Web Clipper');
|
|
const [generatedKey, setGeneratedKey] = useState<string>('');
|
|
const [copied, setCopied] = useState<string>('');
|
|
|
|
const origin =
|
|
typeof window !== 'undefined' ? window.location.origin : 'https://everclone.vloser.us';
|
|
|
|
const loadKeys = async () => {
|
|
try {
|
|
const res = await fetch('/api/keys');
|
|
const data = await res.json();
|
|
if (data.keys) setKeys(data.keys);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
loadKeys();
|
|
}
|
|
}, [open]);
|
|
|
|
if (!open) return null;
|
|
|
|
const handleCreateKey = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const res = await fetch('/api/keys', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: newKeyName }),
|
|
});
|
|
const data = await res.json();
|
|
if (data.rawKey) {
|
|
setGeneratedKey(data.rawKey);
|
|
loadKeys();
|
|
}
|
|
};
|
|
|
|
const handleDeleteKey = async (id: string) => {
|
|
await fetch(`/api/keys?id=${id}`, { method: 'DELETE' });
|
|
loadKeys();
|
|
};
|
|
|
|
const copyText = (text: string, label: string) => {
|
|
navigator.clipboard.writeText(text);
|
|
setCopied(label);
|
|
setTimeout(() => setCopied(''), 2000);
|
|
};
|
|
|
|
const activeToken = generatedKey || '<YOUR_API_KEY>';
|
|
|
|
const downloadAndroidHttpShortcuts = () => {
|
|
const shortcutsJson = {
|
|
version: 60,
|
|
categories: [
|
|
{
|
|
id: 'everclone-cat-1',
|
|
name: 'Everclone',
|
|
layoutType: 'linear_list',
|
|
shortcuts: [
|
|
{
|
|
id: 'everclone-share-url',
|
|
name: 'Clip Link / Text to Everclone',
|
|
description: 'Shares any URL or text selection from Android Share sheet to Everclone',
|
|
method: 'POST',
|
|
url: `${origin}/api/hooks/note`,
|
|
headers: [
|
|
{ key: 'Authorization', value: `Bearer ${activeToken}` },
|
|
{ key: 'Content-Type', value: 'application/json' },
|
|
],
|
|
bodyContent: JSON.stringify({
|
|
title: '{share_title}',
|
|
content: '{share_text}',
|
|
tags: ['android'],
|
|
}),
|
|
requestBodyType: 'custom_text',
|
|
launcherShortcut: true,
|
|
quickSettingsTileShortcut: true,
|
|
},
|
|
{
|
|
id: 'everclone-quick-note',
|
|
name: 'Quick Note to Everclone',
|
|
description: 'Prompts for a quick note and saves it to Everclone Inbox',
|
|
method: 'POST',
|
|
url: `${origin}/api/hooks/note`,
|
|
headers: [
|
|
{ key: 'Authorization', value: `Bearer ${activeToken}` },
|
|
{ key: 'Content-Type', value: 'application/json' },
|
|
],
|
|
bodyContent: JSON.stringify({
|
|
title: '{note_title}',
|
|
markdown: '{note_body}',
|
|
tags: ['quick-note', 'android'],
|
|
}),
|
|
requestBodyType: 'custom_text',
|
|
launcherShortcut: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
variables: [
|
|
{
|
|
id: 'var-share-title',
|
|
key: 'share_title',
|
|
type: 'constant',
|
|
value: '',
|
|
isShareTitle: true,
|
|
},
|
|
{
|
|
id: 'var-share-text',
|
|
key: 'share_text',
|
|
type: 'constant',
|
|
value: '',
|
|
isShareText: true,
|
|
},
|
|
{
|
|
id: 'var-note-title',
|
|
key: 'note_title',
|
|
type: 'text',
|
|
title: 'Note Title',
|
|
Flags: 0,
|
|
},
|
|
{
|
|
id: 'var-note-body',
|
|
key: 'note_body',
|
|
type: 'text',
|
|
title: 'Note Content (Markdown)',
|
|
isMultiline: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(shortcutsJson, null, 2)], {
|
|
type: 'application/json',
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'everclone-android-http-shortcuts.json';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const bookmarkletHref = `javascript:(function(){var s=window.getSelection?window.getSelection().toString():'';var u=encodeURIComponent(location.href);var t=encodeURIComponent(document.title);var x=encodeURIComponent(s);window.open('${origin}/api/hooks/note?redirect=1&apiKey=${encodeURIComponent(
|
|
activeToken
|
|
)}&url='+u+'&title='+t+'&text='+x,'_blank');})();`;
|
|
|
|
const curlSnippet = `curl -X POST "${origin}/api/hooks/note" \\
|
|
-H "Authorization: Bearer ${activeToken}" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{"title":"Idea from Android","markdown":"- [ ] Check this out","notebook":"Inbox","tags":["mobile","idea"]}'`;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
|
<div className="bg-white rounded-xl shadow-2xl w-full max-w-3xl overflow-hidden flex flex-col max-h-[90vh]">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
<div className="flex items-center gap-2.5">
|
|
<Smartphone className="w-5 h-5 text-evernote-green" />
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Web & Android Note Ingestion Hooks
|
|
</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-200"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 overflow-y-auto space-y-6">
|
|
{/* 1. API Keys Section */}
|
|
<section className="space-y-3">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
|
<Key className="w-4 h-4 text-evernote-green" />
|
|
<span>Step 1: Generate an API Key for Hooks</span>
|
|
</div>
|
|
|
|
<form onSubmit={handleCreateKey} className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={newKeyName}
|
|
onChange={(e) => setNewKeyName(e.target.value)}
|
|
placeholder="Key description (e.g. Pixel 9 Pro / Bookmarklet)"
|
|
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-evernote-green"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-evernote-green hover:bg-evernote-greenHover rounded-lg shrink-0"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Generate Key</span>
|
|
</button>
|
|
</form>
|
|
|
|
{generatedKey && (
|
|
<div className="p-3.5 bg-emerald-50 border border-emerald-300 rounded-lg flex items-center justify-between gap-2">
|
|
<div className="text-xs">
|
|
<div className="font-semibold text-emerald-900">
|
|
New API Key Created (automatically embedded in snippets below):
|
|
</div>
|
|
<code className="text-emerald-800 font-mono break-all select-all">
|
|
{generatedKey}
|
|
</code>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyText(generatedKey, 'key')}
|
|
className="flex items-center gap-1 px-3 py-1.5 bg-white border border-emerald-300 rounded text-xs font-medium text-emerald-800 hover:bg-emerald-100 shrink-0"
|
|
>
|
|
{copied === 'key' ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
|
<span>{copied === 'key' ? 'Copied' : 'Copy'}</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{keys.length > 0 && (
|
|
<div className="border border-gray-200 rounded-lg divide-y divide-gray-100 text-xs">
|
|
{keys.map((k) => (
|
|
<div key={k.id} className="flex items-center justify-between px-3 py-2">
|
|
<div>
|
|
<span className="font-medium text-gray-800">{k.name}</span>
|
|
<span className="ml-2 font-mono text-gray-400">{k.keyPrefix}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-gray-400">
|
|
{k.lastUsedAt
|
|
? `Last used ${new Date(k.lastUsedAt).toLocaleDateString()}`
|
|
: 'Never used'}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDeleteKey(k.id)}
|
|
className="text-red-500 hover:text-red-700 p-1"
|
|
title="Revoke Key"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* 2. Android Integration */}
|
|
<section className="space-y-2.5 border-t border-gray-200 pt-5">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
|
<Smartphone className="w-4 h-4 text-evernote-green" />
|
|
<span>Step 2: Add Notes from Android (Share Sheet, HTTP Shortcuts & PWA)</span>
|
|
</div>
|
|
<p className="text-xs text-gray-600 leading-relaxed">
|
|
You can send links, articles, text, voice notes, or photos from Android in two ways:
|
|
</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="p-3.5 bg-gray-50 border border-gray-200 rounded-lg space-y-2">
|
|
<div className="text-xs font-semibold text-gray-800">
|
|
Option A: Android HTTP Shortcuts / Tasker
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
Import this file into the free <strong>HTTP Shortcuts</strong> Android app to add{' '}
|
|
<em>“Clip to Everclone”</em> to your Android Share menu & home screen widgets.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={downloadAndroidHttpShortcuts}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-evernote-green text-white rounded-lg text-xs font-medium hover:bg-evernote-greenHover"
|
|
>
|
|
<Download className="w-3.5 h-3.5" />
|
|
<span>Download Android Shortcuts (.json)</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-3.5 bg-gray-50 border border-gray-200 rounded-lg space-y-2">
|
|
<div className="text-xs font-semibold text-gray-800">
|
|
Option B: Install PWA Share Target
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
Open <code>{origin}</code> in Chrome on Android and tap{' '}
|
|
<strong>“Add to Home screen / Install app”</strong>. Everclone will appear directly inside Android's native Share sheet!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 3. Browser Web Clipper Bookmarklet */}
|
|
<section className="space-y-2.5 border-t border-gray-200 pt-5">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
|
<Bookmark className="w-4 h-4 text-evernote-green" />
|
|
<span>Step 3: Web Clipper Bookmarklet</span>
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
Drag the green button below to your browser's Bookmarks Bar. Click it on any web page to clip the full readable article (or highlighted text) directly into Everclone:
|
|
</p>
|
|
<div className="flex items-center gap-3">
|
|
<a
|
|
href={bookmarkletHref}
|
|
onClick={(e) => e.preventDefault()}
|
|
draggable
|
|
className="inline-flex items-center gap-1.5 px-4 py-2 bg-evernote-green text-white text-xs font-semibold rounded-lg shadow hover:bg-evernote-greenHover cursor-grab"
|
|
>
|
|
<span>📎 Clip to Everclone</span>
|
|
</a>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyText(bookmarkletHref, 'bookmarklet')}
|
|
className="text-xs text-gray-600 hover:text-gray-900 underline flex items-center gap-1"
|
|
>
|
|
{copied === 'bookmarklet' ? (
|
|
<>
|
|
<Check className="w-3.5 h-3.5 text-evernote-green" /> Copied Bookmarklet URL
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-3.5 h-3.5" /> Copy Bookmarklet Code
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 4. REST API / cURL Reference */}
|
|
<section className="space-y-2 border-t border-gray-200 pt-5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
|
<Terminal className="w-4 h-4 text-evernote-green" />
|
|
<span>REST API Webhook Reference (POST /api/hooks/note)</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => copyText(curlSnippet, 'curl')}
|
|
className="flex items-center gap-1 text-xs text-evernote-green hover:underline font-medium"
|
|
>
|
|
{copied === 'curl' ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
|
<span>{copied === 'curl' ? 'Copied!' : 'Copy cURL'}</span>
|
|
</button>
|
|
</div>
|
|
<pre className="p-3 bg-slate-900 text-slate-100 rounded-lg text-xs font-mono overflow-x-auto select-all">
|
|
{curlSnippet}
|
|
</pre>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|