Compare commits

..

3 commits

Author SHA1 Message Date
b9bca36b50 feat(activities): edit button on activity detail page
The NIP-52 d-tag we publish for an event is the LNbits event id (set
in nostr_publisher.build_nip52_event), so a single fetchMyEvents call
can tell us whether the displayed activity belongs to the caller.
When it does, show an Edit button next to Bookmark; clicking sets
the store's editingEvent and opens the shell-mounted dialog in edit
mode.

This was the missing surface — users land on /activities/:id when
they tap a posting, not on /events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:01:55 +02:00
345ca073af feat(activities-app): wire shell dialog for edit + approval probes
Probe isAdmin / autoApprove once at auth-ready (re-probe on login)
and feed them plus the store's editingEvent into the shell-mounted
CreateEventDialog. Add handleUpdateEvent that picks the right wallet
admin key from the editing event's wallet id.

Without this the Activities standalone app could only Create — the
existing dialog was create-only at shell level even though the
dialog component itself already supported edit mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:01:55 +02:00
a77bf7ff6c feat(activities): editingEvent in activities store
Carry the LNbits event being edited at store level so the
shell-mounted CreateEventDialog (activities-app/App.vue) can open in
edit mode from anywhere a user surfaces their own event — most
importantly the activity detail page, which is where they actually
land when fixing a posting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:01:55 +02:00
3 changed files with 108 additions and 9 deletions

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { CalendarDays, Map, Heart, Search, Plus } from 'lucide-vue-next' import { CalendarDays, Map, Heart, Search, Plus } from 'lucide-vue-next'
@ -17,6 +17,26 @@ const { t } = useI18n()
const { isAuthenticated, currentUser } = useAuth() const { isAuthenticated, currentUser } = useAuth()
const activitiesStore = useActivitiesStore() const activitiesStore = useActivitiesStore()
// Probe LNbits admin status + extension auto_approve once at auth-ready
// so the shell-mounted dialog renders the right warning copy when an
// owner edits their own event.
const isAdmin = ref(false)
const autoApprove = ref(false)
async function probeApprovalState() {
if (!isAuthenticated.value) return
const wallet = currentUser.value?.wallets?.[0]
if (!wallet?.inkey) return
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
autoApprove.value = await ticketApi.getAutoApprove(wallet.inkey)
if (wallet.adminkey) {
isAdmin.value = await ticketApi.isAdmin(wallet.adminkey)
}
}
onMounted(probeApprovalState)
watch(isAuthenticated, (yes) => {
if (yes) probeApprovalState()
})
// Settings dropped theme/lang/currency now live in the shared profile sheet. // Settings dropped theme/lang/currency now live in the shared profile sheet.
// Create lives in the bottom nav (auth-gated): activity creation is a deliberate // Create lives in the bottom nav (auth-gated): activity creation is a deliberate
// act, surfacing it as a tab keeps it one tap away when authed and out of the // act, surfacing it as a tab keeps it one tap away when authed and out of the
@ -57,14 +77,38 @@ async function handleCreateEvent(eventData: CreateEventRequest) {
if (!invoiceKey) throw new Error('No wallet available. Please log in first.') if (!invoiceKey) throw new Error('No wallet available. Please log in first.')
await ticketApi.createEvent(eventData, invoiceKey) await ticketApi.createEvent(eventData, invoiceKey)
} }
async function handleUpdateEvent(eventId: string, eventData: CreateEventRequest) {
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
// PUT /events/{id} requires the event's wallet admin key.
const wallet = (currentUser.value?.wallets ?? []).find(
(w) => w.id === activitiesStore.editingEvent?.wallet,
)
const adminKey = wallet?.adminkey
if (!adminKey) {
throw new Error("Can't find the admin key for this event's wallet.")
}
await ticketApi.updateEvent(eventId, eventData, adminKey)
}
function handleDialogOpenChange(open: boolean) {
activitiesStore.showCreateDialog = open
// Closing always clears the edit selection so the next "+ Create"
// opens clean instead of inheriting the last-edited event.
if (!open) activitiesStore.editingEvent = null
}
</script> </script>
<template> <template>
<AppShell :tabs="tabs" :is-active="isActive"> <AppShell :tabs="tabs" :is-active="isActive">
<CreateEventDialog <CreateEventDialog
:open="activitiesStore.showCreateDialog" :open="activitiesStore.showCreateDialog"
@update:open="activitiesStore.showCreateDialog = $event" :event="activitiesStore.editingEvent"
:is-admin="isAdmin"
:auto-approve="autoApprove"
:on-create-event="handleCreateEvent" :on-create-event="handleCreateEvent"
:on-update-event="handleUpdateEvent"
@update:open="handleDialogOpenChange"
/> />
</AppShell> </AppShell>
</template> </template>

