Initial VTuber Awards implementation
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Select from 'primevue/select'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
|
||||
import Button from '../components/ui/Button.vue'
|
||||
import Card from '../components/ui/Card.vue'
|
||||
import { useAwardsStore } from '../stores/awards'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const store = useAwardsStore()
|
||||
const authStore = useAuthStore()
|
||||
const selectedCategoryId = ref<number | null>(null)
|
||||
const selectedCandidateId = ref<number | null>(null)
|
||||
const submitting = ref(false)
|
||||
const submitMessage = ref('')
|
||||
const submitError = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadHomeData()
|
||||
selectedCategoryId.value = store.categories.categories[0]?.id ?? null
|
||||
})
|
||||
|
||||
const categoryOptions = computed(() =>
|
||||
store.categories.categories.map((category) => ({
|
||||
label: category.name,
|
||||
value: category.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const category = computed(() =>
|
||||
store.categories.categories.find((item) => item.id === selectedCategoryId.value) ?? store.categories.categories[0],
|
||||
)
|
||||
|
||||
async function submitVote() {
|
||||
if (!category.value || !selectedCandidateId.value) return
|
||||
|
||||
submitting.value = true
|
||||
submitMessage.value = ''
|
||||
submitError.value = ''
|
||||
|
||||
try {
|
||||
const response = await store.submitVote({
|
||||
seasonId: store.categories.seasonId,
|
||||
twitchUserId: authStore.session?.twitchUserId ?? '',
|
||||
entries: [
|
||||
{
|
||||
categoryId: category.value.id,
|
||||
candidateId: selectedCandidateId.value,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
submitMessage.value = `Ballot #${response.ballotId} mit ${response.entries} Eintrag gespeichert.`
|
||||
} catch (error) {
|
||||
submitError.value = error instanceof Error ? error.message : 'Vote konnte nicht gespeichert werden.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-10 pb-14">
|
||||
<div class="space-y-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.35em] text-amber-500">Voting</p>
|
||||
<h1 class="max-w-[12ch] font-[Cormorant_Garamond] text-6xl leading-[0.92] text-violet-800">Ein ruhiger, schneller Community-Voting-Flow</h1>
|
||||
<p class="max-w-3xl text-lg leading-8 text-slate-600">
|
||||
Der V2-Flow priorisiert geringe Reibung: Twitch Login, ein Kandidat pro Kategorie, spaeter editierbar bis zur Deadline und klarer Review-Screen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card class="p-7">
|
||||
<div class="grid gap-7 lg:grid-cols-[0.72fr_1.28fr]">
|
||||
<div class="space-y-5">
|
||||
<p v-if="!authStore.isLoggedIn" class="rounded-[26px] border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
||||
Bitte zuerst ueber den Header mit einem Twitch-Account einloggen, damit deine Stimme gespeichert werden kann.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="text-sm font-semibold text-slate-600">Kategorie</label>
|
||||
<Select
|
||||
v-model="selectedCategoryId"
|
||||
:options="categoryOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">{{ category?.groupName }}</p>
|
||||
<h2 class="font-[Cormorant_Garamond] text-5xl text-violet-800">{{ category?.name }}</h2>
|
||||
<p class="text-slate-600">{{ category?.description }}</p>
|
||||
<div class="rounded-[28px] bg-violet-50/70 p-6 text-sm leading-7 text-slate-600">
|
||||
Nur eine Stimme pro Kategorie. Videos/Clips koennen spaeter direkt auf Karten oder Detailmodals referenziert werden.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<label
|
||||
v-for="candidate in category?.candidates ?? []"
|
||||
:key="candidate.id"
|
||||
class="flex cursor-pointer items-center justify-between rounded-[26px] border border-violet-100 bg-white/85 px-5 py-5 transition hover:border-violet-300 hover:bg-white"
|
||||
>
|
||||
<div>
|
||||
<p class="font-semibold text-slate-800">{{ candidate.displayName }}</p>
|
||||
<p class="text-sm text-slate-500">{{ candidate.channelSlug }} · {{ candidate.platform }}</p>
|
||||
</div>
|
||||
<RadioButton
|
||||
v-model="selectedCandidateId"
|
||||
:input-id="`candidate-${candidate.id}`"
|
||||
:name="category?.name"
|
||||
:value="candidate.id"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-7 flex flex-wrap items-center justify-between gap-4 rounded-[28px] bg-violet-50/60 px-6 py-5">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-slate-600">
|
||||
Auswahl:
|
||||
<strong class="text-violet-700">
|
||||
{{ category?.candidates.find((candidate) => candidate.id === selectedCandidateId)?.displayName ?? 'Noch keine Stimme abgegeben' }}
|
||||
</strong>
|
||||
</p>
|
||||
<p v-if="submitMessage" class="text-sm text-emerald-700">{{ submitMessage }}</p>
|
||||
<p v-if="submitError" class="text-sm text-rose-700">{{ submitError }}</p>
|
||||
</div>
|
||||
<Button :disabled="submitting || !authStore.isLoggedIn || !selectedCandidateId" @click="submitVote">
|
||||
{{ submitting ? 'Speichert ...' : 'Stimme speichern' }}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user