Files
everclone/src/components/HooksModal.tsx
T

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>&ldquo;Clip to Everclone&rdquo;</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>&ldquo;Add to Home screen / Install app&rdquo;</strong>. Everclone will appear directly inside Android&apos;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&apos;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>
);
}