- 5 SVG placeholder product images (minimal dark style with watermark initials) - Seed data updated to reference .svg placeholders - Nostr DM inbox in admin (Messages tab) with shop npub display - GET /api/admin/nostr-info endpoint for shop pubkey - My Orders page: customers look up orders by NIP-07 Nostr identity - GET /api/orders/by-pubkey/:pubkey endpoint with hex validation - SeoMeta component for OG/Twitter meta tags - SEO meta on HomeView and ProductView - Base OG meta tags in index.html - "My Orders" link in shop header nav - Splash logo doubled in size on desktop (680px max) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
92 lines
3.4 KiB
Vue
92 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { api } from '@/api/client'
|
|
import { useNostr } from '@/composables/useNostr'
|
|
import type { Order } from '@shared/types'
|
|
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
|
import SatsDisplay from '@/components/ui/SatsDisplay.vue'
|
|
import GlassButton from '@/components/ui/GlassButton.vue'
|
|
import LoadingSpinner from '@/components/ui/LoadingSpinner.vue'
|
|
|
|
const { pubkey, hasExtension, connectExtension } = useNostr()
|
|
const orders = ref<Order[]>([])
|
|
const isLoading = ref(false)
|
|
|
|
async function fetchOrders() {
|
|
if (!pubkey.value) return
|
|
isLoading.value = true
|
|
try {
|
|
const res = await api.get(`/api/orders/by-pubkey/${pubkey.value}`)
|
|
if (res.ok) orders.value = await api.json<Order[]>(res)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (pubkey.value) fetchOrders()
|
|
})
|
|
|
|
async function handleConnect() {
|
|
await connectExtension()
|
|
if (pubkey.value) fetchOrders()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="my-orders">
|
|
<h1>My Orders</h1>
|
|
<p class="subtitle">Look up your orders using your Nostr identity.</p>
|
|
|
|
<div v-if="!pubkey" class="connect-prompt glass-card">
|
|
<p>Connect your Nostr identity to view your order history.</p>
|
|
<div class="actions">
|
|
<GlassButton v-if="hasExtension" @click="handleConnect">Connect Extension (NIP-07)</GlassButton>
|
|
<p v-else class="hint">Install a NIP-07 browser extension (like nos2x or Alby) to connect.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<LoadingSpinner v-if="isLoading" />
|
|
<div v-else-if="orders.length === 0" class="empty">
|
|
<p>No orders found for this Nostr identity.</p>
|
|
</div>
|
|
<div v-else class="orders-list">
|
|
<router-link
|
|
v-for="order in orders"
|
|
:key="order.id"
|
|
:to="`/order/${order.id}`"
|
|
class="order-row glass-card"
|
|
>
|
|
<div class="order-meta">
|
|
<code class="order-id">{{ order.id.slice(0, 12) }}...</code>
|
|
<time>{{ new Date(order.createdAt).toLocaleDateString() }}</time>
|
|
</div>
|
|
<div class="order-info">
|
|
<span>{{ order.items.reduce((s, i) => s + i.quantity, 0) }} items</span>
|
|
<SatsDisplay :sats="order.totalSats" />
|
|
<StatusBadge :status="order.status" />
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
h1 { font-size: 1.75rem; font-weight: 700; margin-bottom: 0.25rem; }
|
|
.subtitle { color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem; }
|
|
.connect-prompt { text-align: center; }
|
|
.connect-prompt p { color: var(--text-secondary); margin-bottom: 1rem; }
|
|
.actions { display: flex; justify-content: center; gap: 0.75rem; }
|
|
.hint { color: var(--text-muted); font-size: 0.8125rem; }
|
|
.empty { text-align: center; padding: 3rem 0; color: var(--text-muted); }
|
|
.orders-list { display: flex; flex-direction: column; gap: 1rem; }
|
|
.order-row { display: flex; justify-content: space-between; align-items: center; text-decoration: none; color: inherit; cursor: pointer; }
|
|
.order-row:hover { border-color: var(--glass-highlight); transform: translateY(-1px); }
|
|
.order-meta { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
.order-id { font-size: 0.8125rem; color: var(--accent); }
|
|
.order-meta time { font-size: 0.75rem; color: var(--text-muted); }
|
|
.order-info { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; }
|
|
</style>
|