refactor(hunt): replace admin middleware with auth and implement membership state

- Change middleware from 'admin' to 'auth' on hunt and quest admin pages
- Add global hunt store to track current hunt ID and membership status
- Use hunt store membership state to conditionally display admin UI elements
- Handle fetch errors gracefully with alerts and back navigation buttons
- Update admin UI logic to depend on membership state instead of user role
- Add membership state update after API calls in hunt, quest, teams, and admin pages
- Refactor UI templates to show error messages when fetch fails
- Centralize membership logic for better authorization control in frontend
This commit is contained in:
2026-07-30 22:33:47 +02:00
parent cfda014760
commit 370ba3406f
7 changed files with 351 additions and 261 deletions
+12 -3
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { sluggy } from '#imports'; import { sluggy } from '#imports';
import { useHuntStore } from '~/stores/hunt';
import { useTitleStore } from '~/stores/title'; import { useTitleStore } from '~/stores/title';
const { loggedIn, clear, user } = useUserSession(); const { loggedIn, clear, user } = useUserSession();
@@ -8,10 +9,18 @@ const localePath = useLocalePath();
const route = useRoute(); const route = useRoute();
const breakpoints = useBreakpoint(); const breakpoints = useBreakpoint();
const titleStore = useTitleStore(); const titleStore = useTitleStore();
const huntStore = useHuntStore();
const isSidebarVisible = ref(breakpoints.mdUp); const isSidebarVisible = ref(breakpoints.mdUp);
const slugs = computed(() => checkSlug(route.params)); const slugs = computed(() => checkSlug(route.params));
const isHuntAdmin = computed(
() =>
slugs.value?.huntSlug !== undefined &&
huntStore.huntId === slugs.value.huntSlug.id &&
huntStore.isMember
);
</script> </script>
<template> <template>
@@ -75,7 +84,7 @@ const slugs = computed(() => checkSlug(route.params));
</VaSidebarItemContent> </VaSidebarItemContent>
</VaSidebarItem> </VaSidebarItem>
<VaSidebarItem <VaSidebarItem
v-if="slugs?.huntSlug && user?.role === 'ADMIN'" v-if="slugs?.huntSlug && isHuntAdmin"
text-color="danger" text-color="danger"
hover-color="danger" hover-color="danger"
active-color="onDanger" active-color="onDanger"
@@ -90,7 +99,7 @@ const slugs = computed(() => checkSlug(route.params));
</VaSidebarItemContent> </VaSidebarItemContent>
</VaSidebarItem> </VaSidebarItem>
<VaSidebarItem <VaSidebarItem
v-if="slugs?.huntSlug && user?.role === 'ADMIN'" v-if="slugs?.huntSlug && isHuntAdmin"
text-color="danger" text-color="danger"
hover-color="danger" hover-color="danger"
active-color="onDanger" active-color="onDanger"
@@ -125,7 +134,7 @@ const slugs = computed(() => checkSlug(route.params));
</VaSidebarItemContent> </VaSidebarItemContent>
</VaSidebarItem> </VaSidebarItem>
<VaSidebarItem <VaSidebarItem
v-if="slugs?.huntSlug && slugs?.questSlug && user?.role === 'ADMIN'" v-if="slugs?.huntSlug && slugs?.questSlug && isHuntAdmin"
text-color="danger" text-color="danger"
hover-color="danger" hover-color="danger"
active-color="onDanger" active-color="onDanger"
+60 -44
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { sluggy } from '#imports'; import { parseError, sluggy } from '#imports';
import { useI18nKey } from '~/composables/useI18nKey'; import { useI18nKey } from '~/composables/useI18nKey';
import { useHuntStore } from '~/stores/hunt';
import { useTitleStore } from '~/stores/title'; import { useTitleStore } from '~/stores/title';
definePageMeta({ definePageMeta({
middleware: ['admin'], middleware: ['auth'],
keepalive: false keepalive: false
}); });
@@ -18,8 +19,11 @@ const router = useRouter();
const { huntSlug } = app.$slugData; const { huntSlug } = app.$slugData;
const { name, id } = app.$slugData.questSlug!; const { name, id } = app.$slugData.questSlug!;
const titleStore = useTitleStore(); const titleStore = useTitleStore();
const huntStore = useHuntStore();
const { data, refresh } = await useFetch(`/api/admin/quest/${id}`); const { data, refresh, error } = await useFetch(`/api/admin/quest/${id}`);
huntStore.setMembership(huntSlug!.id, !error.value);
useHead({ useHead({
title: t('layouts.title', { title: t('layouts.title', {
@@ -38,52 +42,64 @@ titleStore.title = data.value ? tKey(data.value, 'title').value : undefined;
<template> <template>
<div> <div>
<h1 class="my-4 text-3xl">Admin{{ data?.title_de }}</h1> <div v-if="error">
<VaButtonGroup> <VaAlert color="danger" class="my-4">
{{ t(parseError(error)) }}
</VaAlert>
<VaButton <VaButton
icon="arrow_back" icon="arrow_back"
:to=" :to="localePath(`/hunt/${sluggy(huntSlug!)}`)"
localePath(
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
)
"
>{{ t('page.common.back_to_hunt') }}</VaButton >{{ t('page.common.back_to_hunt') }}</VaButton
> >
<VaButton icon="replay" color="success" @click="refresh"
>Refresh</VaButton
>
</VaButtonGroup>
<div v-if="data" class="my-2 flex flex-col gap-2 p-4">
<p>{{ tKey(data, 'description') }}</p>
<p v-if="hasKey(data, 'question')">{{ tKey(data, 'question') }}</p>
</div> </div>
<div class="mb-4 grid grid-cols-2 gap-2 p-2 md:flex md:flex-row"> <template v-else>
<VaChip outline>In progress</VaChip> <h1 class="my-4 text-3xl">Admin{{ data?.title_de }}</h1>
<VaChip color="primary">Reviewable</VaChip> <VaButtonGroup>
<VaChip color="danger">Review missing</VaChip> <VaButton
<VaChip color="success">Reviewed</VaChip> icon="arrow_back"
</div> :to="
<div v-if="data"> localePath(
<p class="text-xl">{{ data.points }} Points</p> `/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
<p v-if="data.extraPoints" class="text-xl"> )
{{ data.extraPoints }} Extra points "
</p> >{{ t('page.common.back_to_hunt') }}</VaButton
<p v-if="hasKey(data, 'extraText')"> >
Extra: {{ tKey(data, 'extraText') }} <VaButton icon="replay" color="success" @click="refresh"
</p> >Refresh</VaButton
</div> >
<div v-if="data && data.answers.length > 0"> </VaButtonGroup>
<VaAccordion multiple stateful> <div v-if="data" class="my-2 flex flex-col gap-2 p-4">
<AdminAnswer <p>{{ tKey(data, 'description') }}</p>
v-for="answer in data.answers" <p v-if="hasKey(data, 'question')">{{ tKey(data, 'question') }}</p>
:key="answer.id" </div>
:answer="answer" <div class="mb-4 grid grid-cols-2 gap-2 p-2 md:flex md:flex-row">
:quest="data" <VaChip outline>In progress</VaChip>
@update="refresh" <VaChip color="primary">Reviewable</VaChip>
/> <VaChip color="danger">Review missing</VaChip>
</VaAccordion> <VaChip color="success">Reviewed</VaChip>
</div> </div>
<p v-else class="mt-4 text-lg">No answers yet</p> <div v-if="data">
<p class="text-xl">{{ data.points }} Points</p>
<p v-if="data.extraPoints" class="text-xl">
{{ data.extraPoints }} Extra points
</p>
<p v-if="hasKey(data, 'extraText')">
Extra: {{ tKey(data, 'extraText') }}
</p>
</div>
<div v-if="data && data.answers.length > 0">
<VaAccordion multiple stateful>
<AdminAnswer
v-for="answer in data.answers"
:key="answer.id"
:answer="answer"
:quest="data"
@update="refresh"
/>
</VaAccordion>
</div>
<p v-else class="mt-4 text-lg">No answers yet</p>
</template>
</div> </div>
</template> </template>
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseError, sluggy } from '#imports'; import { parseError, sluggy, useHuntStore } from '#imports';
import { compressImage, createImageURL } from '#shared/utils/image'; import { compressImage, createImageURL } from '#shared/utils/image';
import ClickableImage from '~/components/ClickableImage.vue'; import ClickableImage from '~/components/ClickableImage.vue';
import MultilineString from '~/components/MultilineString.vue'; import MultilineString from '~/components/MultilineString.vue';
@@ -22,6 +22,13 @@ const localePath = useLocalePath();
const { tKey, hasKey, tSluggy } = useI18nKey(); const { tKey, hasKey, tSluggy } = useI18nKey();
const titleStore = useTitleStore(); const titleStore = useTitleStore();
const huntStore = useHuntStore();
const isHuntAdmin = computed(
() =>
huntSlug !== undefined &&
huntStore.huntId === huntSlug.id &&
huntStore.isMember
);
const { data, pending, refresh, error } = await useFetch( const { data, pending, refresh, error } = await useFetch(
`/api/quest/${questSlug?.id}` `/api/quest/${questSlug?.id}`
@@ -195,11 +202,11 @@ async function submit() {
>{{ t('layouts.refresh') }}</VaButton >{{ t('layouts.refresh') }}</VaButton
> >
<VaButton <VaButton
v-if="user && user.role === 'ADMIN'" v-if="user && isHuntAdmin"
color="danger" color="danger"
:to=" :to="
localePath( localePath(
`/hunt/${data?.hunt ? tSluggy(data.hunt).value : sluggy(huntSlug!)}/${data ? tSluggy(data).value : sluggy(questSlug!)}}/admin` `/hunt/${data?.hunt ? tSluggy(data.hunt).value : sluggy(huntSlug!)}/${data ? tSluggy(data).value : sluggy(questSlug!)}/admin`
) )
" "
>{{ t('page.common.admin') }}</VaButton >{{ t('page.common.admin') }}</VaButton
@@ -268,7 +275,7 @@ async function submit() {
</VaCardContent> </VaCardContent>
</VaCard> </VaCard>
<VaCard <VaCard
v-if="user && user.role !== 'ADMIN' && data.teamId" v-if="user && !isHuntAdmin && data.teamId"
stripe stripe
:stripe-color="data.answer?.final ? 'success' : 'primary'" :stripe-color="data.answer?.final ? 'success' : 'primary'"
> >
+218 -197
View File
@@ -1,12 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseError, sluggy } from '#imports';
import { createImageURL } from '#shared/utils/image'; import { createImageURL } from '#shared/utils/image';
import superjson, { type SuperJSONResult } from 'superjson'; import superjson, { type SuperJSONResult } from 'superjson';
import { useI18nKey } from '~/composables/useI18nKey'; import { useI18nKey } from '~/composables/useI18nKey';
import { useHuntStore } from '~/stores/hunt';
import { useTitleStore } from '~/stores/title'; import { useTitleStore } from '~/stores/title';
import { fullDate } from '~~/server/utils/date'; import { fullDate } from '~~/server/utils/date';
definePageMeta({ definePageMeta({
middleware: ['admin'], middleware: ['auth'],
keepalive: false keepalive: false
}); });
@@ -22,14 +24,20 @@ const { init } = useToast();
const { name, id } = app.$slugData.huntSlug!; const { name, id } = app.$slugData.huntSlug!;
const titleStore = useTitleStore(); const titleStore = useTitleStore();
const huntStore = useHuntStore();
const { data, pending, refresh } = await useFetch(`/api/admin/hunt/${id}`, { const { data, pending, refresh, error } = await useFetch(
transform: (value) => { `/api/admin/hunt/${id}`,
return superjson.deserialize( {
value as unknown as SuperJSONResult transform: (value) => {
) as unknown as typeof value; return superjson.deserialize(
value as unknown as SuperJSONResult
) as unknown as typeof value;
}
} }
}); );
huntStore.setMembership(id, !error.value);
const formData = useCloned(data, { const formData = useCloned(data, {
clone: (source) => superjson.deserialize(superjson.serialize(source)) clone: (source) => superjson.deserialize(superjson.serialize(source))
@@ -204,199 +212,212 @@ async function deleteImage() {
<template> <template>
<div> <div>
<h1>admin</h1> <div v-if="error">
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4"> <VaAlert color="danger" class="my-4">
<VaInput {{ t(parseError(error)) }}
v-model="formData.cloned.value.name_de" </VaAlert>
label="Name DE"
clearable
:clear-value="data.name_de"
clearable-icon="replay"
/>
<VaInput
v-model="formData.cloned.value.name_en"
label="Name EN"
clearable
:clear-value="data.name_en"
clearable-icon="replay"
/>
<VaTextarea
v-model="formData.cloned.value.description_de"
label="Description DE"
clearable
:clear-value="data.description_de"
clearable-icon="replay"
/>
<VaTextarea
v-model="formData.cloned.value.description_en"
label="Description EN"
clearable
:clear-value="data.description_en"
clearable-icon="replay"
/>
<VaCheckbox v-model="formData.cloned.value.virtual" label="Virtual" />
<VaCheckbox
v-model="formData.cloned.value.allowJoin"
label="Allow Join"
/>
<VaCheckbox
v-model="formData.cloned.value.revealQuests"
label="Reveal Quests"
/>
<VaCheckbox
v-model="formData.cloned.value.revealAnswers"
label="Reveal Answers"
/>
<div class="flex flex-row gap-2">
<VaCheckbox
label="Start?"
:model-value="!!formData.cloned.value.start"
@update:model-value="
(newVal: boolean) =>
(formData.cloned.value!.start = newVal ? new Date() : null)
"
/>
<VaDateInput
v-if="formData.cloned.value.start"
v-model="formData.cloned.value.start"
label="Start"
manual-input
/>
<VaTimeInput
v-if="formData.cloned.value.start"
v-model="formData.cloned.value.start"
label="Start"
view="seconds"
manual-input
/>
</div>
<div class="flex flex-row gap-2">
<VaCheckbox
label="End?"
:model-value="!!formData.cloned.value.end"
@update:model-value="
(newVal: boolean) =>
(formData.cloned.value!.end = newVal ? new Date() : null)
"
/>
<VaDateInput
v-if="formData.cloned.value.end"
v-model="formData.cloned.value.end"
label="End"
manual-input
/>
<VaTimeInput
v-if="formData.cloned.value.end"
v-model="formData.cloned.value.end"
label="End"
view="seconds"
manual-input
/>
</div>
<p>
Last update:
{{ d(data.updatedAt, fullDate) }}
</p>
<VaButtonGroup :loading="pending" :disabled="pending">
<VaButton
icon="replay"
:disabled="!formData.isModified.value"
@click.prevent="formData.sync()"
>Reset all values</VaButton
>
<VaButton
:disabled="formData.isModified.value"
@click.prevent="refresh()"
>Refresh</VaButton
>
<VaButton :disabled="!formData.isModified.value" @click.prevent="submit"
>Save</VaButton
>
</VaButtonGroup>
</VaForm>
<VaDivider />
<h2 class="text-2xl">Image</h2>
<div>
<VaImage
v-if="newImage"
:src="createImageURL(newImage)"
class="max-h-60 w-full"
fit="contain"
lazy
/>
<VaImage
v-else-if="data?.picture"
:src="data?.picture?.url"
class="max-h-60 w-full"
fit="contain"
lazy
>
<template #loader> <VaProgressCircle indeterminate /> </template
></VaImage>
<VaFileUpload
v-model="newImage"
:disabled="uploading || pending"
type="single"
file-types="jpg,png,jpeg"
/>
<VaAlert
v-if="imageError.length > 0"
color="danger"
class="w-full text-center"
>{{ t(imageError) }}</VaAlert
>
<VaButtonGroup :disabled="uploading || pending">
<VaButton color="success" :disabled="!newImage" @click="updateImage">
Upload new image
</VaButton>
<VaButton
color="danger"
icon="delete"
:disabled="!data?.picture"
@click="deleteImage"
>Delete image</VaButton
>
</VaButtonGroup>
</div>
<VaDivider />
<h2 class="text-2xl">Hunt admin members:</h2>
<div
v-if="data"
class="mt-8 grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5"
>
<VaButton <VaButton
v-for="member in data.members" icon="arrow_back"
:key="member.member.id" :to="localePath(`/hunt/${sluggy(app.$slugData.huntSlug!)}`)"
color="danger" >{{ t('page.common.back_to_hunt') }}</VaButton
:disabled="loading"
:loading="loading"
@click="kick(member)"
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
>
<VaChip v-if="data.members.length <= 0">No members yet</VaChip>
</div>
<div v-if="data" class="mt-8 flex flex-col gap-2 md:flex-row">
<VaSelect
v-model="newUser"
v-model:search="search"
label="New Admin Member"
placeholder="Start typing their email..."
autocomplete
highlight-matched-text
:options="users"
:loading="searchPending"
track-by="id"
text-by="email"
/><VaButton
icon="add"
color="danger"
:disabled="!newUser || loading"
:loading="loading"
@click="addUser"
>Add admin</VaButton
> >
</div> </div>
<template v-else>
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
<VaInput
v-model="formData.cloned.value.name_de"
label="Name DE"
clearable
:clear-value="data.name_de"
clearable-icon="replay"
/>
<VaInput
v-model="formData.cloned.value.name_en"
label="Name EN"
clearable
:clear-value="data.name_en"
clearable-icon="replay"
/>
<VaTextarea
v-model="formData.cloned.value.description_de"
label="Description DE"
clearable
:clear-value="data.description_de"
clearable-icon="replay"
/>
<VaTextarea
v-model="formData.cloned.value.description_en"
label="Description EN"
clearable
:clear-value="data.description_en"
clearable-icon="replay"
/>
<VaCheckbox v-model="formData.cloned.value.virtual" label="Virtual" />
<VaCheckbox
v-model="formData.cloned.value.allowJoin"
label="Allow Join"
/>
<VaCheckbox
v-model="formData.cloned.value.revealQuests"
label="Reveal Quests"
/>
<VaCheckbox
v-model="formData.cloned.value.revealAnswers"
label="Reveal Answers"
/>
<div class="flex flex-row gap-2">
<VaCheckbox
label="Start?"
:model-value="!!formData.cloned.value.start"
@update:model-value="
(newVal: boolean) =>
(formData.cloned.value!.start = newVal ? new Date() : null)
"
/>
<VaDateInput
v-if="formData.cloned.value.start"
v-model="formData.cloned.value.start"
label="Start"
manual-input
/>
<VaTimeInput
v-if="formData.cloned.value.start"
v-model="formData.cloned.value.start"
label="Start"
view="seconds"
manual-input
/>
</div>
<div class="flex flex-row gap-2">
<VaCheckbox
label="End?"
:model-value="!!formData.cloned.value.end"
@update:model-value="
(newVal: boolean) =>
(formData.cloned.value!.end = newVal ? new Date() : null)
"
/>
<VaDateInput
v-if="formData.cloned.value.end"
v-model="formData.cloned.value.end"
label="End"
manual-input
/>
<VaTimeInput
v-if="formData.cloned.value.end"
v-model="formData.cloned.value.end"
label="End"
view="seconds"
manual-input
/>
</div>
<p>
Last update:
{{ d(data.updatedAt, fullDate) }}
</p>
<VaButtonGroup :loading="pending" :disabled="pending">
<VaButton
icon="replay"
:disabled="!formData.isModified.value"
@click.prevent="formData.sync()"
>Reset all values</VaButton
>
<VaButton
:disabled="formData.isModified.value"
@click.prevent="refresh()"
>Refresh</VaButton
>
<VaButton
:disabled="!formData.isModified.value"
@click.prevent="submit"
>Save</VaButton
>
</VaButtonGroup>
</VaForm>
<VaDivider />
<h2 class="text-2xl">Image</h2>
<div>
<VaImage
v-if="newImage"
:src="createImageURL(newImage)"
class="max-h-60 w-full"
fit="contain"
lazy
/>
<VaImage
v-else-if="data?.picture"
:src="data?.picture?.url"
class="max-h-60 w-full"
fit="contain"
lazy
>
<template #loader> <VaProgressCircle indeterminate /> </template
></VaImage>
<VaFileUpload
v-model="newImage"
:disabled="uploading || pending"
type="single"
file-types="jpg,png,jpeg"
/>
<VaAlert
v-if="imageError.length > 0"
color="danger"
class="w-full text-center"
>{{ t(imageError) }}</VaAlert
>
<VaButtonGroup :disabled="uploading || pending">
<VaButton color="success" :disabled="!newImage" @click="updateImage">
Upload new image
</VaButton>
<VaButton
color="danger"
icon="delete"
:disabled="!data?.picture"
@click="deleteImage"
>Delete image</VaButton
>
</VaButtonGroup>
</div>
<VaDivider />
<h2 class="text-2xl">Hunt admin members:</h2>
<div
v-if="data"
class="mt-8 grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5"
>
<VaButton
v-for="member in data.members"
:key="member.member.id"
color="danger"
:disabled="loading"
:loading="loading"
@click="kick(member)"
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
>
<VaChip v-if="data.members.length <= 0">No members yet</VaChip>
</div>
<div v-if="data" class="mt-8 flex flex-col gap-2 md:flex-row">
<VaSelect
v-model="newUser"
v-model:search="search"
label="New Admin Member"
placeholder="Start typing their email..."
autocomplete
highlight-matched-text
:options="users"
:loading="searchPending"
track-by="id"
text-by="email"
/><VaButton
icon="add"
color="danger"
:disabled="!newUser || loading"
:loading="loading"
@click="addUser"
>Add admin</VaButton
>
</div>
</template>
</div> </div>
</template> </template>
+4
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseError } from '#imports'; import { parseError } from '#imports';
import TeamAddModal from '~/components/TeamAddModal.vue'; import TeamAddModal from '~/components/TeamAddModal.vue';
import { useHuntStore } from '~/stores/hunt';
import { useSettingsStore } from '~/stores/settings'; import { useSettingsStore } from '~/stores/settings';
import { useTitleStore } from '~/stores/title'; import { useTitleStore } from '~/stores/title';
import { fullDate } from '~~/server/utils/date'; import { fullDate } from '~~/server/utils/date';
@@ -15,9 +16,12 @@ const { init } = useToast();
const { name, id } = app.$slugData.huntSlug!; const { name, id } = app.$slugData.huntSlug!;
const titleStore = useTitleStore(); const titleStore = useTitleStore();
const huntStore = useHuntStore();
const { data, pending, refresh, error } = await useFetch(`/api/hunt/${id}`); const { data, pending, refresh, error } = await useFetch(`/api/hunt/${id}`);
huntStore.setMembership(id, data.value?.isMember ?? false);
const settings = useSettingsStore(); const settings = useSettingsStore();
useHead({ useHead({
+32 -13
View File
@@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseError, sluggy } from '#imports';
import Team from '~/components/admin/Team.vue'; import Team from '~/components/admin/Team.vue';
import { useHuntStore } from '~/stores/hunt';
import { useTitleStore } from '~/stores/title'; import { useTitleStore } from '~/stores/title';
definePageMeta({ definePageMeta({
middleware: ['admin'] middleware: ['auth']
}); });
const app = useNuxtApp(); const app = useNuxtApp();
@@ -15,8 +17,11 @@ const router = useRouter();
const { name, id } = app.$slugData.huntSlug!; const { name, id } = app.$slugData.huntSlug!;
const titleStore = useTitleStore(); const titleStore = useTitleStore();
const huntStore = useHuntStore();
const { data, refresh } = await useFetch(`/api/admin/hunt/${id}/teams`); const { data, refresh, error } = await useFetch(`/api/admin/hunt/${id}/teams`);
huntStore.setMembership(id, !error.value);
useHead({ useHead({
title: t('layouts.title', { title: t('layouts.title', {
@@ -38,17 +43,31 @@ export type Team = Data['teams'][number];
<template> <template>
<div> <div>
<h1>teams</h1> <div v-if="error">
<VaButton color="success" icon="replay" @click="refresh">Refresh</VaButton> <VaAlert color="danger" class="my-4">
<VaAccordion v-if="data" stateful> {{ t(parseError(error)) }}
<Team </VaAlert>
v-for="team in data.teams" <VaButton
:key="team.id" icon="arrow_back"
:team="team" :to="localePath(`/hunt/${sluggy(app.$slugData.huntSlug!)}`)"
:hunt-id="id" >{{ t('page.common.back_to_hunt') }}</VaButton
@refresh="refresh" >
/> </div>
</VaAccordion> <template v-else>
<h1>teams</h1>
<VaButton color="success" icon="replay" @click="refresh"
>Refresh</VaButton
>
<VaAccordion v-if="data" stateful>
<Team
v-for="team in data.teams"
:key="team.id"
:team="team"
:hunt-id="id"
@refresh="refresh"
/>
</VaAccordion>
</template>
</div> </div>
</template> </template>
+14
View File
@@ -0,0 +1,14 @@
export const useHuntStore = defineStore('hunt', () => {
const huntId = ref<number>();
const isMember = ref(false);
function setMembership(id: number, member: boolean) {
huntId.value = id;
isMember.value = member;
}
return { huntId, isMember, setMembership };
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useHuntStore, import.meta.hot));
}