Skip to content

Commit 328638d

Browse files
authored
feat: show comments on flow item (#192)
1 parent a018049 commit 328638d

13 files changed

Lines changed: 360 additions & 64 deletions

File tree

apps/atrium-telegram/app/app.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
export default defineAppConfig({
22
ui: {
3+
badge: {
4+
variants: {
5+
color: {
6+
neutral: '',
7+
},
8+
},
9+
},
310
input: {
411
slots: {
512
base: '!ring-default placeholder:text-muted/50',
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<template>
2+
<div class="flex flex-row gap-2 items-start">
3+
<div class="mt-2.5">
4+
<UAvatar :src="user?.avatarUrl ?? undefined" />
5+
</div>
6+
<div class="w-full flex flex-col gap-1.5">
7+
<UDropdownMenu
8+
:items="items"
9+
:ui="{
10+
content: 'w-56',
11+
item: 'p-2 motion-preset-slide-left motion-duration-200',
12+
}"
13+
:content="{
14+
sideOffset: -32,
15+
}"
16+
>
17+
<ActiveCard>
18+
<div class="w-full relative flex flex-col justify-between gap-2">
19+
<div class="flex flex-col gap-1">
20+
<div class="text-base/5 whitespace-break-spaces text-default font-medium">
21+
{{ comment?.text }}
22+
</div>
23+
24+
<div v-if="comment?.createdAt" class="mt-1 flex justify-end text-xs text-muted">
25+
{{ format(new Date(comment.createdAt), 'dd MMMM в HH:mm', { locale: ru }) }}
26+
</div>
27+
</div>
28+
</div>
29+
</ActiveCard>
30+
</UDropdownMenu>
31+
32+
<!-- <div v-if="comment?.notifications?.length" class="-mt-4 ml-4 flex flex-row flex-wrap gap-1">
33+
<UserBeacon
34+
v-for="notification in comment.notifications"
35+
:key="notification.id"
36+
:notification="notification"
37+
/>
38+
</div> -->
39+
</div>
40+
</div>
41+
</template>
42+
43+
<script setup lang="ts">
44+
import type { DropdownMenuItem } from '@nuxt/ui'
45+
import { format } from 'date-fns'
46+
import { ru } from 'date-fns/locale/ru'
47+
48+
const { itemId, commentId } = defineProps<{
49+
itemId: string
50+
commentId: string
51+
}>()
52+
53+
const flowStore = useFlowStore()
54+
const userStore = useUserStore()
55+
56+
const item = computed(() => flowStore.items.find((i) => i.id === itemId))
57+
const comment = computed(() => item.value?.comments.find((comment) => comment.id === commentId))
58+
const user = computed(() => userStore.find(comment.value?.userId ?? ''))
59+
60+
const items = computed<DropdownMenuItem[]>(() => {
61+
const menuItems: DropdownMenuItem[] = [
62+
{
63+
label: 'Скопировать сообщение',
64+
icon: 'i-lucide-copy',
65+
color: 'neutral',
66+
disabled: false,
67+
onSelect: () => navigator.clipboard.writeText(comment.value?.text ?? ''),
68+
condition: true,
69+
},
70+
// {
71+
// label: 'Маякнуть (будет позже)',
72+
// icon: 'i-lucide-users-round',
73+
// color: 'neutral',
74+
// disabled: true,
75+
// onSelect: () => modalCreateEpicCommentBeacon.open({ commentId }),
76+
// condition: true,
77+
// },
78+
{
79+
label: 'Лайкнуть (будет позже)',
80+
icon: 'i-lucide-thumbs-up',
81+
color: 'neutral',
82+
disabled: true,
83+
onSelect: () => {},
84+
condition: user.value?.id !== userStore.id,
85+
},
86+
{
87+
label: 'Редактировать',
88+
icon: 'i-lucide-edit',
89+
disabled: true,
90+
onSelect: () => {},
91+
condition: user.value?.id === userStore.id,
92+
},
93+
{
94+
label: 'Удалить',
95+
icon: 'i-lucide-trash-2',
96+
disabled: true,
97+
onSelect: () => {},
98+
condition: user.value?.id === userStore.id,
99+
},
100+
]
101+
102+
return menuItems.filter((item) => item.condition)
103+
})
104+
</script>

apps/atrium-telegram/app/pages/flow/[itemId]/index.vue

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@
2121
</div>
2222

2323
<div class="mt-6 flex justify-between items-center">
24-
<div class="flex flex-row gap-4">
25-
<div class="flex flex-row gap-1.5 items-center text-muted text-sm">
26-
<UIcon name="i-lucide-message-circle" class="size-5" />
27-
<p>0</p>
28-
</div>
29-
</div>
24+
<div class="flex flex-row gap-4" />
3025

3126
<time
3227
v-if="item?.createdAt"
@@ -51,6 +46,41 @@
5146
/>
5247
</div>
5348
</Section>
49+
50+
<div class="flex flex-col gap-2.5">
51+
<div class="flex flex-row gap-2.5 items-center">
52+
<SectionTitle title="Комментарии" />
53+
<UBadge
54+
v-if="item?.comments.length"
55+
size="sm"
56+
color="primary"
57+
variant="soft"
58+
class="min-w-6 justify-center"
59+
>
60+
{{ item?.comments.length }}
61+
</UBadge>
62+
</div>
63+
64+
<UButton
65+
variant="solid"
66+
color="secondary"
67+
size="xl"
68+
block
69+
class="items-center justify-center"
70+
icon="i-lucide-message-circle"
71+
label="Написать сообщение"
72+
@click="vibrate()"
73+
/>
74+
75+
<div v-if="item?.comments.length" class="w-full flex flex-col gap-3.5 flex-1 last-of-type:mb-20">
76+
<FlowItemComment
77+
v-for="comment in item?.comments"
78+
:key="comment.id"
79+
:item-id="comment.itemId"
80+
:comment-id="comment.id"
81+
/>
82+
</div>
83+
</div>
5484
</PageContainer>
5585
</template>
5686

@@ -64,6 +94,7 @@ definePageMeta({
6494
})
6595
6696
const { params } = useRoute('flow-itemId')
97+
const { vibrate } = useFeedback()
6798
6899
const userStore = useUserStore()
69100
const flowStore = useFlowStore()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createFlowItemCommentSchema } from '#shared/services/flow'
2+
import { repository } from '@roll-stack/database'
3+
import { type } from 'arktype'
4+
5+
export default defineEventHandler(async (event) => {
6+
try {
7+
const itemId = getRouterParam(event, 'itemId')
8+
if (!itemId) {
9+
throw createError({
10+
statusCode: 400,
11+
message: 'Id is required',
12+
})
13+
}
14+
15+
const body = await readBody(event)
16+
const data = createFlowItemCommentSchema(body)
17+
if (data instanceof type.errors) {
18+
throw data
19+
}
20+
21+
// Guards:
22+
// If not exist
23+
const item = await repository.flow.findItem(itemId)
24+
if (!item) {
25+
throw createError({
26+
statusCode: 404,
27+
message: 'Item not found',
28+
})
29+
}
30+
31+
await repository.flow.createItemComment({
32+
text: data.text,
33+
itemId,
34+
userId: event.context.user.id,
35+
})
36+
37+
return { ok: true }
38+
} catch (error) {
39+
throw errorResolver(error)
40+
}
41+
})

apps/atrium-telegram/shared/services/flow.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ export const createFlowItemSchema = type({
88
type: flowTypeSchema.describe('error.length.invalid'),
99
})
1010
export type CreateFlowItem = typeof createFlowItemSchema.infer
11+
12+
export const createFlowItemCommentSchema = type({
13+
text: type('string <= 2500').describe('error.length.invalid'),
14+
})
15+
export type CreateFlowItemComment = typeof createFlowItemCommentSchema.infer

apps/web-app/app/components/form/CreateTicketMessage.vue

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,33 @@
1717
<UTextarea
1818
v-model="state.text"
1919
color="neutral"
20-
variant="none"
2120
required
22-
autoresize
2321
placeholder="Напишите свое сообщение..."
24-
:rows="3"
22+
size="xl"
23+
:rows="5"
2524
:disabled="loading"
2625
class="w-full"
27-
:ui="{ base: 'p-0 resize-none text-lg leading-6' }"
26+
:ui="{ base: 'text-lg leading-6' }"
2827
/>
2928
</UFormField>
3029

31-
<div class="flex items-center justify-between gap-2">
32-
<div class="flex items-center gap-2">
33-
<UAvatar
34-
:src="userStore.avatarUrl ?? undefined"
35-
alt=""
36-
class="size-8"
30+
<div class="flex items-start justify-between gap-2">
31+
<!-- <UFormField name="files">
32+
<UFileUpload
33+
v-model="state.files"
34+
multiple
35+
:disabled="loading"
36+
class="w-full"
37+
label="Прикрепить файлы"
3738
/>
38-
<p class="text-sm font-semibold">
39-
{{ userStore.fullName }}
40-
</p>
41-
</div>
39+
</UFormField> -->
4240

4341
<UButton
4442
type="submit"
4543
color="secondary"
46-
size="lg"
44+
size="xl"
4745
icon="i-lucide-send"
46+
class="px-6"
4847
:loading="loading"
4948
:disabled="!state.text"
5049
:label="$t('common.send')"
@@ -55,8 +54,8 @@
5554
</template>
5655

5756
<script setup lang="ts">
57+
import type { CreateTicketMessage } from '#shared/services/ticket'
5858
import type { FormSubmitEvent } from '@nuxt/ui'
59-
import type { CreateTicketMessage } from '~~/shared/services/ticket'
6059
6160
const { id } = defineProps<{
6261
id: string
Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<template>
22
<div v-if="isFirstMessageOfDay" class="mt-6.5 mb-2.5 w-full flex flex-row items-center justify-center">
33
<UBadge color="neutral" variant="soft">
4-
{{ isToday(new Date(createdAt)) ? 'Сегодня, ' : '' }}
5-
{{ format(new Date(createdAt), 'd MMMM', { locale: ru }) }}
4+
{{ isToday(new Date(message.createdAt)) ? 'Сегодня, ' : '' }}
5+
{{ format(new Date(message.createdAt), 'd MMMM', { locale: ru }) }}
66
</UBadge>
77
</div>
88

@@ -14,50 +14,40 @@
1414
</UserPopover>
1515
</div>
1616

17-
<div
18-
class="min-h-12 min-w-18 relative bg-elevated/50 px-3.5 py-2 rounded-lg"
19-
:class="[
20-
side === 'left' && 'text-neutral-900 md:max-w-[85%] lg:max-w-[70%] bg-orange-50 dark:bg-orange-100 border-b-2 border-orange-200 dark:border-orange-300',
21-
isBot && '!w-full !text-neutral-900 !max-w-full !bg-transparent !border-transparent !border-0',
22-
]"
23-
>
24-
<div
25-
class="text-sm/5 md:text-base/5 font-medium whitespace-break-spaces text-pretty"
26-
:class="[
27-
isBot && '!text-muted !text-sm/5',
28-
]"
29-
>
30-
{{ text }}
31-
</div>
32-
33-
<div
34-
class="mt-0.5 flex justify-end text-xs text-dimmed"
35-
:class="[
36-
side === 'left' && 'text-neutral-600',
37-
isBot && '!text-dimmed !justify-start !bg-elevated/50 !rounded-lg !mt-1 !px-1.5 !py-1 !w-fit',
38-
]"
39-
>
40-
{{ format(new Date(createdAt), 'HH:mm') }}
41-
</div>
42-
</div>
17+
<TicketMessageText
18+
v-if="isMessageWithText && message"
19+
:message="message"
20+
:side="side"
21+
/>
22+
<TicketMessageImage
23+
v-else-if="isMessageWithImage && message"
24+
:message="message"
25+
:side="side"
26+
/>
27+
<TicketMessageFile
28+
v-else-if="isMessageWithFile && message"
29+
:message="message"
30+
:side="side"
31+
/>
4332
</div>
4433
</article>
4534
</template>
4635

4736
<script setup lang="ts">
37+
import type { TicketMessage } from '@roll-stack/database'
4838
import { format, isToday } from 'date-fns'
4939
import { ru } from 'date-fns/locale/ru'
5040
51-
const { userId, isFirstMessageOfDay = false } = defineProps<{
52-
createdAt: string
53-
text: string | null
54-
userId: string
41+
const { message, isFirstMessageOfDay = false } = defineProps<{
42+
message: TicketMessage
5543
side: 'left' | 'right'
5644
isFirstMessageOfDay?: boolean
5745
}>()
5846
5947
const userStore = useUserStore()
60-
const user = computed(() => userStore.find(userId))
48+
const user = computed(() => userStore.find(message.userId))
6149
62-
const isBot = computed(() => !user.value?.type)
50+
const isMessageWithText = computed(() => !message?.fileUrl)
51+
const isMessageWithImage = computed(() => message?.fileUrl && message?.fileType === 'image')
52+
const isMessageWithFile = computed(() => message?.fileUrl && message?.fileType !== 'image')
6353
</script>

0 commit comments

Comments
 (0)