WIP message-delivery-method-web-push
Main daploy / deploy (push) Successful in 1m22s

This commit is contained in:
2024-11-27 15:57:11 +02:00
parent 056247a414
commit 1c8019d41a
48 changed files with 464 additions and 128 deletions
+1
View File
@@ -17,6 +17,7 @@
},
"dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@hereconnect/types": "^1.0.0",
"@primevue/forms": "^4.2.1",
"@primevue/themes": "^4.2.1",
"@tanstack/vue-query": "^5.60.6",
+2 -2
View File
@@ -4,8 +4,8 @@ import type {
QRCodeDocument,
MessageDocument,
WebPushSubscriptionDocument,
} from '@/types/DBDocumentTypes'
import type { PublicSettingsDocument } from '@/types/DBDocumentTypes/PublicSettingsDocument'
PublicSettingsDocument,
} from '@hereconnect/types'
export const qrCodeDbRemote = new PouchDB<QRCodeDocument>(`${import.meta.env.VITE_API_URL}/qr`, {
skip_setup: true,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import { qrCodeDbLocal, qrCodeDbRemote } from '@/api/dbs'
import { getUserUuid } from '@/utils/getUserUuid'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { WebPushSubscriptionDocument } from '@/types/DBDocumentTypes'
import type { WebPushSubscriptionDocument } from '@hereconnect/types'
import { publicSettingsDbRemote, webPushSubscriptionsDbRemote } from '@/api/dbs'
import { getUserUuid } from '@/utils/getUserUuid'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
@@ -40,7 +40,9 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, useTemplateRef, watch, nextTick } from 'vue'
import { messagesDbLocal as db, messagesDbRemote as remoteDb } from '@/api/dbs'
import { type MessageDocument } from '@/types/DBDocumentTypes'
import { type MessageDocument } from '@hereconnect/types'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
import { getUserUuid } from '@/utils/getUserUuid'
const formRef = useTemplateRef('form')
const messagesContainerRef = useTemplateRef('messagesContainer')
@@ -84,9 +86,11 @@ async function sendMessage() {
type: 'message',
qr_code_uri,
chat,
created_at,
from: userRole,
body: newMessage.value.trim(),
browser_uuid: getBrowserUuid(),
user_uuid: getUserUuid(),
created_at,
}
try {
@@ -27,7 +27,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import CreateMessageDeliveryOptionTelegram from '@/components/create/CreateMessageDeliveryOptionTelegram.vue'
import CreateMessageDeliveryOptionWebPush from '@/components/create/CreateMessageDeliveryOptionWebPush.vue'
import { ref } from 'vue'
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import type { DeliveryOption } from '@/constants/deliveryOptions'
defineProps<{
@@ -9,7 +9,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { type DeliveryOption, DELIVERY_METHOD_TELEGRAM } from '@/constants/deliveryOptions'
import { computed } from 'vue'
@@ -42,7 +42,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { type DeliveryOption, DELIVERY_METHOD_WEB_PUSH } from '@/constants/deliveryOptions'
import { computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'
@@ -19,7 +19,7 @@
<script setup lang="ts">
import { qrCodePlacements } from '@/constants/qrCodePlacements'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
const selectedPlacement = defineModel<null | QRCodeDocument['placement']>()
defineEmits(['nextStep'])
@@ -31,7 +31,7 @@
<script setup lang="ts">
import { nextTick, useTemplateRef, watch } from 'vue'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { defaultMessages } from '@/constants/defaultMessages'
// Пропсы
@@ -14,7 +14,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
// Пропсы, которые будут передаваться из родительского компонента
const props = defineProps<{
@@ -14,7 +14,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
defineProps<{ buttonLabel: string }>()
const name = defineModel<QRCodeDocument['name']>('name')
const emit = defineEmits(['nextStep'])
@@ -13,7 +13,7 @@
import { computed } from 'vue'
import { VueMarkdownIt } from '@f3ve/vue-markdown-it'
import { useQuery } from '@tanstack/vue-query'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
// Импортируем все инструкции из директории
const importInstructions = import.meta.glob('/src/content/placementInstructions/*.md', {
@@ -48,7 +48,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import md5 from 'md5'
import type { ContactInfo } from '@/types/DBDocumentTypes'
import type { ContactInfo } from '@hereconnect/types'
import { sanitizeFileName } from '@/utils/sanitizeFileName'
const { contactInfo } = defineProps<{ contactInfo: ContactInfo }>()
@@ -45,8 +45,10 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { messagesDbLocal as db } from '@/api/dbs'
import type { MessageDocument, QRCodeDocument } from '@/types/DBDocumentTypes'
import type { MessageDocument, QRCodeDocument } from '@hereconnect/types'
import { ROUTE_NAMES } from '@/constants/routes'
import { getUserUuid } from '@/utils/getUserUuid'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
const { doc } = defineProps<{
doc: QRCodeDocument
@@ -76,6 +78,8 @@ async function sendMessage(content: string) {
chat,
created_at: new Date().toISOString(),
from: 'guest',
user_uuid: getUserUuid(),
browser_uuid: getBrowserUuid(),
body: content.trim(),
}
@@ -1,16 +0,0 @@
/**
* Базовый интерфейс для всех документов в базе данных.
* Содержит общие поля, такие как идентификатор, тип и дату создания.
*/
export interface BaseDocument {
/** Уникальный идентификатор документа */
_id: string
_rev?: string
/** Тип документа */
type: string
/** Дата создания документа в формате ISO */
created_at: string
}
@@ -1,30 +0,0 @@
import { type BaseDocument } from './BaseDocument'
/**
* Интерфейс для документа сообщения.
* Используется для хранения сообщений, отправленных через QR‑код, с поддержкой вложений.
*/
export interface MessageDocument extends BaseDocument {
/** Тип документа, всегда 'message' */
type: 'message'
/** Идентификатор QR‑кода, к которому относится сообщение */
qr_code_uri: string
/** Идентификатор чата для связывания сообщений */
chat: string
/** Отправитель сообщения ('guest' или 'owner') */
from: 'guest' | 'owner'
/** Содержимое сообщения */
body: string
/** Опциональная геопозиция (широта и долгота) */
location?: { lat: number; lng: number }
/** Вложения, такие как фото */
// _attachments?: {
// [key: string]: Attachment
// }
}
@@ -1,16 +0,0 @@
import type { BaseDocument } from '@/types/DBDocumentTypes/BaseDocument'
export interface PublicSettingsDocument extends BaseDocument {
_id: string // Формат: "public_settings:<name>"
type: 'public_settings'
/**
* Название настройки
*/
name: string
/**
* Значение настройки
*/
value: string
}
@@ -1,44 +0,0 @@
import type { BaseDocument } from './BaseDocument'
export type ContactInfo = {
name?: string
phone?: string
email?: string
}
/**
* Интерфейс для документа QR‑кода.
* Представляет QR‑код, созданный пользователем, и информацию о привязанном объекте.
*/
export interface QRCodeDocument extends BaseDocument {
/** Тип документа, всегда 'qr_code' */
type: 'qr_code'
/** Уникальный URL для настройки и взаимодействия */
uri: string
/** Ссылка на QR‑код */
url: string
/** Идентификатор пользователя, который создал QR‑код */
user_uuid: string
/** Идентификатор браузера, через который был создан QR-код */
browser_uuid: string
/** Название объекта, для которого создан QR‑код, например "Мой автомобиль" */
name: string
messageDeliveryMethod: 'telegram' | 'webPush'
/** Тип объекта, для которого создан QR‑код, например "car" */
placement: 'car' | 'pet' | 'door' | 'store' | 'restaurant_table' | 'other'
/** Доступные действия для гостей (например, ["send_message"]) */
actions: string[]
/** Предустановленное сообщение для гостей */
predefinedMessages?: string[]
contactInfo?: ContactInfo
}
@@ -1,25 +0,0 @@
import type { BaseDocument } from './BaseDocument'
/**
* Интерфейс для документа пользователя.
* Содержит данные, такие как имя пользователя, уникальный UUID, email и роли.
*/
export interface UserDocument extends BaseDocument {
/** Тип документа, всегда 'user' */
type: 'user'
/** Имя пользователя */
name: string
/** Уникальный UUID пользователя, не связанный с _id */
user_uuid: string
/** Email пользователя */
email: string
/** Роли пользователя (например, ['admin', 'user']) */
roles: string[]
/** Пароль пользователя (должен храниться в зашифрованном виде) */
password: string
}
@@ -1,11 +0,0 @@
import type { BaseDocument } from '@/types/DBDocumentTypes/BaseDocument'
export interface WebPushSubscriptionDocument extends BaseDocument {
_id: string // Формат: "web_push_subscription:<user_uuid>:<browser_uuid>"
type: 'web_push_subscription'
user_uuid: string // UUID пользователя, связанного с подпиской
browser_uuid: string // UUID браузера, связанного с подпиской
subscription: PushSubscriptionJSON
created_at: string // Дата создания подписки в ISO формате
updated_at?: string // Дата последнего обновления подписки
}
@@ -1,5 +0,0 @@
export * from './BaseDocument'
export * from './QRCodeDocument'
export * from './MessageDocument'
export * from './UserDocument'
export * from './WebPushSubscriptionDocument'
+1 -1
View File
@@ -44,7 +44,7 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { createQrCodeDocument } from '@/api/qrCode'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import { ROUTE_NAMES } from '@/constants/routes'
const placement = ref<null | QRCodeDocument['placement']>(null)
@@ -1,52 +1,62 @@
<template>
<div class="flex flex-col min-h-full">
<main class="flex-grow flex items-center justify-center py-6 px-4 md:px-6">
<main
class="flex-grow flex justify-center py-6 px-4 md:px-6"
:class="{
'items-center':
svgQueryIsLoading || qrCodeQueryIsLoading || svgQueryIsError || qrCodeQueryIsError,
}"
>
<section class="max-w-prose w-full flex flex-col gap-1">
<h2 class="text-xl font-semibold mb-4">Ваш QRкод готов</h2>
<p class="text-gray-600 mb-4">Сохраните его и ознакомьтесь с инструкциями по размещению.</p>
<!-- Рендер QRкода -->
<div v-if="!svgQueryIsLoading && svgQueryData" class="border p-4 bg-white rounded-lg mb-6">
<img :src="svgQueryData" alt="QR‑код" />
</div>
<p v-if="svgQueryIsLoading || qrCodeQueryIsLoading" class="text-gray-500 mt-4">
Загрузка...
</p>
<p v-else-if="svgQueryIsError" class="text-red-500 mt-4">
Ошибка генерации QRкода: {{ svgQueryError?.message }}
</p>
<!-- Сообщение об ошибке -->
<p v-if="qrCodeQueryIsError" class="text-red-500 mt-4">
<p v-else-if="qrCodeQueryIsError" class="text-red-500 mt-4">
Ошибка загрузки данных QRкода: {{ qrCodeQueryError!.message }}
</p>
<p v-if="svgQueryIsLoading" class="text-gray-500 mt-4">Загрузка...</p>
<template v-else>
<h2 class="text-xl font-semibold mb-4">Ваш QRкод готов</h2>
<p class="text-gray-600 mb-4">
Сохраните его и ознакомьтесь с инструкциями по размещению.
</p>
<!-- Кнопки для скачивания -->
<div
v-if="!svgQueryIsLoading && !qrCodeQueryIsError && svgQueryData"
class="flex flex-row gap-4 justify-center"
>
<Button
label="Скачать SVG"
icon="pi pi-download"
class="p-button-sm"
:href="svgQueryData"
:download="`${sanitizedFileName}.svg`"
as="a"
<!-- Рендер QRкода -->
<div class="border p-4 bg-white rounded-lg mb-6">
<img v-if="!svgQueryIsLoading && svgQueryData" :src="svgQueryData" alt="QR‑код" />
</div>
<!-- Кнопки для скачивания -->
<div
v-if="!svgQueryIsLoading && !qrCodeQueryIsError && svgQueryData"
class="flex flex-row gap-4 justify-center"
>
<Button
label="Скачать SVG"
icon="pi pi-download"
class="p-button-sm"
:href="svgQueryData"
:download="`${sanitizedFileName}.svg`"
as="a"
/>
<DownloadImageButton
:svgUrl="svgQueryData"
:filename="`${sanitizedFileName}.png`"
label="Скачать PNG"
type="image/png"
:width="width"
/>
</div>
<!-- Инструкции по размещению -->
<ManagePlacementInstructions
:placement="qrCodeDoc?.placement"
v-if="qrCodeDoc?.placement"
/>
<DownloadImageButton
:svgUrl="svgQueryData"
:filename="`${sanitizedFileName}.png`"
label="Скачать PNG"
type="image/png"
:width="width"
/>
</div>
<!-- Инструкции по размещению -->
<ManagePlacementInstructions
:placement="qrCodeDoc?.placement"
v-if="qrCodeDoc?.placement"
/>
</template>
</section>
</main>
@@ -71,7 +81,7 @@ import QRCodeStyling from 'qr-code-styling'
import { optimizeSvg } from '@/utils/images/svg/optimizeSvg'
import { replaceFillInSvg } from '@/utils/images/svg/replaceFillInSvg'
import { getQrCodeDocument } from '@/api/qrCode'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { addLabelToSvg } from '@/utils/images/svg/addLabelToSvg'
import logo from '@/assets/logo.svg?raw'
import { sanitizeFileName } from '@/utils/sanitizeFileName'
@@ -86,11 +96,15 @@ const height = ref(width.value)
// Хук для загрузки данных QR‑кода
const {
data: qrCodeDoc,
isLoading: qrCodeQueryIsLoading,
isError: qrCodeQueryIsError,
error: qrCodeQueryError,
} = useQuery({
queryKey: ['qrCode', computed(() => qr_code_uri)],
queryFn: () => getQrCodeDocument(qr_code_uri),
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return getQrCodeDocument(qr_code_uri)
},
})
const sanitizedFileName = computed(() => {
@@ -110,6 +124,8 @@ const {
enabled: computed(() => !!qrCodeDoc.value?.url),
retry: false,
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
// Заменяем цвет `fill` в логотипе
const updatedLogo = replaceFillInSvg(logo, logoColor.value)
+1 -1
View File
@@ -45,7 +45,7 @@
</template>
<script setup lang="ts">
import type { MessageDocument } from '@/types/DBDocumentTypes'
import type { MessageDocument } from '@hereconnect/types'
import ChatComponent from '@/components/ChatComponent.vue'
defineProps<{ qr_code_uri: MessageDocument['qr_code_uri']; chat: MessageDocument['chat'] }>()
</script>
+1 -1
View File
@@ -31,7 +31,7 @@
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { getQrCodeDocument } from '@/api/qrCode'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import QueryRender from '@/components/QueryRender.vue'
const { qr_code_uri } = defineProps<{ qr_code_uri: string }>()