diff --git a/src/activities-app/App.vue b/src/activities-app/App.vue index 4ac820a..640608b 100644 --- a/src/activities-app/App.vue +++ b/src/activities-app/App.vue @@ -7,6 +7,8 @@ import AppShell from '@/components/layout/AppShell.vue' import type { BottomTab } from '@/components/layout/BottomNav.vue' import { useAuth } from '@/composables/useAuthService' import { useActivitiesStore } from '@/modules/activities/stores/activities' +import { useActivities } from '@/modules/activities/composables/useActivities' +import { useApprovalState } from '@/modules/activities/composables/useApprovalState' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { TicketApiService } from '@/modules/activities/services/TicketApiService' import type { CreateEventRequest } from '@/modules/activities/types/ticket' @@ -16,6 +18,11 @@ const route = useRoute() const { t } = useI18n() const { isAuthenticated, currentUser } = useAuth() const activitiesStore = useActivitiesStore() +const { isAdmin, autoApprove } = useApprovalState() +// Used to merge own LNbits drafts into the activities feed right after +// the user creates or edits an event — otherwise the new draft only +// surfaces on the next ActivitiesPage subscribe cycle. +const { loadOwnEvents } = useActivities() // 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 @@ -27,7 +34,12 @@ const tabs = computed(() => [ { name: t('activities.createNew'), icon: Plus, - onClick: () => { activitiesStore.showCreateDialog = true }, + onClick: () => { + // Defensively clear any lingering edit selection so the Create + // tap always opens in Create mode regardless of a prior Edit. + activitiesStore.editingEvent = null + activitiesStore.showCreateDialog = true + }, disabled: !isAuthenticated.value, }, { name: t('activities.nav.map'), icon: Map, path: '/activities/map' }, @@ -57,14 +69,40 @@ async function handleCreateEvent(eventData: CreateEventRequest) { if (!invoiceKey) throw new Error('No wallet available. Please log in first.') 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 +} diff --git a/src/modules/activities/components/ActivityCard.vue b/src/modules/activities/components/ActivityCard.vue index 71054fb..d021b10 100644 --- a/src/modules/activities/components/ActivityCard.vue +++ b/src/modules/activities/components/ActivityCard.vue @@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n' import { format } from 'date-fns' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { MapPin, Calendar, Ticket } from 'lucide-vue-next' +import { MapPin, Calendar, Ticket, User } from 'lucide-vue-next' import BookmarkButton from './BookmarkButton.vue' import { useDateLocale } from '../composables/useDateLocale' import type { Activity } from '../types/activity' @@ -87,6 +87,17 @@ const placeholderBg = computed(() => { {{ categoryLabel }} + + + + Yours + + { {{ priceDisplay }} + + + {{ activity.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }} + diff --git a/src/modules/activities/components/CreateEventDialog.vue b/src/modules/activities/components/CreateEventDialog.vue index dd5ab5f..180184b 100644 --- a/src/modules/activities/components/CreateEventDialog.vue +++ b/src/modules/activities/components/CreateEventDialog.vue @@ -1,5 +1,5 @@ @@ -83,7 +123,7 @@ function handleEventCreated() {
- @@ -105,7 +145,16 @@ function handleEventCreated() {
- {{ event.name }} +
+ {{ event.name }} + + {{ event.status === 'rejected' ? 'Rejected' : 'Pending review' }} + +
{{ event.info }}
@@ -128,19 +177,33 @@ function handleEventCreated() {
- + + @@ -189,10 +252,18 @@ function handleEventCreated() { diff --git a/src/modules/base/components/ImageUpload.vue b/src/modules/base/components/ImageUpload.vue index 228e7ec..e64c825 100644 --- a/src/modules/base/components/ImageUpload.vue +++ b/src/modules/base/components/ImageUpload.vue @@ -449,7 +449,11 @@ const removeImage = async (imageToRemove: ImageWithMetadata) => { if (props.disabled) return try { - // Only try to delete from pict-rs if we have a delete token (newly uploaded images) + // Server-side delete only when we have a delete_token (newly + // uploaded this session). Pre-existing images re-populated from a + // stored URL ship `delete_token: ''` by convention — we don't own + // the original upload's one-time token, and removing on the client + // shouldn't reach back and wipe the server-side file. if (imageToRemove.delete_token) { await imageService.deleteImage(imageToRemove.delete_token, imageToRemove.alias) } diff --git a/src/modules/base/services/ImageUploadService.ts b/src/modules/base/services/ImageUploadService.ts index 2063ce3..69aadae 100644 --- a/src/modules/base/services/ImageUploadService.ts +++ b/src/modules/base/services/ImageUploadService.ts @@ -300,9 +300,12 @@ export class ImageUploadService extends BaseService { } /** - * Extract file ID from alias, handling both file IDs and full URLs + * Extract a pict-rs file ID from an alias, accepting both bare IDs + * and full `/image/original/` URLs. Public so callers + * re-populating uploads from stored URLs (edit flows) don't have to + * re-implement the parse. */ - private extractFileId(alias: string): string { + extractFileId(alias: string): string { if (!alias) { return '' } diff --git a/src/modules/market/components/CreateProductDialog.vue b/src/modules/market/components/CreateProductDialog.vue index dd5b505..23eb9ac 100644 --- a/src/modules/market/components/CreateProductDialog.vue +++ b/src/modules/market/components/CreateProductDialog.vue @@ -516,30 +516,17 @@ watch(() => props.isOpen, async (isOpen) => { // Reset form with appropriate initial values resetForm({ values: initialValues }) - // Convert existing image URLs to the format expected by ImageUpload component + // Convert existing image URLs to the format expected by ImageUpload. + // delete_token is intentionally empty for pre-existing images: see + // ImageUploadService.deleteImage gate — removing on the client + // should not delete the server-side file. if (props.product?.images && props.product.images.length > 0) { - // For existing products, we need to convert URLs back to a format ImageUpload can display - uploadedImages.value = props.product.images.map((url, index) => { - let alias = url - - // If it's a full pict-rs URL, extract just the file ID - if (url.includes('/image/original/')) { - const parts = url.split('/image/original/') - if (parts.length > 1 && parts[1]) { - alias = parts[1] - } - } else if (url.startsWith('http://') || url.startsWith('https://')) { - // Keep full URLs as-is - alias = url - } - - return { - alias: alias, - delete_token: '', - isPrimary: index === 0, - details: {} - } - }) + uploadedImages.value = props.product.images.map((url, index) => ({ + alias: imageService.extractFileId(url), + delete_token: '', + isPrimary: index === 0, + details: {} as any, + })) } else { uploadedImages.value = [] }