+
diff --git a/src/components/ui/label/index.ts b/src/components/ui/label/index.ts
new file mode 100644
index 0000000..f81b2a4
--- /dev/null
+++ b/src/components/ui/label/index.ts
@@ -0,0 +1,8 @@
+import { Label as LabelPrimitive } from 'radix-vue'
+import { tv } from 'tailwind-variants'
+
+export const labelVariants = tv({
+ base: 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
+})
+
+export const Label = LabelPrimitive.Root
\ No newline at end of file
diff --git a/src/components/ui/scroll-area/ScrollArea.vue b/src/components/ui/scroll-area/ScrollArea.vue
new file mode 100644
index 0000000..f1181e0
--- /dev/null
+++ b/src/components/ui/scroll-area/ScrollArea.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/ui/scroll-area/ScrollBar.vue b/src/components/ui/scroll-area/ScrollBar.vue
new file mode 100644
index 0000000..3fd5b1b
--- /dev/null
+++ b/src/components/ui/scroll-area/ScrollBar.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/ui/scroll-area/index.ts b/src/components/ui/scroll-area/index.ts
new file mode 100644
index 0000000..54d33cf
--- /dev/null
+++ b/src/components/ui/scroll-area/index.ts
@@ -0,0 +1 @@
+export { default as ScrollArea } from './ScrollArea.vue'
\ No newline at end of file
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index a1b2f45..3e80b93 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -1,9 +1,10 @@
export default {
nav: {
+ title: 'Atitlán Directory',
home: 'Home',
directory: 'Directory',
faq: 'FAQ',
- title: 'Atitlán Directory'
+ support: 'Support'
},
home: {
title: 'Find Bitcoin Lightning Acceptors',
diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts
index a352c81..6306384 100644
--- a/src/i18n/locales/es.ts
+++ b/src/i18n/locales/es.ts
@@ -1,9 +1,10 @@
export default {
nav: {
+ title: 'Directorio Atitlán',
home: 'Inicio',
directory: 'Directorio',
faq: 'Preguntas',
- title: 'Directorio Atitlán'
+ support: 'Soporte'
},
home: {
title: 'Encuentra Aceptadores de Bitcoin Lightning',
diff --git a/src/lib/nostr-bundle.ts b/src/lib/nostr-bundle.ts
new file mode 100644
index 0000000..4da9833
--- /dev/null
+++ b/src/lib/nostr-bundle.ts
@@ -0,0 +1,67 @@
+import {
+ getPublicKey,
+ generateSecretKey,
+ nip04,
+ getEventHash,
+ finalizeEvent,
+ validateEvent,
+ nip19,
+ SimplePool,
+ type Event,
+ type Filter
+} from 'nostr-tools';
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
+
+// Expose NostrTools to the window object
+(window as any).NostrTools = {
+ getPublicKey,
+ generatePrivateKey: () => bytesToHex(generateSecretKey()),
+ nip04,
+ getEventHash,
+ getSignature: (event: Event, privateKey: string) => {
+ const signedEvent = finalizeEvent(event, hexToBytes(privateKey));
+ return signedEvent.sig;
+ },
+ signEvent: (event: Event, privateKey: string) => {
+ const signedEvent = finalizeEvent(event, hexToBytes(privateKey));
+ return signedEvent.sig;
+ },
+ verifySignature: validateEvent,
+ nip19,
+ relayInit: (url: string) => {
+ const pool = new SimplePool();
+ return {
+ connect: async () => {
+ await pool.ensureRelay(url);
+ return true;
+ },
+ sub: (filters: Filter[]) => {
+ return {
+ on: (type: string, callback: (event: Event) => void) => {
+ if (type === 'event') {
+ void pool.subscribeMany(
+ [url],
+ filters,
+ { onevent: callback }
+ );
+ }
+ }
+ };
+ },
+ publish: (event: Event) => {
+ return {
+ on: (type: string, cb: (msg?: string) => void) => {
+ if (type === 'ok') {
+ Promise.all(pool.publish([url], event))
+ .then(() => cb())
+ .catch((err: Error) => cb(err.message));
+ }
+ }
+ };
+ },
+ close: () => {
+ pool.close([url]);
+ }
+ };
+ }
+};
\ No newline at end of file
diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts
new file mode 100644
index 0000000..26d004a
--- /dev/null
+++ b/src/lib/nostr.ts
@@ -0,0 +1,108 @@
+import type { NostrEvent, NostrRelayConfig } from '../types/nostr'
+
+declare global {
+ interface Window {
+ NostrTools: {
+ getPublicKey: (privkey: string) => string
+ generatePrivateKey: () => string
+ nip04: {
+ encrypt: (privkey: string, pubkey: string, content: string) => Promise
+ decrypt: (privkey: string, pubkey: string, content: string) => Promise
+ }
+ getEventHash: (event: NostrEvent) => string
+ signEvent: (event: NostrEvent, privkey: string) => Promise
+ getSignature: (event: NostrEvent, privkey: string) => string
+ verifySignature: (event: NostrEvent) => boolean
+ nip19: {
+ decode: (str: string) => { type: string; data: string }
+ npubEncode: (hex: string) => string
+ }
+ relayInit: (url: string) => {
+ connect: () => Promise
+ sub: (filters: any[]) => {
+ on: (event: string, callback: (event: NostrEvent) => void) => void
+ }
+ publish: (event: NostrEvent) => {
+ on: (type: 'ok' | 'failed', cb: (msg?: string) => void) => void
+ }
+ close: () => void
+ }
+ }
+ }
+}
+
+export async function connectToRelay(url: string) {
+ const relay = window.NostrTools.relayInit(url)
+ try {
+ await relay.connect()
+ return relay
+ } catch (err) {
+ console.error(`Failed to connect to ${url}:`, err)
+ return null
+ }
+}
+
+export async function publishEvent(event: NostrEvent, relays: NostrRelayConfig[]) {
+ const connectedRelays = await Promise.all(
+ relays.map(relay => connectToRelay(relay.url))
+ )
+
+ const activeRelays = connectedRelays.filter(relay => relay !== null)
+
+ return Promise.all(
+ activeRelays.map(relay =>
+ new Promise((resolve) => {
+ const pub = relay.publish(event)
+ pub.on('ok', () => {
+ resolve(true)
+ })
+ pub.on('failed', () => {
+ resolve(false)
+ })
+ })
+ )
+ )
+}
+
+export async function encryptMessage(privkey: string, pubkey: string, content: string): Promise {
+ return await window.NostrTools.nip04.encrypt(privkey, pubkey, content)
+}
+
+export async function decryptMessage(privkey: string, pubkey: string, content: string): Promise {
+ return await window.NostrTools.nip04.decrypt(privkey, pubkey, content)
+}
+
+export function generatePrivateKey(): string {
+ return window.NostrTools.generatePrivateKey()
+}
+
+export function getPublicKey(privateKey: string): string {
+ return window.NostrTools.getPublicKey(privateKey)
+}
+
+export function getEventHash(event: NostrEvent): string {
+ return window.NostrTools.getEventHash(event)
+}
+
+export async function signEvent(event: NostrEvent, privateKey: string): Promise {
+ return window.NostrTools.getSignature(event, privateKey)
+}
+
+export function verifySignature(event: NostrEvent): boolean {
+ return window.NostrTools.verifySignature(event)
+}
+
+export function npubToHex(npub: string): string {
+ try {
+ const { type, data } = window.NostrTools.nip19.decode(npub)
+ if (type !== 'npub') throw new Error('Invalid npub')
+ return data
+ } catch (err) {
+ console.error('Failed to decode npub:', err)
+ throw err
+ }
+}
+
+export function hexToNpub(hex: string): string {
+ return window.NostrTools.nip19.npubEncode(hex)
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index bd0f277..4fae542 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,13 +1,18 @@
import { createApp } from 'vue'
+import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { i18n } from './i18n'
import './assets/index.css'
import { registerSW } from 'virtual:pwa-register'
+import './lib/nostr-bundle'
const app = createApp(App)
+const pinia = createPinia()
+
app.use(router)
app.use(i18n)
+app.use(pinia)
// Simple periodic service worker updates
const intervalMS = 60 * 60 * 1000 // 1 hour
diff --git a/src/pages/Support.vue b/src/pages/Support.vue
new file mode 100644
index 0000000..f8b546d
--- /dev/null
+++ b/src/pages/Support.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
Customer Support
+
+ Chat with our support team. We're here to help!
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/router/index.ts b/src/router/index.ts
index 9e69a33..8702666 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'
import Directory from '@/pages/Directory.vue'
import FAQ from '@/pages/FAQ.vue'
+import Support from '@/pages/Support.vue'
const router = createRouter({
history: createWebHistory(),
@@ -26,6 +27,11 @@ const router = createRouter({
name: 'directory-item',
component: () => import('@/pages/DirectoryItem.vue'),
props: true
+ },
+ {
+ path: '/support',
+ name: 'support',
+ component: Support
}
]
})
diff --git a/src/stores/nostr.ts b/src/stores/nostr.ts
new file mode 100644
index 0000000..946d261
--- /dev/null
+++ b/src/stores/nostr.ts
@@ -0,0 +1,325 @@
+import { defineStore } from 'pinia'
+import { ref, computed, watch } from 'vue'
+import type { NostrEvent, NostrProfile, NostrAccount, DirectMessage } from '../types/nostr'
+
+declare global {
+ interface Window {
+ NostrTools: {
+ getPublicKey: (privkey: string) => string
+ generatePrivateKey: () => string
+ nip04: {
+ encrypt: (privkey: string, pubkey: string, content: string) => Promise
+ decrypt: (privkey: string, pubkey: string, content: string) => Promise
+ }
+ getEventHash: (event: NostrEvent) => string
+ signEvent: (event: NostrEvent, privkey: string) => Promise
+ getSignature: (event: NostrEvent, privkey: string) => string
+ verifySignature: (event: NostrEvent) => boolean
+ nip19: {
+ decode: (str: string) => { type: string; data: string }
+ npubEncode: (hex: string) => string
+ }
+ relayInit: (url: string) => {
+ connect: () => Promise
+ sub: (filters: any[]) => {
+ on: (event: string, callback: (event: NostrEvent) => void) => void
+ }
+ publish: (event: NostrEvent) => {
+ on: (type: 'ok' | 'failed', cb: (msg?: string) => void) => void
+ }
+ close: () => void
+ }
+ }
+ }
+}
+
+const DEFAULT_RELAYS = [
+ 'wss://nostr.atitlan.io'
+]
+
+// Get support agent's public key from environment variable
+const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
+
+// Helper functions
+async function connectToRelay(url: string) {
+ const relay = window.NostrTools.relayInit(url)
+ try {
+ await relay.connect()
+ return relay
+ } catch (err) {
+ console.error(`Failed to connect to ${url}:`, err)
+ return null
+ }
+}
+
+async function publishEvent(event: NostrEvent, relays: { url: string }[]) {
+ const promises = relays.map(async ({ url }) => {
+ const relay = window.NostrTools.relayInit(url)
+ try {
+ await relay.connect()
+ const pub = relay.publish(event)
+ return new Promise((resolve, reject) => {
+ pub.on('ok', () => resolve(true))
+ pub.on('failed', reject)
+ })
+ } catch (err) {
+ console.error(`Failed to publish to ${url}:`, err)
+ return false
+ }
+ })
+
+ await Promise.all(promises)
+}
+
+export const useNostrStore = defineStore('nostr', () => {
+ // State
+ const account = ref(JSON.parse(localStorage.getItem('nostr_account') || 'null'))
+ const profiles = ref