CreateView.vue
5,408 bytes
| 1 | <script setup lang="ts"> |
|---|---|
| 2 | import { ref } from 'vue' |
| 3 | import { useRouter, useRoute } from 'vue-router' |
| 4 | import { useI18n } from 'vue-i18n' |
| 5 | import PollService from '@/services/PollService' |
| 6 | |
| 7 | const router = useRouter() |
| 8 | const route = useRoute() |
| 9 | const { t } = useI18n() |
| 10 | |
| 11 | const tripId = route.params.tripId as string |
| 12 | |
| 13 | const question = ref('') |
| 14 | const allowMultipleVotes = ref(false) |
| 15 | const isAnonymous = ref(false) |
| 16 | const options = ref<string[]>(['', '']) |
| 17 | const errors = ref<string[]>([]) |
| 18 | const isSaving = ref(false) |
| 19 | |
| 20 | function addOption() { |
| 21 | options.value.push('') |
| 22 | } |
| 23 | |
| 24 | function removeOption(index: number) { |
| 25 | if (options.value.length > 2) { |
| 26 | options.value.splice(index, 1) |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | async function handleSubmit() { |
| 31 | errors.value = [] |
| 32 | |
| 33 | const filledOptions = options.value.filter((o) => o.trim() !== '') |
| 34 | if (filledOptions.length < 2) { |
| 35 | errors.value = [t('polls.create.atLeastTwo')] |
| 36 | return |
| 37 | } |
| 38 | |
| 39 | isSaving.value = true |
| 40 | |
| 41 | const result = await PollService.create({ |
| 42 | tripId, |
| 43 | question: question.value, |
| 44 | allowMultipleVotes: allowMultipleVotes.value, |
| 45 | isAnonymous: isAnonymous.value, |
| 46 | options: filledOptions, |
| 47 | }) |
| 48 | |
| 49 | if (result.errors) { |
| 50 | errors.value = result.errors |
| 51 | } else { |
| 52 | router.push({ name: 'PollsIndex', params: { tripId } }) |
| 53 | } |
| 54 | |
| 55 | isSaving.value = false |
| 56 | } |
| 57 | </script> |
| 58 | |
| 59 | <template> |
| 60 | <div style="max-width: 600px; margin: 0 auto"> |
| 61 | <!-- Header --> |
| 62 | <div class="sa-gradient-header sa-gradient-header-blue"> |
| 63 | <h2 class="mb-0">{{ t('polls.create.title') }}</h2> |
| 64 | </div> |
| 65 | |
| 66 | <!-- Errors --> |
| 67 | <div v-if="errors.length" class="sa-card-static sa-card-accent sa-card-accent-danger mb-4"> |
| 68 | <div class="sa-card-body"> |
| 69 | <div v-for="error in errors" :key="error"> |
| 70 | <i class="bi bi-exclamation-triangle me-2"></i>{{ error }} |
| 71 | </div> |
| 72 | </div> |
| 73 | </div> |
| 74 | |
| 75 | <!-- Form --> |
| 76 | <div class="sa-card-static"> |
| 77 | <div class="sa-card-body"> |
| 78 | <form @submit.prevent="handleSubmit"> |
| 79 | <!-- Question --> |
| 80 | <div class="mb-4"> |
| 81 | <label for="question" class="form-label">{{ t('polls.create.question') }}</label> |
| 82 | <input |
| 83 | id="question" |
| 84 | v-model="question" |
| 85 | type="text" |
| 86 | class="form-control" |
| 87 | :placeholder="t('polls.create.questionPlaceholder')" |
| 88 | required |
| 89 | /> |
| 90 | </div> |
| 91 | |
| 92 | <!-- Toggle: Allow Multiple Votes --> |
| 93 | <div class="d-flex align-items-center justify-content-between mb-3 p-3" style="background: var(--sa-gray-50); border-radius: var(--sa-radius-sm)"> |
| 94 | <div> |
| 95 | <div class="fw-bold" style="font-size: 0.938rem">{{ t('polls.create.allowMultiple') }}</div> |
| 96 | <div style="font-size: 0.8rem; color: var(--sa-gray-500)">{{ t('polls.create.allowMultipleHelp') }}</div> |
| 97 | </div> |
| 98 | <input |
| 99 | id="allowMultiple" |
| 100 | v-model="allowMultipleVotes" |
| 101 | type="checkbox" |
| 102 | class="sa-toggle" |
| 103 | /> |
| 104 | </div> |
| 105 | |
| 106 | <!-- Toggle: Anonymous Voting --> |
| 107 | <div class="d-flex align-items-center justify-content-between mb-4 p-3" style="background: var(--sa-gray-50); border-radius: var(--sa-radius-sm)"> |
| 108 | <div> |
| 109 | <div class="fw-bold" style="font-size: 0.938rem">{{ t('polls.create.anonymous') }}</div> |
| 110 | <div style="font-size: 0.8rem; color: var(--sa-gray-500)">{{ t('polls.create.anonymousHelp') }}</div> |
| 111 | </div> |
| 112 | <input |
| 113 | id="isAnonymous" |
| 114 | v-model="isAnonymous" |
| 115 | type="checkbox" |
| 116 | class="sa-toggle" |
| 117 | /> |
| 118 | </div> |
| 119 | |
| 120 | <!-- Options --> |
| 121 | <div class="mb-4"> |
| 122 | <label class="form-label">{{ t('polls.create.options') }}</label> |
| 123 | <div v-for="(_, index) in options" :key="index" class="d-flex gap-2 mb-2"> |
| 124 | <input |
| 125 | v-model="options[index]" |
| 126 | type="text" |
| 127 | class="form-control" |
| 128 | :placeholder="t('polls.create.optionPlaceholder', { n: index + 1 })" |
| 129 | /> |
| 130 | <button |
| 131 | v-if="options.length > 2" |
| 132 | type="button" |
| 133 | class="sa-btn sa-btn-icon sa-btn-sm sa-btn-ghost" |
| 134 | style="flex-shrink: 0" |
| 135 | @click="removeOption(index)" |
| 136 | > |
| 137 | <i class="bi bi-x-lg"></i> |
| 138 | </button> |
| 139 | </div> |
| 140 | <button |
| 141 | type="button" |
| 142 | class="sa-btn sa-btn-ghost sa-btn-sm" |
| 143 | @click="addOption" |
| 144 | > |
| 145 | <i class="bi bi-plus-lg"></i> {{ t('polls.create.addOption') }} |
| 146 | </button> |
| 147 | </div> |
| 148 | |
| 149 | <!-- Actions --> |
| 150 | <div class="d-flex gap-2"> |
| 151 | <button |
| 152 | type="button" |
| 153 | class="sa-btn sa-btn-ghost" |
| 154 | @click="router.push({ name: 'PollsIndex', params: { tripId } })" |
| 155 | > |
| 156 | {{ t('common.cancel') }} |
| 157 | </button> |
| 158 | <button |
| 159 | type="submit" |
| 160 | class="sa-btn sa-btn-primary" |
| 161 | :class="{ 'sa-btn-loading': isSaving }" |
| 162 | :disabled="isSaving" |
| 163 | > |
| 164 | {{ isSaving ? t('polls.create.creating') : t('polls.create.createPoll') }} |
| 165 | </button> |
| 166 | </div> |
| 167 | </form> |
| 168 | </div> |
| 169 | </div> |
| 170 | </div> |
| 171 | </template> |
| 172 | |