-
Notifications
You must be signed in to change notification settings - Fork 0
feat: show tickets page #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <template> | ||
| <ActiveCard> | ||
| <UIcon name="i-lucide-mail-question-mark" class="size-8 text-primary" /> | ||
|
|
||
| <h3 class="text-xl/5 font-bold"> | ||
| {{ ticket.title }} | ||
| </h3> | ||
|
|
||
| <div class="w-full text-base/5 font-normal whitespace-pre-wrap break-words line-clamp-5"> | ||
| {{ ticket.description }} | ||
| </div> | ||
|
|
||
| <div class="flex justify-between items-center"> | ||
| <div class="flex flex-row gap-4"> | ||
| <div class="flex flex-row gap-1.5 items-center text-muted"> | ||
| <UIcon name="i-lucide-message-circle" class="size-5" /> | ||
| <p>{{ ticket?.messages.length }}</p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <time | ||
| :datetime="ticket.updatedAt" | ||
| class="text-sm text-muted" | ||
| v-text="format(new Date(ticket.updatedAt), 'обновлен d MMMM yyyy', { locale: ru })" | ||
| /> | ||
| </div> | ||
| </ActiveCard> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import type { TicketWithData } from '~/stores/ticket' | ||
| import { format } from 'date-fns' | ||
| import { ru } from 'date-fns/locale/ru' | ||
|
|
||
| defineProps<{ | ||
| ticket: TicketWithData | ||
| }>() | ||
| </script> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| <template> | ||
| <div class="flex flex-row gap-2 items-start"> | ||
| <div class="mt-2.5"> | ||
| <UAvatar :src="user?.avatarUrl ?? undefined" /> | ||
| </div> | ||
| <div class="w-full flex flex-col gap-1.5"> | ||
| <UDropdownMenu | ||
| :items="items" | ||
| :ui="{ | ||
| content: 'w-56', | ||
| item: 'p-2 motion-preset-slide-left motion-duration-200', | ||
| }" | ||
| :content="{ | ||
| sideOffset: -32, | ||
| }" | ||
| > | ||
| <ActiveCard> | ||
| <div class="w-full relative flex flex-col justify-between gap-2"> | ||
| <div class="flex flex-col gap-1"> | ||
| <div class="text-base/5 whitespace-break-spaces text-default font-medium"> | ||
| {{ message?.text }} | ||
| </div> | ||
|
|
||
| <div v-if="message?.fileUrl"> | ||
| <UButton | ||
| variant="solid" | ||
| color="secondary" | ||
| :icon="getFileIcon(message.fileType)" | ||
| @click="handleFileClick(message.fileUrl)" | ||
| > | ||
| Прикрепленный файл | ||
| </UButton> | ||
| </div> | ||
|
|
||
| <div v-if="message?.createdAt" class="mt-1 flex justify-end text-xs text-muted"> | ||
| {{ format(new Date(message.createdAt), 'dd MMMM в HH:mm', { locale: ru }) }} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </ActiveCard> | ||
| </UDropdownMenu> | ||
|
|
||
| <!-- <div v-if="message?.notifications?.length" class="-mt-4 ml-4 flex flex-row flex-wrap gap-1"> | ||
| <UserBeacon | ||
| v-for="notification in comment.notifications" | ||
| :key="notification.id" | ||
| :notification="notification" | ||
| /> | ||
| </div> --> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import type { DropdownMenuItem } from '@nuxt/ui' | ||
| import { format } from 'date-fns' | ||
| import { ru } from 'date-fns/locale/ru' | ||
|
|
||
| const { ticketId, messageId } = defineProps<{ | ||
| ticketId: string | ||
| messageId: string | ||
| }>() | ||
|
|
||
| // const overlay = useOverlay() | ||
| // const modalCreateEpicCommentBeacon = overlay.create(ModalCreateEpicCommentBeacon) | ||
|
|
||
| const ticketStore = useTicketStore() | ||
| const userStore = useUserStore() | ||
|
|
||
| const ticket = computed(() => ticketStore.tickets.find((t) => t.id === ticketId)) | ||
| const message = computed(() => ticket.value?.messages.find((m) => m.id === messageId)) | ||
| const user = computed(() => userStore.find(message.value?.userId ?? '')) | ||
|
|
||
| const items = computed<DropdownMenuItem[]>(() => { | ||
| const menuItems: DropdownMenuItem[] = [ | ||
| { | ||
| label: 'Скопировать сообщение', | ||
| icon: 'i-lucide-copy', | ||
| color: 'neutral', | ||
| disabled: false, | ||
| onSelect: () => navigator.clipboard.writeText(message.value?.text ?? ''), | ||
| condition: true, | ||
| }, | ||
| // { | ||
| // label: 'Маякнуть (будет позже)', | ||
| // icon: 'i-lucide-users-round', | ||
| // color: 'neutral', | ||
| // disabled: true, | ||
| // onSelect: () => modalCreateEpicCommentBeacon.open({ messageId }), | ||
| // condition: true, | ||
| // }, | ||
| { | ||
| label: 'Лайкнуть (будет позже)', | ||
| icon: 'i-lucide-thumbs-up', | ||
| color: 'neutral', | ||
| disabled: true, | ||
| onSelect: () => {}, | ||
| condition: user.value?.id !== userStore.id, | ||
| }, | ||
| { | ||
| label: 'Редактировать', | ||
| icon: 'i-lucide-edit', | ||
| disabled: true, | ||
| onSelect: () => {}, | ||
| condition: user.value?.id === userStore.id, | ||
| }, | ||
| { | ||
| label: 'Удалить', | ||
| icon: 'i-lucide-trash-2', | ||
| disabled: true, | ||
| onSelect: () => {}, | ||
| condition: user.value?.id === userStore.id, | ||
| }, | ||
| ] | ||
|
|
||
| return menuItems.filter((item) => item.condition) | ||
| }) | ||
|
|
||
| function getFileIcon(type: 'image' | 'video' | 'document' | null) { | ||
| switch (type) { | ||
| case 'image': | ||
| return 'i-lucide-image' | ||
| case 'video': | ||
| return 'i-lucide-video' | ||
| case 'document': | ||
| return 'i-lucide-file' | ||
| default: | ||
| return 'i-lucide-file' | ||
| } | ||
| } | ||
|
|
||
| function handleFileClick(fileUrl: string) { | ||
| window.open(fileUrl, '_blank') | ||
| } | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| <template> | ||
| <PageContainer> | ||
| <Section> | ||
| <div class="flex flex-row items-start justify-between gap-2.5"> | ||
| <UIcon name="i-lucide-mail-question-mark" class="size-10 text-primary" /> | ||
| </div> | ||
|
|
||
| <h1 class="text-2xl/6 font-bold"> | ||
| {{ ticket?.title }} | ||
| </h1> | ||
|
|
||
| <div class="w-full text-base/5 whitespace-pre-wrap break-words"> | ||
| {{ ticket?.description }} | ||
| </div> | ||
| </Section> | ||
|
|
||
| <Section class="flex flex-row justify-between items-center"> | ||
| <div class="flex flex-row items-center gap-2"> | ||
| <UIcon name="i-lucide-message-circle" class="size-5" /> | ||
| {{ ticket?.messages.length }} {{ pluralizationRu(ticket?.messages.length ?? 0, ['сообщение', 'сообщения', 'сообщений']) }} | ||
| </div> | ||
| </Section> | ||
|
|
||
| <div class="w-full flex flex-col gap-3.5 flex-1 last-of-type:mb-20"> | ||
| <TicketMessage | ||
| v-for="message in messages" | ||
| :key="message.id" | ||
| :ticket-id="message.ticketId" | ||
| :message-id="message.id" | ||
| /> | ||
|
|
||
| <UButton | ||
| v-if="isShowMore" | ||
| variant="solid" | ||
| color="secondary" | ||
| size="xl" | ||
| class="w-full items-center justify-center" | ||
| icon="i-lucide-message-circle" | ||
| :label="$t('common.show-more')" | ||
| @click="shownMessages += 10" | ||
| /> | ||
| </div> | ||
|
|
||
| <!-- <UDrawer v-model:open="isDrawerOpened"> | ||
| <CreateCard | ||
| v-if="epic?.id" | ||
| :label="$t('app.create.epic-comment.button')" | ||
| icon="i-lucide-message-circle" | ||
| /> | ||
|
|
||
| <template #body> | ||
| <FormCreateEpicComment | ||
| :epic-id="epic?.id ?? ''" | ||
| @submitted="isDrawerOpened = false" | ||
| @success="isDrawerOpened = false" | ||
| /> | ||
| </template> | ||
| </UDrawer> --> | ||
| </PageContainer> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| definePageMeta({ | ||
| name: 'ticket-ticketId', | ||
| canReturn: true, | ||
| }) | ||
|
|
||
| const { params } = useRoute('ticket-ticketId') | ||
|
|
||
| const ticketStore = useTicketStore() | ||
| const ticket = computed(() => ticketStore.tickets.find((e) => e.id === params.ticketId)) | ||
|
|
||
| // On load show last 10 messages. On button click = show more 10 messages | ||
| const shownMessages = ref(10) | ||
| const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value)) | ||
| const isShowMore = computed<boolean>(() => messages.value?.length && ticket.value?.messages.length ? messages.value.length < ticket.value.messages.length : false) | ||
|
Comment on lines
+74
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Simplify computed property logic and fix potential edge cases The -const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value))
-const isShowMore = computed<boolean>(() => messages.value?.length && ticket.value?.messages.length ? messages.value.length < ticket.value.messages.length : false)
+const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value) ?? [])
+const isShowMore = computed<boolean>(() => {
+ if (!ticket.value?.messages.length) return false
+ return messages.value.length < ticket.value.messages.length
+})
🤖 Prompt for AI Agents |
||
|
|
||
| // const isDrawerOpened = ref(false) | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <template> | ||
| <PageContainer> | ||
| <NuxtLink | ||
| v-for="ticket of ticketStore.tickets" | ||
| :key="ticket.id" | ||
| :to="`/ticket/${ticket.id}`" | ||
| class="motion-preset-slide-left" | ||
| > | ||
| <TicketCard :ticket="ticket"> | ||
| {{ ticket.title }} | ||
| </TicketCard> | ||
| </NuxtLink> | ||
|
|
||
| <div class="mt-16 flex flex-row justify-center"> | ||
| <UIcon name="i-lucide-route" class="size-8 text-dimmed/25" /> | ||
| </div> | ||
| </PageContainer> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| const ticketStore = useTicketStore() | ||
| </script> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Ticket, TicketMessage, User } from '@roll-stack/database' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { initDataRaw as _initDataRaw, useSignal } from '@telegram-apps/sdk-vue' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type TicketWithData = Ticket & { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: TicketMessage[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastMessage: TicketMessage | null | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| user: User | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const useTicketStore = defineStore('ticket', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tickets = ref<TicketWithData[]>([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const initDataRaw = useSignal(_initDataRaw) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function update() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await $fetch('/api/ticket/list', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: `tma ${initDataRaw.value}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!data) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| tickets.value = data | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof Error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error.message.includes('401')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No session | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error.message.includes('404')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Not found | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling implementation The current error handling checks for status codes via string matching in error messages, which is fragile and non-standard. Additionally, the error handlers don't perform any actions, making them ineffective. - } catch (error) {
- if (error instanceof Error) {
- if (error.message.includes('401')) {
- // No session
- }
- if (error.message.includes('404')) {
- // Not found
- }
- }
+ } catch (error: any) {
+ // Handle FetchError from $fetch
+ if (error?.data?.statusCode === 401 || error?.statusCode === 401) {
+ // No session - could redirect to login or show auth prompt
+ console.error('Authentication required for ticket access')
+ tickets.value = []
+ } else if (error?.data?.statusCode === 404 || error?.statusCode === 404) {
+ // Not found - likely means no tickets endpoint
+ console.error('Ticket endpoint not found')
+ tickets.value = []
+ } else {
+ // Log unexpected errors
+ console.error('Failed to fetch tickets:', error)
+ throw error // Re-throw to let caller handle if needed
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| tickets, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| update, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Ensure proper handling of undefined condition property
The TypeScript type
DropdownMenuItemmight not include aconditionproperty based on the Nuxt UI types. This could cause TypeScript errors or runtime issues.🌐 Web query:
💡 Result:
No. The official DropdownMenuItem type in @nuxt/ui does not include a "condition" property — the documented item properties (label, icon, avatar, kbds, type, color, checked, disabled, slot, onSelect, onUpdateChecked, children, class, ui, etc.) do not list "condition". [1][2]
Remove/guard unknown "condition" on DropdownMenuItem objects
The @nuxt/ui DropdownMenuItem type does not include a "condition" property; keeping it in an object literal will cause TypeScript errors.
condition: true(line 82) and compute/filter visibility before creating DropdownMenuItem objects.as DropdownMenuItem & { condition?: boolean }or module augmentation).🤖 Prompt for AI Agents