View file

@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { Activity } from '../types/activity' import type { Activity } from '../types/activity'
import type { TicketedEvent } from '../types/ticket'
/** /**
* Pinia store for cached activities from Nostr relays. * Pinia store for cached activities from Nostr relays.
@ -14,6 +15,9 @@ export const useActivitiesStore = defineStore('activities', () => {
/** Toggle by the standalone bottom-nav Create tab; mounted dialog lives /** Toggle by the standalone bottom-nav Create tab; mounted dialog lives
* in activities-app/App.vue so it's available from every route. */ * in activities-app/App.vue so it's available from every route. */
const showCreateDialog = ref(false) const showCreateDialog = ref(false)
/** When set, the shell-mounted CreateEventDialog opens in edit mode
* for this LNbits event. Cleared when the dialog closes. */
const editingEvent = ref<TicketedEvent | null>(null)
// Computed // Computed
const activities = computed(() => Array.from(activitiesMap.value.values())) const activities = computed(() => Array.from(activitiesMap.value.values()))
@ -88,6 +92,7 @@ export const useActivitiesStore = defineStore('activities', () => {
isLoading, isLoading,
lastUpdated, lastUpdated,
showCreateDialog, showCreateDialog,
editingEvent,
// Computed // Computed
activities, activities,

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { format } from 'date-fns' import { format } from 'date-fns'
@ -8,13 +8,18 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { import {
Calendar, MapPin, ArrowLeft, Calendar, MapPin, ArrowLeft, Pencil,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useActivityDetail } from '../composables/useActivityDetail' import { useActivityDetail } from '../composables/useActivityDetail'
import BookmarkButton from '../components/BookmarkButton.vue' import BookmarkButton from '../components/BookmarkButton.vue'
import RSVPButton from '../components/RSVPButton.vue' import RSVPButton from '../components/RSVPButton.vue'
import OrganizerCard from '../components/OrganizerCard.vue' import OrganizerCard from '../components/OrganizerCard.vue'
import { NIP52_KINDS } from '../types/nip52' import { NIP52_KINDS } from '../types/nip52'
import { useAuth } from '@/composables/useAuthService'
import { useActivitiesStore } from '../stores/activities'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { TicketApiService } from '../services/TicketApiService'
import type { TicketedEvent } from '../types/ticket'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -24,6 +29,38 @@ const activityId = route.params.id as string
const { activity, isLoading, error, reload } = useActivityDetail(activityId) const { activity, isLoading, error, reload } = useActivityDetail(activityId)
const { dateLocale } = useDateLocale() const { dateLocale } = useDateLocale()
// Owner-edit affordance: the NIP-52 d-tag we use for the activity id is
// the same as the LNbits event id (set at publish time in
// nostr_publisher.build_nip52_event). Look the user's own events up
// once and offer an Edit button on a match.
const { isAuthenticated, currentUser } = useAuth()
const activitiesStore = useActivitiesStore()
const ownedLnbitsEvent = ref<TicketedEvent | null>(null)
async function loadOwnedEvent() {
ownedLnbitsEvent.value = null
if (!isAuthenticated.value) return
const invoiceKey = currentUser.value?.wallets?.[0]?.inkey
if (!invoiceKey) return
try {
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
const mine = await ticketApi.fetchMyEvents(invoiceKey)
ownedLnbitsEvent.value =
(mine as TicketedEvent[]).find((e) => e.id === activityId) ?? null
} catch {
ownedLnbitsEvent.value = null
}
}
onMounted(loadOwnedEvent)
watch(isAuthenticated, () => loadOwnedEvent())
function openEditDialog() {
if (!ownedLnbitsEvent.value) return
activitiesStore.editingEvent = ownedLnbitsEvent.value
activitiesStore.showCreateDialog = true
}
const dateDisplay = computed(() => { const dateDisplay = computed(() => {
if (!activity.value) return '' if (!activity.value) return ''
const a = activity.value const a = activity.value
@ -60,11 +97,24 @@ function goBack() {
<ArrowLeft class="w-4 h-4" /> <ArrowLeft class="w-4 h-4" />
Back Back
</Button> </Button>
<BookmarkButton <div class="flex items-center gap-1.5">
v-if="activity" <Button
:pubkey="activity.organizer.pubkey" v-if="ownedLnbitsEvent"
:d-tag="activity.id" variant="ghost"
/> size="sm"
class="gap-1.5"
@click="openEditDialog"
aria-label="Edit event"
>
<Pencil class="w-4 h-4" />
Edit
</Button>
<BookmarkButton
v-if="activity"
:pubkey="activity.organizer.pubkey"
:d-tag="activity.id"
/>
</div>
</div> </div>
<!-- Loading --> <!-- Loading -->