move * to apps/frontend
Main daploy / deploy (push) Successful in 43s

This commit is contained in:
2024-11-21 01:15:06 +02:00
parent 3ac451e5f3
commit 11bba7a275
89 changed files with 35 additions and 23 deletions
@@ -0,0 +1,27 @@
<template>
<section>
<h2 class="text-xl font-semibold mb-4">Выберите доступные действия</h2>
<p class="text-gray-600 mb-4">Укажите, какие действия будут доступны для сканирующих QRкод.</p>
<p class="text-sm text-gray-500 mb-6">Вы сможете изменить эти настройки позже.</p>
<div class="flex flex-col gap-3 mb-4">
<div v-for="(label, action) in actionsOptions" :key="action" class="flex items-center gap-2">
<Checkbox v-model="model" :inputId="action" :value="action" @change="next" />
<label :for="action">{{ label }}</label>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const emit = defineEmits(['nextStep'])
const model = defineModel()
const actionsOptions = {
sendMessage: 'Отправить сообщение',
viewContactInfo: 'Посмотреть ваши контактные данные',
}
function next() {
emit('nextStep')
}
</script>
@@ -0,0 +1,46 @@
<template>
<section>
<h2 class="text-xl font-semibold mb-4">Куда отправлять сообщения?</h2>
<p class="text-gray-600 mb-4">Выберите, как вы хотите получать сообщения.</p>
<div class="flex flex-col gap-3 mb-4">
<div class="flex items-center gap-2" v-for="option in deliveryOptions" :key="option.method">
<RadioButton
v-model="model"
:inputId="option.method"
name="delivery"
:value="option.method"
@change="handleSelection(option.method)"
/>
<label :for="option.method">{{ option.label }}</label>
</div>
</div>
<!-- Сообщение о недоступности -->
<p v-if="telegramNotice" class="text-red-500 mb-2">
Отправка в Telegram пока в разработке.<br />
Этот способ отправки сообщений можно будет выбрать позже.
</p>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { deliveryOptions } from '@/constants/deliveryOptions'
const emit = defineEmits(['nextStep'])
const model = defineModel<QRCodeDocument['messageDeliveryMethod']>()
// Локальное состояние для уведомления
const telegramNotice = ref(false)
// Обработчик выбора
function handleSelection(method: QRCodeDocument['messageDeliveryMethod']) {
if (method === 'telegram') {
telegramNotice.value = true
} else {
telegramNotice.value = false
emit('nextStep')
}
}
</script>
@@ -0,0 +1,25 @@
<template>
<section>
<h2 class="text-xl font-semibold mb-4">Где вы хотите разместить QRкод?</h2>
<p class="text-gray-600 mb-4">Этот выбор поможет настроить QRкод с подходящими действиями.</p>
<div class="flex flex-col gap-3 mb-4">
<div v-for="{ label, type } in qrCodePlacements" :key="type" class="flex items-center gap-2">
<RadioButton
v-model="selectedPlacement"
:inputId="type"
name="placement"
:value="type"
@change="$emit('nextStep')"
/>
<label :for="type">{{ label }}</label>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { qrCodePlacements } from '@/constants/qrCodePlacements'
const selectedPlacement = defineModel<string>()
defineEmits(['nextStep'])
</script>
@@ -0,0 +1,76 @@
<template>
<section>
<h2 class="text-xl font-semibold mb-4">Сообщения для отправки</h2>
<p class="text-gray-600 mb-4">
После считывания QRкода можно будет выбрать одно из этих сообщений или написать своё.
</p>
<p class="text-sm text-gray-500 mb-6">Вы сможете изменить эти сообщения позже.</p>
<!-- Список сообщений -->
<div class="flex flex-col gap-2 mb-4">
<div v-for="(message, index) in messages" :key="index" class="flex gap-2" ref="messagesRef">
<InputText v-model="messages[index]" class="flex-1" placeholder="Введите сообщение" />
<Button
icon="pi pi-trash"
class="p-button-danger p-button-text"
@click="removeMessage(index)"
title="Удалить сообщение"
/>
</div>
</div>
<!-- Кнопка добавления сообщения -->
<Button
label="Добавить сообщение"
icon="pi pi-plus"
class="p-button-sm p-button-outlined mb-4"
@click="addMessage"
/>
</section>
</template>
<script setup lang="ts">
import { nextTick, useTemplateRef, watch } from 'vue'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { defaultMessages } from '@/constants/defaultMessages'
// Пропсы
const props = defineProps<{ placement: QRCodeDocument['placement'] }>()
const messagesRef = useTemplateRef<HTMLDivElement[]>('messagesRef')
// Локальное хранилище сообщений
const messages = defineModel<Exclude<QRCodeDocument['predefinedMessages'], undefined>>({
required: true,
})
// Следим за типом размещения и устанавливаем предустановленные сообщения при его смене
watch(
() => props.placement,
(newPlacement, previousPlacement) => {
let prevDefaultMessages: (typeof defaultMessages)[QRCodeDocument['placement']] = []
if (previousPlacement && previousPlacement in defaultMessages) {
prevDefaultMessages = defaultMessages[previousPlacement]
}
const isMessagesDefault =
messages.value.length === 0 ||
JSON.stringify(messages.value) === JSON.stringify(prevDefaultMessages)
if (isMessagesDefault) {
messages.value = [...(defaultMessages[newPlacement] || [])]
}
},
{ immediate: true },
)
// Функции для управления сообщениями
function addMessage() {
messages.value.push('')
nextTick(() => {
const lastMessageRef = messagesRef.value![messagesRef.value!.length - 1]
lastMessageRef?.querySelector('input')?.focus() // Фокусируемся на последнем поле
})
}
function removeMessage(index: number) {
messages.value.splice(index, 1)
}
</script>
@@ -0,0 +1,46 @@
<template>
<section :class="{ 'active-section': isActive }" ref="section">
<slot />
</section>
</template>
<script setup lang="ts">
import { watch, useTemplateRef } from 'vue'
const props = defineProps<{ isActive: boolean; isNextActiveSection: boolean }>()
const sectionRef = useTemplateRef('section')
// плавная прокрутка при активации секции
watch(
() => sectionRef.value && props.isNextActiveSection,
(isNextActiveSection) => {
if (isNextActiveSection) {
sectionRef.value?.querySelector('input[type="text"]')?.focus()
sectionRef.value?.scrollIntoView({ behavior: 'smooth' })
}
},
{ immediate: true },
)
</script>
<style scoped>
section {
opacity: 0.1;
transition: opacity 0.5s;
pointer-events: none;
}
section {
border-top: 1px solid silver;
padding-top: 1em;
}
section:first-of-type {
border-top: none;
}
section.active-section {
opacity: 1;
pointer-events: all;
}
</style>
@@ -0,0 +1,107 @@
<template>
<div>
<!-- Вывод секций через цикл -->
<CreateSection
v-for="{ name, isActive, isNextActiveSection } in visibleSections"
:key="name"
:isActive
:isNextActiveSection
>
<slot :name="name" />
</CreateSection>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
// Пропсы, которые будут передаваться из родительского компонента
const props = defineProps<{
placement: null | QRCodeDocument['placement']
selectedActions: QRCodeDocument['actions']
messageDeliveryMethod: null | QRCodeDocument['messageDeliveryMethod']
}>()
// Доступные шаги и их условия
const steps = [
{
name: 'placement',
isActive: true,
},
{
name: 'actions',
get isActive() {
return !!props.placement
},
},
{
name: 'delivery',
get isHidden() {
return !props.placement || !props.selectedActions.includes('sendMessage')
},
get isActive() {
return props.selectedActions.length > 0 && props.selectedActions.includes('sendMessage')
},
},
{
name: 'messages',
get isHidden() {
return (
!props.placement ||
!props.selectedActions.includes('sendMessage') ||
props.messageDeliveryMethod === 'telegram'
)
},
get isActive() {
return props.messageDeliveryMethod && props.selectedActions.includes('sendMessage')
},
},
{
name: 'summary',
get isActive() {
return props.selectedActions.includes('sendMessage')
? !!props.messageDeliveryMethod && props.messageDeliveryMethod !== 'telegram'
: props.selectedActions.length > 0
},
},
]
const activeSections = computed(() => {
const result = []
for (const step of steps) {
const isActive = !step.isHidden && step.isActive
if (isActive) {
result.push(step.name)
}
}
return result
})
const nextActiveSectionName = ref<string>('')
watch(
activeSections,
(newActiveSections, oldActiveSteps) => {
nextActiveSectionName.value =
(oldActiveSteps && newActiveSections[oldActiveSteps.length]) ?? newActiveSections.at(-1)!
},
{
immediate: true,
},
)
// Выводимые секции
const visibleSections = computed(() => {
const result = []
for (const step of steps) {
if (step.isHidden) continue
result.push({
name: step.name,
isActive: activeSections.value.includes(step.name),
isNextActiveSection: nextActiveSectionName.value === step.name,
})
}
return result
})
</script>
@@ -0,0 +1,21 @@
<template>
<section>
<h2 class="text-xl font-semibold mb-4">Введите название для вашего QRкода</h2>
<p class="text-gray-600 mb-4">Название поможет вам организовать ваши QRкоды.</p>
<form @submit.prevent="emit('nextStep')">
<label for="name" class="block text-gray-700 font-semibold mb-2">Название</label>
<InputText id="name" v-model="name" class="w-full" placeholder="Введите название" />
<p v-if="!name" class="text-red-500 text-sm mt-1">Название обязательно.</p>
<Button type="submit" :label="buttonLabel" class="p-button-md mt-4 mb-4" :disabled="!name" />
</form>
</section>
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
defineProps<{ buttonLabel: string }>()
const name = defineModel<QRCodeDocument['name']>('name')
const emit = defineEmits(['nextStep'])
</script>