Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/atrium-telegram/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ watch(colorMode, () => {
// Init Stores
const user = useUserStore()
const task = useTaskStore()
const epic = useEpicStore()
const ticket = useTicketStore()
const kitchen = useKitchenStore()
const flow = useFlowStore()

Expand All @@ -79,7 +79,7 @@ onMounted(async () => {
user.updateOnline(),
user.update(),
task.update(),
epic.update(),
ticket.update(),
kitchen.update(),
flow.update(),
])
Expand All @@ -89,11 +89,11 @@ onMounted(async () => {
user.updateOnline(),
user.update(),
task.update(),
epic.update(),
ticket.update(),
kitchen.update(),
flow.update(),
])
}, 30000)
}, 20000)
})

onUnmounted(() => {
Expand Down
38 changes: 38 additions & 0 deletions apps/atrium-telegram/app/components/TicketCard.vue
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>
144 changes: 144 additions & 0 deletions apps/atrium-telegram/app/components/TicketMessage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<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 && message.fileType !== 'image'">
<UButton
variant="solid"
color="secondary"
:icon="getFileIcon(message.fileType)"
@click="handleFileClick(message.fileUrl)"
>
Прикрепленный файл
</UButton>
</div>
<div v-else-if="message?.fileUrl && message.fileType === 'image'">
<img
:src="message.fileUrl"
alt=""
class="w-full h-full object-contain rounded-lg"
@click="handleFileClick(message.fileUrl)"
>
</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 type { TicketMessage } from '@roll-stack/database'
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Ensure proper handling of undefined condition property

The TypeScript type DropdownMenuItem might not include a condition property based on the Nuxt UI types. This could cause TypeScript errors or runtime issues.


🌐 Web query:

Does the DropdownMenuItem type from @nuxt/ui include a condition property?

💡 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.

  • Fix: apps/atrium-telegram/app/components/TicketMessage.vue — remove condition: true (line 82) and compute/filter visibility before creating DropdownMenuItem objects.
  • Alternative: if you need the field, map items to the library's shape or explicitly cast/augment the type (e.g., as DropdownMenuItem & { condition?: boolean } or module augmentation).
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/TicketMessage.vue around line 82, the
DropdownMenuItem objects include an unsupported "condition" property which
causes TypeScript errors; remove the `condition: true` entry from the object
literal and instead compute visibility before constructing the DropdownMenuItem
array (filter items based on your condition), or if you must keep the field for
internal logic map your items into the library's expected shape or explicitly
cast/augment the type (e.g., map to DropdownMenuItem & { condition?: boolean })
so the final objects passed to the DropdownMenu accept the correct type.

},
// {
// 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: TicketMessage['fileType']) {
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>
8 changes: 4 additions & 4 deletions apps/atrium-telegram/app/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ function _useNavigation() {
badge: flowStore.nowViewedItemsCount.toString(),
},
{
path: '/epic',
names: ['epic', 'epic-epicId'],
title: t('app.epics'),
icon: 'i-lucide-crown',
path: '/ticket',
names: ['ticket', 'ticket-ticketId'],
title: t('app.tickets'),
icon: 'i-lucide-mail-question-mark',
},
{
path: '/tasks',
Expand Down
4 changes: 4 additions & 0 deletions apps/atrium-telegram/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
</NuxtLink>
</div>
</div>

<div class="mt-16 flex flex-row justify-center">
<UIcon name="i-lucide-route" class="size-8 text-dimmed/25" />
</div>
</PageContainer>
</template>

Expand Down
3 changes: 3 additions & 0 deletions apps/atrium-telegram/app/pages/startapp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ if (tgWebAppStartParam?.length && tgWebAppStartParam.includes(separator)) {
case 'epic':
await navigateTo({ path: `/epic/${value}`, query })
break
case 'ticket':
await navigateTo({ path: `/ticket/${value}`, query })
break
default:
await navigateTo('/')
}
Expand Down
79 changes: 79 additions & 0 deletions apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue
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)

// const isDrawerOpened = ref(false)
</script>
22 changes: 22 additions & 0 deletions apps/atrium-telegram/app/pages/ticket/index.vue
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>
44 changes: 44 additions & 0 deletions apps/atrium-telegram/app/stores/ticket.ts
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
}
}
}
}

return {
tickets,

update,
}
})
Loading