mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
1 line
13 KiB
JSON
1 line
13 KiB
JSON
[{"id":"ayzmwqh","name":"Resend","pinned":false,"avatar":"","url":"gh://sune-org/store@main/resend.sune","updatedAt":1764801821214,"settings":{"model":"google/gemini-3-pro-preview","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<div x-data=\"resendMaster()\" x-init=\"init()\" class=\"w-full max-w-md mx-auto bg-slate-50 border border-slate-200 rounded-xl shadow-sm overflow-hidden font-sans text-slate-800\">\n <!-- Header -->\n <div class=\"bg-white p-4 border-b border-slate-200 flex justify-between items-center\">\n <div class=\"flex items-center gap-2\">\n <div class=\"bg-black text-white p-1 rounded\"><i data-lucide=\"send\" class=\"w-4 h-4\"></i></div>\n <h2 class=\"font-bold text-sm tracking-tight\">Resend Master</h2>\n </div>\n <span class=\"text-[10px] font-mono text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded\" x-text=\"'v'+v\"></span>\n </div>\n\n <!-- Tabs -->\n <div class=\"flex border-b border-slate-200 bg-white\">\n <template x-for=\"t in ['setup', 'contacts', 'compose']\">\n <button @click=\"tab = t\" :class=\"{'text-black border-black': tab === t, 'text-slate-500 border-transparent': tab !== t}\" class=\"flex-1 py-3 text-xs font-bold border-b-2 capitalize transition-colors\" x-text=\"t\"></button>\n </template>\n </div>\n\n <!-- Content -->\n <div class=\"p-4 min-h-[320px]\">\n \n <!-- SETUP -->\n <div x-show=\"tab === 'setup'\" class=\"space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300\">\n <div class=\"bg-amber-50 border border-amber-200 rounded p-3\">\n <p class=\"text-[10px] text-amber-800 leading-tight\"><strong>Required:</strong> You must use a Cloudflare Worker proxy to avoid CORS errors. Paste your worker URL below.</p>\n </div>\n <div>\n <label class=\"block text-xs font-bold text-slate-700 mb-1\">Worker Proxy URL</label>\n <input type=\"text\" x-model=\"proxy\" @input=\"save\" class=\"w-full bg-white border border-slate-300 rounded px-3 py-2 text-xs font-mono text-slate-600 focus:ring-2 focus:ring-black/5 focus:border-black outline-none transition-all\" placeholder=\"https://my-worker.usr.workers.dev\">\n </div>\n <div>\n <label class=\"block text-xs font-bold text-slate-700 mb-1\">Resend API Key</label>\n <input type=\"password\" x-model=\"k\" @input=\"save\" class=\"w-full bg-white border border-slate-300 rounded px-3 py-2 text-sm focus:ring-2 focus:ring-black/5 focus:border-black outline-none transition-all\" placeholder=\"re_123...\">\n </div>\n <div>\n <label class=\"block text-xs font-bold text-slate-700 mb-1\">Audience ID (Optional)</label>\n <div class=\"flex gap-2\">\n <input type=\"text\" x-model=\"aid\" @input=\"save\" class=\"w-full bg-white border border-slate-300 rounded px-3 py-2 text-sm focus:ring-2 focus:ring-black/5 focus:border-black outline-none\" placeholder=\"aud_...\">\n <button @click=\"fetchAudience\" :disabled=\"loading\" class=\"bg-slate-100 text-slate-600 px-3 rounded border border-slate-200 hover:bg-slate-200 disabled:opacity-50\"><i data-lucide=\"download-cloud\" class=\"w-4 h-4\"></i></button>\n </div>\n </div>\n </div>\n\n <!-- CONTACTS -->\n <div x-show=\"tab === 'contacts'\" class=\"space-y-3 animate-in fade-in slide-in-from-bottom-2 duration-300\">\n <div class=\"flex justify-between items-center\">\n <span class=\"text-xs font-bold text-slate-500\" x-text=\"contacts.length + ' Contacts (' + selCount + ' selected)'\"></span>\n <div class=\"flex gap-2\">\n <button @click=\"toggleAll\" class=\"text-xs text-black hover:underline font-medium\">Toggle All</button>\n <button @click=\"clearContacts\" class=\"text-xs text-red-500 hover:underline font-medium\">Clear</button>\n </div>\n </div>\n \n <div class=\"flex gap-2\">\n <input type=\"email\" x-model=\"newC\" @keydown.enter=\"addC\" class=\"flex-1 border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-black outline-none\" placeholder=\"Add email manually...\">\n <button @click=\"addC\" class=\"bg-black text-white px-3 rounded hover:bg-slate-800\"><i data-lucide=\"plus\" class=\"w-4 h-4\"></i></button>\n </div>\n\n <div class=\"h-56 overflow-y-auto border border-slate-200 rounded bg-white divide-y divide-slate-100 scrollbar-thin\">\n <template x-for=\"c in contacts\" :key=\"c.email\">\n <div class=\"flex items-center p-2.5 hover:bg-slate-50 cursor-pointer group\" @click=\"c.sel = !c.sel\">\n <div :class=\"c.sel ? 'bg-black border-black' : 'bg-white border-slate-300'\" class=\"w-4 h-4 border rounded flex items-center justify-center mr-3 transition-colors\">\n <i x-show=\"c.sel\" data-lucide=\"check\" class=\"w-3 h-3 text-white\"></i>\n </div>\n <p class=\"text-sm font-medium truncate text-slate-700 group-hover:text-black\" x-text=\"c.email\"></p>\n </div>\n </template>\n <div x-show=\"contacts.length === 0\" class=\"p-8 text-center\">\n <i data-lucide=\"users\" class=\"w-8 h-8 text-slate-200 mx-auto mb-2\"></i>\n <p class=\"text-xs text-slate-400\">No contacts yet.</p>\n </div>\n </div>\n </div>\n\n <!-- COMPOSE -->\n <div x-show=\"tab === 'compose'\" class=\"space-y-3 animate-in fade-in slide-in-from-bottom-2 duration-300\">\n <div class=\"grid grid-cols-2 gap-2\">\n <div>\n <label class=\"block text-[10px] uppercase font-bold text-slate-400 mb-1\">From</label>\n <input type=\"text\" x-model=\"from\" @input=\"save\" class=\"w-full border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-black outline-none\" placeholder=\"me@domain.com\">\n </div>\n <div>\n <label class=\"block text-[10px] uppercase font-bold text-slate-400 mb-1\">Reply To</label>\n <input type=\"text\" x-model=\"reply\" @input=\"save\" class=\"w-full border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-black outline-none\">\n </div>\n </div>\n <div>\n <label class=\"block text-[10px] uppercase font-bold text-slate-400 mb-1\">Subject</label>\n <input type=\"text\" x-model=\"sub\" @input=\"save\" class=\"w-full border border-slate-300 rounded px-2 py-1.5 text-sm font-medium focus:border-black outline-none\">\n </div>\n <div>\n <label class=\"block text-[10px] uppercase font-bold text-slate-400 mb-1\">HTML Body</label>\n <textarea x-model=\"body\" @input=\"save\" class=\"w-full h-24 border border-slate-300 rounded px-2 py-1.5 text-xs font-mono resize-none focus:border-black outline-none\" placeholder=\"<h1>Hello</h1>\"></textarea>\n </div>\n \n <div class=\"pt-2\">\n <button @click=\"sendBatch\" :disabled=\"loading || selCount === 0\" class=\"w-full py-2.5 rounded-lg bg-black text-white font-bold text-sm shadow-lg shadow-slate-200 hover:bg-slate-800 hover:shadow-xl hover:-translate-y-0.5 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none flex justify-center items-center gap-2\">\n <span x-show=\"!loading\">Send to <span x-text=\"selCount\"></span> Recipients</span>\n <i x-show=\"loading\" data-lucide=\"loader-2\" class=\"w-4 h-4 animate-spin\"></i>\n </button>\n <p x-show=\"msg\" x-text=\"msg\" :class=\"err ? 'text-red-500' : 'text-emerald-600'\" class=\"text-xs text-center font-medium mt-2 animate-pulse\"></p>\n </div>\n </div>\n </div>\n</div>\n\n<script>\nfunction resendMaster() {\n return {\n v: '1.2.1',\n tab: 'setup',\n k: '', aid: '', proxy: '',\n from: '', reply: '', sub: '', body: '',\n contacts: [], newC: '',\n loading: false, msg: '', err: false,\n \n get selCount() { return this.contacts.filter(c => c.sel).length; },\n \n init() {\n const id = window.SUNE?.id || 'resend_master_v2';\n const s = JSON.parse(localStorage.getItem(id) || '{}');\n this.k = s.k || ''; this.aid = s.aid || ''; \n this.proxy = s.proxy || '';\n this.from = s.from || ''; this.reply = s.reply || '';\n this.sub = s.sub || ''; this.body = s.body || '';\n this.contacts = s.contacts || [];\n this.$nextTick(() => lucide.createIcons());\n },\n \n save() {\n const id = window.SUNE?.id || 'resend_master_v2';\n localStorage.setItem(id, JSON.stringify({\n k: this.k, aid: this.aid, proxy: this.proxy,\n from: this.from, reply: this.reply, sub: this.sub, body: this.body,\n contacts: this.contacts\n }));\n },\n\n addC() {\n if(!this.newC.includes('@')) return;\n this.contacts.unshift({email: this.newC, sel: true});\n this.newC = ''; this.save();\n this.$nextTick(() => lucide.createIcons());\n },\n \n clearContacts() { if(confirm('Clear all?')) { this.contacts = []; this.save(); } },\n \n toggleAll() {\n const allSel = this.contacts.every(c => c.sel);\n this.contacts.forEach(c => c.sel = !allSel);\n this.save();\n },\n\n // Helper to strip trailing slash from proxy\n get pUrl() { return this.proxy.replace(/\\/$/, ''); },\n\n async fetchAudience() {\n if(!this.k || !this.aid || !this.proxy) return this.notify('API Key, Audience ID & Proxy required', true);\n this.loading = true; this.msg = 'Fetching contacts...';\n try {\n const res = await fetch(`${this.pUrl}/audiences/${this.aid}/contacts`, {\n headers: { 'Authorization': `Bearer ${this.k}` }\n });\n if(!res.ok) throw new Error((await res.text()) || 'Fetch failed');\n const d = await res.json();\n if(d.data) {\n const exist = new Set(this.contacts.map(c => c.email));\n let added = 0;\n d.data.forEach(c => {\n if(!exist.has(c.email)) { this.contacts.push({email: c.email, sel: false}); added++; }\n });\n this.save();\n this.notify(`Added ${added} new contacts`);\n } else throw new Error(d.message || 'Failed');\n } catch(e) { this.notify(e.message, true); }\n this.loading = false;\n },\n\n async sendBatch() {\n if(!this.k || !this.from || !this.sub || !this.body || !this.proxy) return this.notify('Missing fields or proxy', true);\n this.loading = true; this.msg = 'Preparing batch...';\n \n const targets = this.contacts.filter(c => c.sel).map(c => c.email);\n const chunks = [];\n for (let i = 0; i < targets.length; i += 100) chunks.push(targets.slice(i, i + 100));\n\n try {\n let sent = 0;\n for(const chunk of chunks) {\n const payload = chunk.map(to => ({\n from: this.from, to: [to], subject: this.sub, html: this.body,\n reply_to: this.reply || undefined\n }));\n \n const res = await fetch(`${this.pUrl}/emails/batch`, {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${this.k}`, 'Content-Type': 'application/json' },\n body: JSON.stringify(payload)\n });\n if(!res.ok) throw new Error((await res.json()).message || 'Send failed');\n sent += chunk.length;\n }\n this.notify(`Success! Sent to ${sent} recipients.`);\n } catch(e) { this.notify(e.message, true); }\n this.loading = false;\n },\n\n notify(m, isErr=false) {\n this.msg = m; this.err = isErr;\n setTimeout(() => this.msg = '', 5000);\n }\n }\n}\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>","hide_composer":false,"include_thoughts":false,"json_output":false,"img_output":false,"ignore_master_prompt":false,"json_schema":""},"storage":{}}] |