Skip to content

Commit 1f0cc9c

Browse files
authored
fix(atproto): improve handle edit layout for narrow screens (#371)
* fix(atproto): improve handle edit layout for narrow screens Change handle editing from row to column layout so the input gets full width and Save/Cancel buttons appear below it. This fixes the cramped layout where the input was too narrow to see the full handle when editing. * feat: add Not Published chip to event cards and detail page Add a warning-colored "Not published" chip for events that can be published to AT Protocol but haven't been yet. - Show chip when: no atprotoUri, no sourceType, user has active session - Click to publish with loading ("Publishing...") and error states - Updates to "Published" badge after successful sync - Added to both EventsItemComponent (list) and EventPage (detail) - Includes unit tests for visibility and interaction
1 parent 525234c commit 1f0cc9c

5 files changed

Lines changed: 528 additions & 26 deletions

File tree

src/api/events.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export interface EventApiType {
7676
previewAdminMessage: (slug: string, data: { subject: string, message: string, testEmail: string }) => Promise<AxiosResponse<{ message: string }>>
7777
contactOrganizers: (slug: string, data: { contactType: string, subject: string, message: string }) => Promise<AxiosResponse<AdminMessageResult>>
7878

79+
// AT Protocol sync
80+
syncAtproto: (slug: string) => Promise<AxiosResponse<EventEntity>>
81+
7982
// Recurrence-related methods (deprecated)
8083
/**
8184
* @deprecated Use eventSeriesApi.getOccurrences instead
@@ -167,5 +170,8 @@ export const eventsApi: EventApiType = {
167170
contactOrganizers: (slug: string, data: { contactType: string, subject: string, message: string }): Promise<AxiosResponse<AdminMessageResult>> => api.post(`/api/events/${slug}/contact-organizers`, data, createEventApiHeaders(slug)),
168171

169172
// Activity feed endpoint
170-
getFeed: (groupSlug: string, eventSlug: string, query?: { limit?: number, offset?: number }): Promise<AxiosResponse<ActivityFeedEntity[]>> => api.get(`/api/events/${eventSlug}/feed`, { params: query })
173+
getFeed: (groupSlug: string, eventSlug: string, query?: { limit?: number, offset?: number }): Promise<AxiosResponse<ActivityFeedEntity[]>> => api.get(`/api/events/${eventSlug}/feed`, { params: query }),
174+
175+
// AT Protocol sync
176+
syncAtproto: (slug: string): Promise<AxiosResponse<EventEntity>> => api.post(`/api/events/${slug}/sync-atproto`, {}, createEventApiHeaders(slug))
171177
}

src/components/atproto/AtprotoIdentityCard.vue

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@
128128
<div v-else-if="editingHandle" class="row items-center">
129129
<div class="text-subtitle2 text-grey-7 col-3">Handle</div>
130130
<div class="col">
131-
<div class="row items-center q-gutter-sm">
131+
<div class="column q-gutter-y-sm">
132132
<q-input
133133
data-cy="new-handle-input"
134134
v-model="newHandle"
135135
label="Username"
136136
filled
137137
dense
138-
class="col"
138+
class="full-width"
139139
:error="!!handleError"
140140
:error-message="handleError"
141141
@keyup.enter="submitHandleChange"
@@ -145,28 +145,30 @@
145145
<span class="text-grey-7 text-body2">{{ handleDomain || '.opnmt.me' }}</span>
146146
</template>
147147
</q-input>
148-
<q-btn
149-
data-cy="submit-handle-btn"
150-
color="primary"
151-
no-caps
152-
size="sm"
153-
:loading="updatingHandle"
154-
:disable="updatingHandle || !newHandle.trim()"
155-
@click="submitHandleChange"
156-
>
157-
Save
158-
</q-btn>
159-
<q-btn
160-
data-cy="cancel-handle-btn"
161-
flat
162-
no-caps
163-
size="sm"
164-
color="grey-7"
165-
:disable="updatingHandle"
166-
@click="cancelEditingHandle"
167-
>
168-
Cancel
169-
</q-btn>
148+
<div class="row q-gutter-x-sm">
149+
<q-btn
150+
data-cy="submit-handle-btn"
151+
color="primary"
152+
no-caps
153+
size="sm"
154+
:loading="updatingHandle"
155+
:disable="updatingHandle || !newHandle.trim()"
156+
@click="submitHandleChange"
157+
>
158+
Save
159+
</q-btn>
160+
<q-btn
161+
data-cy="cancel-handle-btn"
162+
flat
163+
no-caps
164+
size="sm"
165+
color="grey-7"
166+
:disable="updatingHandle"
167+
@click="cancelEditingHandle"
168+
>
169+
Cancel
170+
</q-btn>
171+
</div>
170172
</div>
171173
</div>
172174
</div>

src/components/event/EventsItemComponent.vue

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
11
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
23
import { EventEntity } from '../../types'
34
import { getImageSrc } from '../../utils/imageUtils'
45
import { formatDate } from '../../utils/dateUtils'
56
import { getSourceColor } from '../../utils/eventUtils'
7+
import { useAuthStore } from '../../stores/auth-store'
8+
import { eventsApi } from '../../api/events'
69
710
interface Props {
811
event: EventEntity;
912
layout?: 'grid' | 'list';
1013
}
1114
12-
defineProps<Props>()
15+
const props = defineProps<Props>()
16+
const emit = defineEmits<{
17+
synced: [event: EventEntity]
18+
}>()
19+
20+
const authStore = useAuthStore()
21+
22+
// Sync state: 'idle' | 'syncing' | 'error'
23+
const syncStatus = ref<'idle' | 'syncing' | 'error'>('idle')
24+
25+
/**
26+
* Check if the publish chip should be shown
27+
* Conditions:
28+
* - Event has no atprotoUri (not already published)
29+
* - Event has no sourceType (not imported from external source)
30+
* - User has active ATProto session
31+
*/
32+
const canPublish = computed(() => {
33+
const hasActiveSession = authStore.user?.atprotoIdentity?.hasActiveSession === true
34+
const notAlreadyPublished = !props.event.atprotoUri
35+
const notImported = !props.event.sourceType
36+
return hasActiveSession && notAlreadyPublished && notImported
37+
})
38+
39+
/**
40+
* Handle the publish button click
41+
*/
42+
const handlePublish = async () => {
43+
if (syncStatus.value === 'syncing') return
44+
45+
syncStatus.value = 'syncing'
46+
try {
47+
const response = await eventsApi.syncAtproto(props.event.slug)
48+
emit('synced', response.data)
49+
syncStatus.value = 'idle'
50+
} catch (error) {
51+
syncStatus.value = 'error'
52+
// Reset to idle after a brief delay to show the error
53+
setTimeout(() => {
54+
syncStatus.value = 'idle'
55+
}, 3000)
56+
}
57+
}
1358
1459
/**
1560
* Format a series slug to be more readable
@@ -73,6 +118,27 @@ const formatSeriesSlug = (slug: string): string => {
73118
/>
74119
{{ event.sourceType }}
75120
</q-badge>
121+
<q-badge
122+
v-if="canPublish"
123+
:color="syncStatus === 'syncing' ? 'grey' : syncStatus === 'error' ? 'negative' : 'warning'"
124+
class="q-ml-sm cursor-pointer"
125+
data-cy="publish-atproto-chip"
126+
:clickable="syncStatus !== 'syncing'"
127+
@click.stop.prevent="handlePublish"
128+
>
129+
<q-spinner-dots
130+
v-if="syncStatus === 'syncing'"
131+
size="xs"
132+
class="q-mr-xs"
133+
/>
134+
<q-icon
135+
v-else
136+
:name="syncStatus === 'error' ? 'sym_r_error' : 'sym_r_cloud_off'"
137+
size="xs"
138+
class="q-mr-xs"
139+
/>
140+
{{ syncStatus === 'syncing' ? 'Publishing...' : syncStatus === 'error' ? 'Publish failed' : 'Not published' }}
141+
</q-badge>
76142
<q-badge
77143
v-if="event.atprotoUri"
78144
color="blue"

0 commit comments

Comments
 (0)