Switch to translatable data
This commit is contained in:
@@ -6,7 +6,21 @@ on:
|
|||||||
branches: ['main']
|
branches: ['main']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --include=dev
|
||||||
|
- name: Check formatting
|
||||||
|
run: bun run check
|
||||||
|
- name: Run tests
|
||||||
|
run: bun run test
|
||||||
deploy:
|
deploy:
|
||||||
|
needs: test
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
- [x] Image Diashow
|
- [x] Image Diashow
|
||||||
- **Hunt User**
|
- **Hunt User**
|
||||||
- [x] View all images on one page
|
- [x] View all images on one page
|
||||||
- [ ] i18n data
|
- [x] i18n data
|
||||||
- [ ] Mark image as private
|
- [ ] Mark image as private
|
||||||
- [ ] How-to/Guide/Good explanation
|
- [ ] How-to/Guide/Good explanation
|
||||||
- [ ] Leave/Remove/Edit team
|
- [ ] Leave/Remove/Edit team
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
quest: {
|
quest: {
|
||||||
title: string;
|
title_de: string;
|
||||||
|
title_en: string;
|
||||||
textRequired: boolean;
|
textRequired: boolean;
|
||||||
pictureRequired: boolean;
|
pictureRequired: boolean;
|
||||||
optional: boolean;
|
optional: boolean;
|
||||||
@@ -12,12 +15,13 @@ const props = defineProps<{
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
const { tKey } = useI18nKey();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span>{{ quest.pictureRequired ? '📸' : '📷' }}</span>
|
<span>{{ quest.pictureRequired ? '📸' : '📷' }}</span>
|
||||||
<span title="aaa">{{ quest.textRequired ? '❓' : '❔' }}</span>
|
<span title="aaa">{{ quest.textRequired ? '❓' : '❔' }}</span>
|
||||||
<span class="mx-2">{{ quest.title }}</span>
|
<span class="mx-2">{{ tKey(quest, 'title') }}</span>
|
||||||
<VaChip v-if="quest.answer?.review?.score" color="success" class="float-right"
|
<VaChip v-if="quest.answer?.review?.score" color="success" class="float-right"
|
||||||
>{{ quest.answer?.review?.score || 0 }} / {{ quest.points
|
>{{ quest.answer?.review?.score || 0 }} / {{ quest.points
|
||||||
}}{{ quest.extraPoints ? `+${quest.extraPoints}` : '' }}</VaChip
|
}}{{ quest.extraPoints ? `+${quest.extraPoints}` : '' }}</VaChip
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getFlagEmoji } from '#shared/utils/flag';
|
||||||
import { fullDate } from '~~/server/utils/date';
|
import { fullDate } from '~~/server/utils/date';
|
||||||
|
|
||||||
const { d, t } = useI18n();
|
const { d, t, locale } = useI18n();
|
||||||
|
const { tKey } = useI18nKey();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
answer: {
|
answer: {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -9,6 +12,7 @@ const props = defineProps<{
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
team: { id: number; name: string };
|
team: { id: number; name: string };
|
||||||
final: boolean;
|
final: boolean;
|
||||||
|
lang: string;
|
||||||
review: {
|
review: {
|
||||||
internalNote: string | null;
|
internalNote: string | null;
|
||||||
rankingValue: number | null;
|
rankingValue: number | null;
|
||||||
@@ -25,12 +29,14 @@ const props = defineProps<{
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
quest: {
|
quest: {
|
||||||
title: string;
|
title_de: string;
|
||||||
|
title_en: string;
|
||||||
pictureRequired: boolean;
|
pictureRequired: boolean;
|
||||||
textRequired: boolean;
|
textRequired: boolean;
|
||||||
points: number;
|
points: number;
|
||||||
extraPoints: number | null;
|
extraPoints: number | null;
|
||||||
extraText: string | null;
|
extraText_de: string | null;
|
||||||
|
extraText_en: string | null;
|
||||||
textSolution: string | null;
|
textSolution: string | null;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
@@ -139,7 +145,7 @@ async function submit() {
|
|||||||
<ClickableImage
|
<ClickableImage
|
||||||
v-if="answer.picture?.url"
|
v-if="answer.picture?.url"
|
||||||
:src="answer.picture.url"
|
:src="answer.picture.url"
|
||||||
:title="`${quest.title} – ${answer.team.name}`"
|
:title="`${tKey(quest, 'title')} – ${answer.team.name}`"
|
||||||
class="h-48"
|
class="h-48"
|
||||||
/>
|
/>
|
||||||
<h3 v-if="quest.textSolution">Solution:</h3>
|
<h3 v-if="quest.textSolution">Solution:</h3>
|
||||||
@@ -172,6 +178,17 @@ async function submit() {
|
|||||||
messages="Special showcase at the end, before the final leaderboard"
|
messages="Special showcase at the end, before the final leaderboard"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="answer.lang"
|
||||||
|
:class="answer.lang !== locale ? 'py-2 text-xl text-red-500' : ''"
|
||||||
|
>
|
||||||
|
Users language is {{ t(`locales.${answer.lang}`) }}
|
||||||
|
{{
|
||||||
|
getFlagEmoji(
|
||||||
|
answer.lang.toLowerCase() === 'en' ? 'GB' : answer.lang
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
<VaTextarea
|
<VaTextarea
|
||||||
v-model="formData.cloned.value.review"
|
v-model="formData.cloned.value.review"
|
||||||
label="Review"
|
label="Review"
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ async function kick(member: Team['members'][number]) {
|
|||||||
init({
|
init({
|
||||||
title: 'Error while kicking',
|
title: 'Error while kicking',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
|
// @ts-expect-error weird error stuff
|
||||||
message: e?.message || e?.statusMessage || t(parseError(e))
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -97,6 +98,7 @@ async function addUser() {
|
|||||||
init({
|
init({
|
||||||
title: 'Error while adding',
|
title: 'Error while adding',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
|
// @ts-expect-error weird error stuff
|
||||||
message: e?.message || e?.statusMessage || t(parseError(e))
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,7 @@ async function deleteTeam() {
|
|||||||
init({
|
init({
|
||||||
title: 'Error while deleting',
|
title: 'Error while deleting',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
|
// @ts-expect-error weird error stuff
|
||||||
message: e?.message || e?.statusMessage || t(parseError(e))
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { Locale } from '@intlify/core-base';
|
||||||
|
|
||||||
|
export const useI18nKey = () => {
|
||||||
|
const { locale, locales } = useI18n();
|
||||||
|
|
||||||
|
function tKey<K extends string>(
|
||||||
|
obj: Record<`${K}_${Locale}`, string>,
|
||||||
|
key: K
|
||||||
|
) {
|
||||||
|
return computed(() => obj[`${key}_${locale.value}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tSluggy(
|
||||||
|
obj: { id: number | string } & (
|
||||||
|
| Record<`name_${Locale}`, string | null | undefined>
|
||||||
|
| Record<`title_${Locale}`, string | null | undefined>
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return computed(() =>
|
||||||
|
sluggy({
|
||||||
|
id: obj.id,
|
||||||
|
title: hasKey(obj, 'title') ? tKey(obj, 'title').value : undefined,
|
||||||
|
name: hasKey(obj, 'name') ? tKey(obj, 'name').value : undefined
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasKey<K extends string>(
|
||||||
|
obj:
|
||||||
|
| Record<`${K}_${Locale}`, string | null | undefined>
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| {},
|
||||||
|
key: K
|
||||||
|
): obj is Record<`${K}_${Locale}`, string> {
|
||||||
|
return (
|
||||||
|
!!obj &&
|
||||||
|
locales.value.every(
|
||||||
|
(l) =>
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
Object.prototype.hasOwnProperty.call(obj, `${key}_${l.code}`) &&
|
||||||
|
// @ts-expect-error this is a type check
|
||||||
|
typeof obj[`${key}_${l.code}`] === 'string' &&
|
||||||
|
// @ts-expect-error this is a type check
|
||||||
|
obj[`${key}_${l.code}`]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tKey, hasKey, tSluggy };
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
import { useTitleStore } from '~/stores/title';
|
import { useTitleStore } from '~/stores/title';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -9,6 +10,7 @@ const app = useNuxtApp();
|
|||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { tKey, hasKey, tSluggy } = useI18nKey();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { huntSlug } = app.$slugData;
|
const { huntSlug } = app.$slugData;
|
||||||
@@ -21,7 +23,7 @@ const { data, pending, refresh, error } = await useFetch(
|
|||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('layouts.title', {
|
title: t('layouts.title', {
|
||||||
title: data.value?.title || name
|
title: hasKey(data.value, 'title') ? tKey(data.value!, 'title').value : name
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,8 +33,7 @@ watch(loggedIn, (isLoggedIn) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
titleStore.title = data.value?.title;
|
titleStore.title = data.value ? tKey(data.value, 'title').value : undefined;
|
||||||
// TODO: handle data!
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -40,7 +41,11 @@ titleStore.title = data.value?.title;
|
|||||||
<VaButtonGroup>
|
<VaButtonGroup>
|
||||||
<VaButton
|
<VaButton
|
||||||
icon="arrow_back"
|
icon="arrow_back"
|
||||||
:to="$localePath(`/hunt/${sluggy(data?.hunt || huntSlug!)}`)"
|
:to="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
|
||||||
|
)
|
||||||
|
"
|
||||||
>Back to Hunt</VaButton
|
>Back to Hunt</VaButton
|
||||||
>
|
>
|
||||||
<VaButton icon="replay" color="success" @click="refresh">Refresh</VaButton>
|
<VaButton icon="replay" color="success" @click="refresh">Refresh</VaButton>
|
||||||
@@ -54,7 +59,7 @@ titleStore.title = data.value?.title;
|
|||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
<p>{{ data.points }} Points</p>
|
<p>{{ data.points }} Points</p>
|
||||||
<p v-if="data.extraPoints">{{ data.extraPoints }} Extra points</p>
|
<p v-if="data.extraPoints">{{ data.extraPoints }} Extra points</p>
|
||||||
<p v-if="data.extraText">Extra: {{ data.extraText }}</p>
|
<p v-if="hasKey(data, 'extraText')">Extra: {{ tKey(data, 'extraText') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="data && data.answers.length > 0">
|
<div v-if="data && data.answers.length > 0">
|
||||||
<VaAccordion multiple stateful>
|
<VaAccordion multiple stateful>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ClickableImage from '~/components/ClickableImage.vue';
|
import ClickableImage from '~/components/ClickableImage.vue';
|
||||||
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
import { useTitleStore } from '~/stores/title';
|
import { useTitleStore } from '~/stores/title';
|
||||||
import { fullDate } from '~~/server/utils/date';
|
import { fullDate } from '~~/server/utils/date';
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ const app = useNuxtApp();
|
|||||||
const { user } = useUserSession();
|
const { user } = useUserSession();
|
||||||
const { huntSlug, questSlug } = app.$slugData;
|
const { huntSlug, questSlug } = app.$slugData;
|
||||||
|
|
||||||
const { t, d } = useI18n();
|
const { t, d, locale } = useI18n();
|
||||||
|
const { tKey, hasKey, tSluggy } = useI18nKey();
|
||||||
|
|
||||||
const titleStore = useTitleStore();
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
@@ -20,13 +22,18 @@ const { data, pending, refresh, error } = await useFetch(
|
|||||||
`/api/quest/${questSlug?.id}`
|
`/api/quest/${questSlug?.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
titleStore.title = data.value?.title;
|
titleStore.title = data.value ? tKey(data.value, 'title').value : undefined;
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('layouts.title', {
|
title: t('layouts.title', {
|
||||||
title:
|
title:
|
||||||
[
|
[
|
||||||
data.value?.title || questSlug?.name,
|
hasKey(data.value, 'title')
|
||||||
data.value?.hunt?.name || huntSlug?.name
|
? tKey(data.value, 'title').value
|
||||||
|
: questSlug?.name,
|
||||||
|
hasKey(data.value?.hunt, 'name')
|
||||||
|
? tKey(data.value?.hunt, 'name').value
|
||||||
|
: huntSlug?.name
|
||||||
]
|
]
|
||||||
.filter((s) => s && s?.length > 0)
|
.filter((s) => s && s?.length > 0)
|
||||||
.join(' | ') || t('layouts.default_tile')
|
.join(' | ') || t('layouts.default_tile')
|
||||||
@@ -52,6 +59,7 @@ async function submit() {
|
|||||||
submitError.value = '';
|
submitError.value = '';
|
||||||
|
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
|
body.set('lang', locale.value);
|
||||||
|
|
||||||
if (data.value?.answer?.updatedAt) {
|
if (data.value?.answer?.updatedAt) {
|
||||||
body.set('updatedAt', data.value?.answer?.updatedAt);
|
body.set('updatedAt', data.value?.answer?.updatedAt);
|
||||||
@@ -100,7 +108,11 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
<VaButtonGroup>
|
<VaButtonGroup>
|
||||||
<VaButton
|
<VaButton
|
||||||
icon="arrow_back"
|
icon="arrow_back"
|
||||||
:to="$localePath(`/hunt/${sluggy(data?.hunt || huntSlug!)}`)"
|
:to="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
|
||||||
|
)
|
||||||
|
"
|
||||||
>Back to Hunt</VaButton
|
>Back to Hunt</VaButton
|
||||||
>
|
>
|
||||||
<VaButton
|
<VaButton
|
||||||
@@ -118,7 +130,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
color="danger"
|
color="danger"
|
||||||
:to="
|
:to="
|
||||||
$localePath(
|
$localePath(
|
||||||
`/hunt/${sluggy(data?.hunt || huntSlug!)}/${sluggy(data || questSlug)}/admin`
|
`/hunt/${data?.hunt ? tSluggy(data.hunt).value : sluggy(huntSlug!)}/${data ? tSluggy(data).value : sluggy(questSlug!)}}/admin`
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>Admin</VaButton
|
>Admin</VaButton
|
||||||
@@ -138,19 +150,21 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
<ClickableImage
|
<ClickableImage
|
||||||
v-if="data?.picture"
|
v-if="data?.picture"
|
||||||
:src="data?.picture?.url"
|
:src="data?.picture?.url"
|
||||||
:title="data?.title || questSlug?.name || ''"
|
:title="(data ? tKey(data, 'title').value : questSlug?.name) || ''"
|
||||||
/>
|
/>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<p class="text-lg">{{ data.description }}</p>
|
<p class="text-lg">{{ tKey(data, 'description') }}</p>
|
||||||
<p v-if="data.question" class="text-xl font-bold">
|
<p v-if="hasKey(data, 'question')" class="text-xl font-bold">
|
||||||
{{ data.textRequired ? '❓' : '❔' }}: {{ data.question }}
|
{{ data.textRequired ? '❓' : '❔' }}:
|
||||||
|
{{ tKey(data, 'question') }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="data.question">
|
<p v-if="hasKey(data, 'question')">
|
||||||
🖋: {{ t(`enums.text_type.${data.textType}`) }}
|
🖋: {{ t(`enums.text_type.${data.textType}`) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="data.extraText">
|
<p v-if="hasKey(data, 'extraText')">
|
||||||
<span class="uppercase">Extra points:</span> {{ data.extraText }}
|
<span class="uppercase">Extra points:</span>
|
||||||
|
{{ tKey(data, 'extraText') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
@@ -166,7 +180,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
{{ d(data.answer.review.updatedAt, fullDate) }}</span
|
{{ d(data.answer.review.updatedAt, fullDate) }}</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex max-h-[60dvh] flex-col gap-2 overflow-y-auto pr-4">
|
<div
|
||||||
|
class="flex max-h-[60dvh] flex-col gap-2 overflow-y-auto pr-4"
|
||||||
|
v-if="data.answer.review.review"
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
v-for="(line, i) in data.answer.review.review.split('\n')"
|
v-for="(line, i) in data.answer.review.review.split('\n')"
|
||||||
:key="i"
|
:key="i"
|
||||||
@@ -186,9 +203,9 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<VaInput
|
<VaInput
|
||||||
:label="data.question"
|
:label="tKey(data, 'question').value"
|
||||||
:disabled="uploading || data.answer?.final"
|
:disabled="uploading || data.answer?.final"
|
||||||
v-if="data.question"
|
v-if="hasKey(data, 'question')"
|
||||||
placeholder="Your answer"
|
placeholder="Your answer"
|
||||||
:required-mark="data.textRequired"
|
:required-mark="data.textRequired"
|
||||||
v-model="answer"
|
v-model="answer"
|
||||||
@@ -198,7 +215,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
|
|||||||
:type="textType[data.textType]"
|
:type="textType[data.textType]"
|
||||||
:messages="[
|
:messages="[
|
||||||
t(`enums.text_type.${data.textType}`),
|
t(`enums.text_type.${data.textType}`),
|
||||||
data.textRequired ? 'Required Answer' : undefined
|
data.textRequired ? 'Required Answer' : ''
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<template v-if="data.textUnit" #appendInner>
|
<template v-if="data.textUnit" #appendInner>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import superjson, { type SuperJSONResult } from 'superjson';
|
import superjson, { type SuperJSONResult } from 'superjson';
|
||||||
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
import { useTitleStore } from '~/stores/title';
|
import { useTitleStore } from '~/stores/title';
|
||||||
import { fullDate } from '~~/server/utils/date';
|
import { fullDate } from '~~/server/utils/date';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ const app = useNuxtApp();
|
|||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
const { t, d } = useI18n();
|
const { t, d } = useI18n();
|
||||||
|
const { tKey, hasKey } = useI18nKey();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { confirm } = useModal();
|
const { confirm } = useModal();
|
||||||
@@ -36,7 +38,7 @@ const formData = useCloned(data, {
|
|||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('layouts.title', {
|
title: t('layouts.title', {
|
||||||
title: data.value?.name || name
|
title: hasKey(data.value, 'name') ? tKey(data.value!, 'name').value : name
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ watch(loggedIn, (isLoggedIn) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
titleStore.title = data.value?.name;
|
titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ async function kick(member: {
|
|||||||
init({
|
init({
|
||||||
title: 'Error while kicking',
|
title: 'Error while kicking',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
|
// @ts-expect-error weird error stuff
|
||||||
message: e?.message || e?.statusMessage || t(parseError(e))
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -143,6 +146,7 @@ async function addUser() {
|
|||||||
init({
|
init({
|
||||||
title: 'Error while adding',
|
title: 'Error while adding',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
|
// @ts-expect-error weird error stuff
|
||||||
message: e?.message || e?.statusMessage || t(parseError(e))
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -157,17 +161,31 @@ async function addUser() {
|
|||||||
<h1>admin</h1>
|
<h1>admin</h1>
|
||||||
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
|
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
|
||||||
<VaInput
|
<VaInput
|
||||||
label="Name"
|
label="Name DE"
|
||||||
v-model="formData.cloned.value.name"
|
v-model="formData.cloned.value.name_de"
|
||||||
clearable
|
clearable
|
||||||
:clear-value="data.name"
|
:clear-value="data.name_de"
|
||||||
|
clearable-icon="replay"
|
||||||
|
/>
|
||||||
|
<VaInput
|
||||||
|
label="Name EN"
|
||||||
|
v-model="formData.cloned.value.name_en"
|
||||||
|
clearable
|
||||||
|
:clear-value="data.name_en"
|
||||||
clearable-icon="replay"
|
clearable-icon="replay"
|
||||||
/>
|
/>
|
||||||
<VaTextarea
|
<VaTextarea
|
||||||
label="Description"
|
label="Description DE"
|
||||||
v-model="formData.cloned.value.description"
|
v-model="formData.cloned.value.description_de"
|
||||||
clearable
|
clearable
|
||||||
:clear-value="data.description"
|
:clear-value="data.description_de"
|
||||||
|
clearable-icon="replay"
|
||||||
|
/>
|
||||||
|
<VaTextarea
|
||||||
|
label="Description EN"
|
||||||
|
v-model="formData.cloned.value.description_en"
|
||||||
|
clearable
|
||||||
|
:clear-value="data.description_en"
|
||||||
clearable-icon="replay"
|
clearable-icon="replay"
|
||||||
/>
|
/>
|
||||||
<VaCheckbox label="Virtual" v-model="formData.cloned.value.virtual" />
|
<VaCheckbox label="Virtual" v-model="formData.cloned.value.virtual" />
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ definePageMeta({
|
|||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { tKey } = useI18nKey();
|
||||||
|
|
||||||
const { name, id } = app.$slugData.huntSlug!;
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
|
|
||||||
const titleStore = useTitleStore();
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
type Picture = {
|
type Picture = {
|
||||||
quest: { id: number; title: string };
|
quest: { id: number; title_de: string; title_en: string };
|
||||||
team: {
|
team: {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
@@ -81,7 +82,7 @@ titleStore.title = data.value?.name;
|
|||||||
<p
|
<p
|
||||||
class="absolute top-2 mx-auto rounded bg-gray-200/75 px-2 py-1 text-center text-xl font-bold"
|
class="absolute top-2 mx-auto rounded bg-gray-200/75 px-2 py-1 text-center text-xl font-bold"
|
||||||
>
|
>
|
||||||
{{ item.quest.title }} — {{ item.team.name }} —
|
{{ tKey(item.quest, 'title') }} — {{ item.team.name }} —
|
||||||
{{ item.picture.creator.name }}
|
{{ item.picture.creator.name }}
|
||||||
</p></template
|
</p></template
|
||||||
></VaCarousel
|
></VaCarousel
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { fullDate } from '~~/server/utils/date';
|
|||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const localePath = useLocalePath();
|
|
||||||
const { t, d } = useI18n();
|
const { t, d } = useI18n();
|
||||||
|
const { tKey, hasKey, tSluggy } = useI18nKey();
|
||||||
|
|
||||||
const { name, id } = app.$slugData.huntSlug!;
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
|
|
||||||
@@ -18,13 +18,13 @@ const hideAnswers = ref(true);
|
|||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('layouts.title', {
|
title: t('layouts.title', {
|
||||||
title: data.value?.name || name
|
title: hasKey(data.value, 'name') ? tKey(data.value!, 'name').value : name
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(loggedIn, () => refresh());
|
watch(loggedIn, () => refresh());
|
||||||
|
|
||||||
titleStore.title = data.value?.name;
|
titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
|
||||||
|
|
||||||
async function invite() {
|
async function invite() {
|
||||||
if (!data.value?.ownTeam) {
|
if (!data.value?.ownTeam) {
|
||||||
@@ -34,7 +34,7 @@ async function invite() {
|
|||||||
const clipboardItemData = {
|
const clipboardItemData = {
|
||||||
// TODO: add invite link
|
// TODO: add invite link
|
||||||
// 'text/plain': `https://pichunt.syma.dev/hunt/${id}/join?id=${data.value.ownTeam.id}&pw=${data.value.ownTeam.password}`
|
// 'text/plain': `https://pichunt.syma.dev/hunt/${id}/join?id=${data.value.ownTeam.id}&pw=${data.value.ownTeam.password}`
|
||||||
'text/plain': `Join team ${data.value.ownTeam.name} with code "${data.value.ownTeam.password}" on hunt "${data.value.name || name}"!`
|
'text/plain': `Join team ${data.value.ownTeam.name} with code "${data.value.ownTeam.password}" on hunt "${data.value ? tKey(data.value, 'name').value : name}"!`
|
||||||
};
|
};
|
||||||
const clipboardItem = new ClipboardItem(clipboardItemData);
|
const clipboardItem = new ClipboardItem(clipboardItemData);
|
||||||
await navigator.clipboard.write([clipboardItem]);
|
await navigator.clipboard.write([clipboardItem]);
|
||||||
@@ -43,7 +43,7 @@ async function invite() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="text-2xl">
|
<h1 class="text-2xl">
|
||||||
{{ data?.name || name }}
|
{{ data ? tKey(data, 'name') : name }}
|
||||||
|
|
||||||
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
<VaChip v-if="data?.totalScore" class="float-right" color="success"
|
||||||
>Total points: {{ data!!.totalScore }}</VaChip
|
>Total points: {{ data!!.totalScore }}</VaChip
|
||||||
@@ -58,40 +58,43 @@ async function invite() {
|
|||||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||||
color="success"
|
color="success"
|
||||||
icon="leaderboard"
|
icon="leaderboard"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/showcase`)"
|
:to="$localePath(`/hunt/${tSluggy(data).value}/showcase`)"
|
||||||
>Showcase</VaButton
|
>Showcase</VaButton
|
||||||
>
|
>
|
||||||
<VaButton
|
<VaButton
|
||||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||||
color="success"
|
color="success"
|
||||||
icon="collections"
|
icon="collections"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/diashow`)"
|
:to="$localePath(`/hunt/${tSluggy(data).value}/diashow`)"
|
||||||
>Diashow</VaButton
|
>Diashow</VaButton
|
||||||
>
|
>
|
||||||
<VaButton
|
<VaButton
|
||||||
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
|
||||||
color="success"
|
color="success"
|
||||||
icon="auto_awesome_mosaic"
|
icon="auto_awesome_mosaic"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/pictures`)"
|
:to="$localePath(`/hunt/${tSluggy(data).value}/pictures`)"
|
||||||
>All pictures</VaButton
|
>All pictures</VaButton
|
||||||
>
|
>
|
||||||
<VaButton
|
<VaButton
|
||||||
v-if="data?.isMember"
|
v-if="data?.isMember"
|
||||||
color="danger"
|
color="danger"
|
||||||
icon="edit"
|
icon="edit"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/admin`)"
|
:to="$localePath(`/hunt/${tSluggy(data).value}/admin`)"
|
||||||
>Admin</VaButton
|
>Admin</VaButton
|
||||||
><VaButton
|
><VaButton
|
||||||
v-if="data?.isMember"
|
v-if="data?.isMember"
|
||||||
color="danger"
|
color="danger"
|
||||||
icon="groups"
|
icon="groups"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/teams`)"
|
:to="$localePath(`/hunt/${tSluggy(data).value}/teams`)"
|
||||||
>Teams</VaButton
|
>Teams</VaButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="data">
|
<div v-if="data">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p v-for="(line, i) in data.description.split('\n')" :key="i">
|
<p
|
||||||
|
v-for="(line, i) in tKey(data, 'description').value.split('\n')"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
{{ line }}
|
{{ line }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +102,7 @@ async function invite() {
|
|||||||
v-if="data?.picture"
|
v-if="data?.picture"
|
||||||
:src="data?.picture?.url"
|
:src="data?.picture?.url"
|
||||||
class="h-48"
|
class="h-48"
|
||||||
:title="data?.name || name"
|
:title="data ? tKey(data, 'name').value : name"
|
||||||
/>
|
/>
|
||||||
<p v-if="data.start">
|
<p v-if="data.start">
|
||||||
Start:
|
Start:
|
||||||
@@ -142,7 +145,7 @@ async function invite() {
|
|||||||
<VaButton
|
<VaButton
|
||||||
color="info"
|
color="info"
|
||||||
icon="login"
|
icon="login"
|
||||||
:to="$localePath(`/login?to=/hunt/${sluggy(data)}`)"
|
:to="$localePath(`/login?to=/hunt/${tSluggy(data).value}`)"
|
||||||
>Login first</VaButton
|
>Login first</VaButton
|
||||||
>
|
>
|
||||||
</VaCardActions>
|
</VaCardActions>
|
||||||
@@ -180,7 +183,7 @@ async function invite() {
|
|||||||
<template #loader> <VaProgressCircle indeterminate /> </template
|
<template #loader> <VaProgressCircle indeterminate /> </template
|
||||||
></VaImage>
|
></VaImage>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<p class="line-clamp-2 h-12">{{ quest.description }}</p>
|
<p class="line-clamp-2 h-12">{{ tKey(quest, 'description') }}</p>
|
||||||
|
|
||||||
<div v-if="!hideAnswers && quest.answer?.text">
|
<div v-if="!hideAnswers && quest.answer?.text">
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
@@ -189,7 +192,11 @@ async function invite() {
|
|||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
<VaCardActions>
|
<VaCardActions>
|
||||||
<VaButton
|
<VaButton
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/${sluggy(quest)}`)"
|
:to="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}`
|
||||||
|
)
|
||||||
|
"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="rate_review"
|
icon="rate_review"
|
||||||
class="flex-grow"
|
class="flex-grow"
|
||||||
@@ -199,7 +206,11 @@ async function invite() {
|
|||||||
v-if="data.isMember"
|
v-if="data.isMember"
|
||||||
color="danger"
|
color="danger"
|
||||||
:icon="(quest.unreviewed || 0) > 0 ? 'priority_high' : undefined"
|
:icon="(quest.unreviewed || 0) > 0 ? 'priority_high' : undefined"
|
||||||
:to="$localePath(`/hunt/${sluggy(data)}/${sluggy(quest)}/admin`)"
|
:to="
|
||||||
|
$localePath(
|
||||||
|
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}/admin`
|
||||||
|
)
|
||||||
|
"
|
||||||
>Admin ({{ quest.unreviewed || 0 }} unreviewed)</VaButton
|
>Admin ({{ quest.unreviewed || 0 }} unreviewed)</VaButton
|
||||||
>
|
>
|
||||||
</VaCardActions>
|
</VaCardActions>
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ definePageMeta({
|
|||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { tKey, hasKey } = useI18nKey();
|
||||||
|
|
||||||
const { name, id } = app.$slugData.huntSlug!;
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
|
|
||||||
const titleStore = useTitleStore();
|
const titleStore = useTitleStore();
|
||||||
|
|
||||||
type Picture = {
|
type Picture = {
|
||||||
quest: { id: number; title: string };
|
quest: { id: number; title_de: string; title_en: string };
|
||||||
team: {
|
team: {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
@@ -47,7 +48,7 @@ const { data, refresh, error } = await useFetch(`/api/hunt/${id}/diashow`, {
|
|||||||
|
|
||||||
const cols = pictures.reduce(
|
const cols = pictures.reduce(
|
||||||
(c, p, i) => {
|
(c, p, i) => {
|
||||||
c[i % 4].push(p);
|
c[i % 4]!.push(p);
|
||||||
return c;
|
return c;
|
||||||
},
|
},
|
||||||
new Array(4).fill(0).map(() => [] as Picture[])
|
new Array(4).fill(0).map(() => [] as Picture[])
|
||||||
@@ -59,14 +60,14 @@ const { data, refresh, error } = await useFetch(`/api/hunt/${id}/diashow`, {
|
|||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('layouts.two_title', {
|
title: t('layouts.two_title', {
|
||||||
title: data.value?.name || name,
|
title: hasKey(data.value, 'name') ? tKey(data.value!, 'name').value : name,
|
||||||
subtitle: 'Diashow'
|
subtitle: 'Diashow'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(loggedIn, () => refresh());
|
watch(loggedIn, () => refresh());
|
||||||
|
|
||||||
titleStore.title = data.value?.name;
|
titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -81,7 +82,7 @@ titleStore.title = data.value?.name;
|
|||||||
:key="pic.picture.url"
|
:key="pic.picture.url"
|
||||||
:src="pic.picture.url"
|
:src="pic.picture.url"
|
||||||
class="h-auto max-w-full rounded-lg"
|
class="h-auto max-w-full rounded-lg"
|
||||||
:title="`${pic.quest.title} — ${pic.team.name} — ${pic.picture.creator.name}`"
|
:title="`${tKey(pic.quest, 'title')} — ${pic.team.name} — ${pic.picture.creator.name}`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<VaAlert
|
<VaAlert
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ definePageMeta({
|
|||||||
layout: 'full'
|
layout: 'full'
|
||||||
});
|
});
|
||||||
const { t, d } = useI18n();
|
const { t, d } = useI18n();
|
||||||
|
const { tKey, hasKey, tSluggy } = useI18nKey();
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
const { name, id } = app.$slugData.huntSlug!;
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
@@ -52,7 +53,9 @@ const columns = leaderboardColumns.map((c) =>
|
|||||||
class="flex flex-col items-center justify-center gap-8 bg-green-300 text-center"
|
class="flex flex-col items-center justify-center gap-8 bg-green-300 text-center"
|
||||||
>
|
>
|
||||||
<h1 class="text-7xl">{{ t('page.showcase.showcase_for') }}</h1>
|
<h1 class="text-7xl">{{ t('page.showcase.showcase_for') }}</h1>
|
||||||
<h1 class="text-9xl font-bold">{{ data.name || name }}</h1>
|
<h1 class="text-9xl font-bold">
|
||||||
|
{{ hasKey(data, 'name') ? tKey(data, 'name') : name }}
|
||||||
|
</h1>
|
||||||
<p v-if="data.start || data.end" class="flex flex-row gap-4 text-4xl">
|
<p v-if="data.start || data.end" class="flex flex-row gap-4 text-4xl">
|
||||||
<span v-if="data.start">{{ d(data.start, fullDate) }}</span
|
<span v-if="data.start">{{ d(data.start, fullDate) }}</span
|
||||||
><span v-if="data.start && data.end"> – </span
|
><span v-if="data.start && data.end"> – </span
|
||||||
@@ -94,11 +97,17 @@ const columns = leaderboardColumns.map((c) =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section v-for="quest in data.quests" :key="quest.id" :id="sluggy(quest)">
|
<section
|
||||||
|
v-for="quest in data.quests"
|
||||||
|
:key="quest.id"
|
||||||
|
:id="tSluggy(quest).value"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4 bg-yellow-100 p-4">
|
<div class="flex flex-col gap-4 bg-yellow-100 p-4">
|
||||||
<div class="my-8 flex w-full flex-row justify-around">
|
<div class="my-8 flex w-full flex-row justify-around">
|
||||||
<h3 class="text-3xl">{{ quest.title }}</h3>
|
<h3 class="text-3xl">{{ tKey(quest, 'title') }}</h3>
|
||||||
<p class="text-xl font-bold">{{ quest.question }}</p>
|
<p class="text-xl font-bold" v-if="hasKey(quest, 'question')">
|
||||||
|
{{ tKey(quest, 'question') }}
|
||||||
|
</p>
|
||||||
<p v-if="quest.textSolution" class="text-3xl">
|
<p v-if="quest.textSolution" class="text-3xl">
|
||||||
{{ quest.textSolution }}
|
{{ quest.textSolution }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const app = useNuxtApp();
|
|||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { tKey, hasKey } = useI18nKey();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { name, id } = app.$slugData.huntSlug!;
|
const { name, id } = app.$slugData.huntSlug!;
|
||||||
@@ -21,7 +22,7 @@ const { data, pending, refresh } = await useFetch(
|
|||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('layouts.title', {
|
title: t('layouts.title', {
|
||||||
title: data.value?.name || name
|
title: hasKey(data.value, 'name') ? tKey(data.value!, 'name').value : name
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ watch(loggedIn, (isLoggedIn) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
titleStore.title = data.value?.name;
|
titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
|
||||||
|
|
||||||
type Data = NonNullable<(typeof data)['value']>;
|
type Data = NonNullable<(typeof data)['value']>;
|
||||||
export type Team = Data['teams'][number];
|
export type Team = Data['teams'][number];
|
||||||
|
|||||||
+12
-10
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
import { fullDate } from '~~/server/utils/date';
|
import { fullDate } from '~~/server/utils/date';
|
||||||
|
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
const { data, pending, refresh } = await useFetch('/api/home');
|
const { data, pending, refresh } = await useFetch('/api/home');
|
||||||
const { t, d } = useI18n();
|
const { t, d } = useI18n();
|
||||||
|
const { tKey, tSluggy } = useI18nKey();
|
||||||
watch(loggedIn, () => refresh());
|
watch(loggedIn, () => refresh());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -23,18 +25,18 @@ watch(loggedIn, () => refresh());
|
|||||||
v-for="hunt in data.own"
|
v-for="hunt in data.own"
|
||||||
:key="hunt.id"
|
:key="hunt.id"
|
||||||
stripe
|
stripe
|
||||||
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
|
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
|
||||||
>
|
>
|
||||||
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
|
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
|
||||||
|
|
||||||
<ClickableImage
|
<ClickableImage
|
||||||
v-if="hunt.picture"
|
v-if="hunt.picture"
|
||||||
:src="hunt.picture?.url"
|
:src="hunt.picture?.url"
|
||||||
:title="hunt.name"
|
:title="tKey(hunt, 'name').value"
|
||||||
/>
|
/>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<h3 class="text-xl">{{ hunt.name }}</h3>
|
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
|
||||||
{{ hunt.description }}
|
{{ tKey(hunt, 'description') }}
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
||||||
@@ -78,17 +80,17 @@ watch(loggedIn, () => refresh());
|
|||||||
<VaCard
|
<VaCard
|
||||||
v-for="hunt in data.others"
|
v-for="hunt in data.others"
|
||||||
:key="hunt.id"
|
:key="hunt.id"
|
||||||
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
|
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
|
||||||
>
|
>
|
||||||
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
|
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
|
||||||
<ClickableImage
|
<ClickableImage
|
||||||
v-if="hunt.picture"
|
v-if="hunt.picture"
|
||||||
:src="hunt.picture?.url"
|
:src="hunt.picture?.url"
|
||||||
:title="hunt.name"
|
:title="tKey(hunt, 'name').value"
|
||||||
/>
|
/>
|
||||||
<VaCardContent>
|
<VaCardContent>
|
||||||
<h3 class="text-xl">{{ hunt.name }}</h3>
|
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
|
||||||
{{ hunt.description }}
|
{{ tKey(hunt, 'description') }}
|
||||||
<VaDivider />
|
<VaDivider />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['auth']
|
middleware: ['auth']
|
||||||
});
|
});
|
||||||
@@ -8,6 +10,7 @@ const { t } = useI18n();
|
|||||||
const { loggedIn, fetch } = useUserSession();
|
const { loggedIn, fetch } = useUserSession();
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
const validation = useValidation();
|
const validation = useValidation();
|
||||||
|
const { tKey, tSluggy } = useI18nKey();
|
||||||
watch(loggedIn, (isLoggedIn) => {
|
watch(loggedIn, (isLoggedIn) => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
router.push(localePath('/login'));
|
router.push(localePath('/login'));
|
||||||
@@ -234,9 +237,8 @@ async function submitPassword() {
|
|||||||
</VaCard>
|
</VaCard>
|
||||||
<VaCard stripe stripe-color="primary">
|
<VaCard stripe stripe-color="primary">
|
||||||
<VaCardTitle>Uploaded pictures</VaCardTitle>
|
<VaCardTitle>Uploaded pictures</VaCardTitle>
|
||||||
<VaCardContent>
|
<VaCardContent v-if="data && data.uploadedFiles.length > 0">
|
||||||
<VaCarousel
|
<VaCarousel
|
||||||
v-if="data"
|
|
||||||
stateful
|
stateful
|
||||||
:items="data.uploadedFiles"
|
:items="data.uploadedFiles"
|
||||||
arrows
|
arrows
|
||||||
@@ -248,22 +250,23 @@ async function submitPassword() {
|
|||||||
<template #default="{ item }"
|
<template #default="{ item }"
|
||||||
><ClickableImage
|
><ClickableImage
|
||||||
:src="item.url"
|
:src="item.url"
|
||||||
:title="`${item.answer.quest.title} — ${item.answer.quest.hunt.name}`"
|
:title="`${tKey(item.answer.quest, 'title').value} — ${tKey(item.answer.quest.hunt, 'name').value}`"
|
||||||
/>
|
/>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="
|
:to="
|
||||||
$localePath(
|
$localePath(
|
||||||
`/hunt/${sluggy(item.answer.quest.hunt)}/${sluggy(item.answer.quest)}`
|
`/hunt/${tSluggy(item.answer.quest.hunt).value}/${tSluggy(item.answer.quest).value}`
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="absolute top-2 mx-auto rounded bg-gray-200/75 px-2 py-1 text-center text-xl hover:underline"
|
class="absolute top-2 mx-auto rounded bg-gray-200/75 px-2 py-1 text-center text-xl hover:underline"
|
||||||
>
|
>
|
||||||
{{ item.answer.quest.title }} —
|
{{ tKey(item.answer.quest, 'title') }} —
|
||||||
{{ item.answer.quest.hunt.name }}
|
{{ tKey(item.answer.quest.hunt, 'name') }}
|
||||||
</NuxtLink></template
|
</NuxtLink></template
|
||||||
>
|
>
|
||||||
</VaCarousel>
|
</VaCarousel>
|
||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
|
<VaCardContent v-else> No pictures uploaded yet. </VaCardContent>
|
||||||
</VaCard>
|
</VaCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"check": "prettier --check .",
|
"check": "prettier --check .",
|
||||||
|
"test": "bun test",
|
||||||
"dev:up": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d",
|
"dev:up": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d",
|
||||||
"dev:down": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml down"
|
"dev:down": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml down"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."Hunt"
|
||||||
|
ADD COLUMN "description_de" TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN "description_en" TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN "name_de" TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN "name_en" TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
update "public"."Hunt"
|
||||||
|
set description_en = description,
|
||||||
|
description_de = description,
|
||||||
|
name_en = name,
|
||||||
|
name_de = name
|
||||||
|
where description_de = '';
|
||||||
|
|
||||||
|
ALTER TABLE "public"."Hunt"
|
||||||
|
DROP COLUMN "description",
|
||||||
|
DROP COLUMN "name",
|
||||||
|
ALTER COLUMN name_de drop default,
|
||||||
|
ALTER COLUMN name_en drop default,
|
||||||
|
ALTER COLUMN description_de drop default,
|
||||||
|
ALTER COLUMN description_en drop default;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."HuntQuest"
|
||||||
|
ADD COLUMN "description_de" TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN "description_en" TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN "extraText_de" TEXT,
|
||||||
|
ADD COLUMN "extraText_en" TEXT,
|
||||||
|
ADD COLUMN "question_de" TEXT,
|
||||||
|
ADD COLUMN "question_en" TEXT,
|
||||||
|
ADD COLUMN "title_de" TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN "title_en" TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
update "public"."HuntQuest"
|
||||||
|
set description_en = description,
|
||||||
|
description_de = description,
|
||||||
|
title_en = title,
|
||||||
|
title_de = title,
|
||||||
|
"extraText_en" = "extraText",
|
||||||
|
"extraText_de" = "extraText",
|
||||||
|
question_en = question,
|
||||||
|
question_de = question
|
||||||
|
where description_de = '';
|
||||||
|
|
||||||
|
ALTER TABLE "public"."HuntQuest"
|
||||||
|
DROP COLUMN "description",
|
||||||
|
DROP COLUMN "title",
|
||||||
|
DROP COLUMN question,
|
||||||
|
DROP COLUMN "extraText",
|
||||||
|
ALTER COLUMN title_de drop default,
|
||||||
|
ALTER COLUMN title_en drop default,
|
||||||
|
ALTER COLUMN description_de drop default,
|
||||||
|
ALTER COLUMN description_en drop default;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."QuestAnswer"
|
||||||
|
ADD COLUMN "lang" CHAR(2) NOT NULL DEFAULT 'de';
|
||||||
+20
-12
@@ -64,9 +64,11 @@ model File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Hunt {
|
model Hunt {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name_en String
|
||||||
description String
|
name_de String
|
||||||
|
description_en String
|
||||||
|
description_de String
|
||||||
|
|
||||||
virtual Boolean @default(false)
|
virtual Boolean @default(false)
|
||||||
start DateTime? @db.Timestamptz(3)
|
start DateTime? @db.Timestamptz(3)
|
||||||
@@ -154,12 +156,15 @@ enum TextType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model HuntQuest {
|
model HuntQuest {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title_en String
|
||||||
description String @default("")
|
title_de String
|
||||||
question String?
|
description_en String @default("")
|
||||||
location String?
|
description_de String @default("")
|
||||||
locationLink String?
|
question_en String?
|
||||||
|
question_de String?
|
||||||
|
location String?
|
||||||
|
locationLink String?
|
||||||
|
|
||||||
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
picture File? @relation(fields: [pictureId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
pictureId Int?
|
pictureId Int?
|
||||||
@@ -172,9 +177,10 @@ model HuntQuest {
|
|||||||
textUnit String?
|
textUnit String?
|
||||||
textSolution String?
|
textSolution String?
|
||||||
|
|
||||||
points Int @default(10) @db.SmallInt
|
points Int @default(10) @db.SmallInt
|
||||||
extraPoints Int? @db.SmallInt
|
extraPoints Int? @db.SmallInt
|
||||||
extraText String?
|
extraText_en String?
|
||||||
|
extraText_de String?
|
||||||
|
|
||||||
hunt Hunt @relation(fields: [huntId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
hunt Hunt @relation(fields: [huntId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
huntId Int
|
huntId Int
|
||||||
@@ -194,6 +200,8 @@ model QuestAnswer {
|
|||||||
lon Decimal? @db.Decimal(10, 8)
|
lon Decimal? @db.Decimal(10, 8)
|
||||||
final Boolean @default(false)
|
final Boolean @default(false)
|
||||||
|
|
||||||
|
lang String @default("de") @db.Char(2)
|
||||||
|
|
||||||
team HuntTeam @relation(fields: [teamId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
team HuntTeam @relation(fields: [teamId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
teamId Int
|
teamId Int
|
||||||
|
|
||||||
|
|||||||
+40
-18
@@ -110,8 +110,10 @@ async function main() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name: 'Bremer Stadtrallye!',
|
name_de: 'Bremer Stadtrallye!',
|
||||||
description: 'DIE Rallye durch Bremen!',
|
name_en: 'Bremer Stadtrallye!',
|
||||||
|
description_de: 'DIE Rallye durch Bremen!',
|
||||||
|
description_en: 'THE Rallye trough Bremen!',
|
||||||
start: new Date('2025-08-23T10:00:00.000Z'),
|
start: new Date('2025-08-23T10:00:00.000Z'),
|
||||||
end: new Date('2025-08-30T10:00:00.000Z'),
|
end: new Date('2025-08-30T10:00:00.000Z'),
|
||||||
allowJoin: true,
|
allowJoin: true,
|
||||||
@@ -120,9 +122,12 @@ async function main() {
|
|||||||
quests: {
|
quests: {
|
||||||
create: [
|
create: [
|
||||||
{
|
{
|
||||||
title: 'MOIN!',
|
title_de: 'MOIN!',
|
||||||
description: 'Finde das größte MOIN',
|
title_en: 'MOIN!',
|
||||||
question: 'Wo steht es?',
|
description_de: 'Finde das größte MOIN',
|
||||||
|
description_en: 'Find the largest MOIN',
|
||||||
|
question_de: 'Wo steht es?',
|
||||||
|
question_en: 'Where is it?',
|
||||||
order: 0,
|
order: 0,
|
||||||
optional: false,
|
optional: false,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
@@ -132,12 +137,18 @@ async function main() {
|
|||||||
points: 10
|
points: 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Die Musikanten',
|
title_de: 'Die Musikanten',
|
||||||
description:
|
title_en: 'The Musikanten',
|
||||||
|
description_de:
|
||||||
'Dort wo die vier Bremer Tiere musizieren, müsst ihr ihre Beine anfassen!',
|
'Dort wo die vier Bremer Tiere musizieren, müsst ihr ihre Beine anfassen!',
|
||||||
question: 'Wie viele Beine haben die Musikanten zusammen?',
|
description_en:
|
||||||
extraText:
|
'Where the four Bremen music animals are, you have to touch their legs!',
|
||||||
'Mehr Punkte, je mehr unterschiedliche Beine ihre berührt!',
|
question_de: 'Wie viele Beine haben die Musikanten zusammen?',
|
||||||
|
question_en: 'How many legs do the musicians have altogether?',
|
||||||
|
extraText_de:
|
||||||
|
'Mehr Punkte, je mehr unterschiedliche Beine ihr berührt!',
|
||||||
|
extraText_en:
|
||||||
|
'The more different legs you touch, the more points you get!',
|
||||||
points: 10,
|
points: 10,
|
||||||
extraPoints: 5,
|
extraPoints: 5,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
@@ -148,32 +159,43 @@ async function main() {
|
|||||||
order: 1
|
order: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tolle Aussicht',
|
title_de: 'Tolle Aussicht',
|
||||||
description:
|
title_en: 'Great view',
|
||||||
|
description_de:
|
||||||
'Mache ein Bild aus einem Gebäude der Hochschule, von so weit oben wie möglich!',
|
'Mache ein Bild aus einem Gebäude der Hochschule, von so weit oben wie möglich!',
|
||||||
|
description_en:
|
||||||
|
'Take a picture of a university building from as high up as possible!',
|
||||||
points: 10,
|
points: 10,
|
||||||
extraPoints: 10,
|
extraPoints: 10,
|
||||||
extraText:
|
extraText_de:
|
||||||
'Wenn ihr das einzige Team in diesem Gebäude/Standort oder die Höchsten wart!',
|
'Wenn ihr das einzige Team in diesem Gebäude/Standort oder die Höchsten wart!',
|
||||||
|
extraText_en:
|
||||||
|
'If you were the only team in that building/location or on the highest floor!',
|
||||||
optional: false,
|
optional: false,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
textRequired: false,
|
textRequired: false,
|
||||||
question: 'In welchem Gebäude und Stockwerk wart ihr?',
|
question_de: 'In welchem Gebäude und Stockwerk wart ihr?',
|
||||||
|
question_en: 'Which building and floor were you in?',
|
||||||
textType: 'TEXT',
|
textType: 'TEXT',
|
||||||
order: 2
|
order: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Einfach mal abhauen',
|
title_de: 'Einfach mal abhauen',
|
||||||
description:
|
title_en: 'Just get away',
|
||||||
|
description_de:
|
||||||
'Fahrt so weit ihr wollt und macht ein Foto vom Haltestellenschild!',
|
'Fahrt so weit ihr wollt und macht ein Foto vom Haltestellenschild!',
|
||||||
question: 'An welcher Haltestelle wart ihr?',
|
description_en:
|
||||||
|
'Drive as far as you want and take a photo of the bus stop sign!',
|
||||||
|
question_de: 'An welcher Haltestelle wart ihr?',
|
||||||
|
question_en: 'Which stop were you at?',
|
||||||
order: 3,
|
order: 3,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
textRequired: true,
|
textRequired: true,
|
||||||
textType: 'TEXT',
|
textType: 'TEXT',
|
||||||
points: 3,
|
points: 3,
|
||||||
extraPoints: 30,
|
extraPoints: 30,
|
||||||
extraText: 'Je weiter weg'
|
extraText_de: 'Je weiter weg',
|
||||||
|
extraText_en: 'The further away'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
description: true,
|
name_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
virtual: true,
|
virtual: true,
|
||||||
start: true,
|
start: true,
|
||||||
end: true,
|
end: true,
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
event,
|
event,
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(256).trim(),
|
name_de: z.string().min(1).max(256).trim(),
|
||||||
description: z.string().min(1).max(1000).trim(),
|
name_en: z.string().min(1).max(256).trim(),
|
||||||
|
description_de: z.string().min(1).max(1000).trim(),
|
||||||
|
description_en: z.string().min(1).max(1000).trim(),
|
||||||
virtual: z.boolean(),
|
virtual: z.boolean(),
|
||||||
start: z.iso.datetime(),
|
start: z.iso.datetime(),
|
||||||
end: z.iso.datetime(),
|
end: z.iso.datetime(),
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
|
name_en: true,
|
||||||
teams: {
|
teams: {
|
||||||
omit: {
|
omit: {
|
||||||
huntId: true,
|
huntId: true,
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
include: {
|
include: {
|
||||||
hunt: {
|
hunt: {
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name_de: true,
|
||||||
|
name_en: true,
|
||||||
id: true
|
id: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -53,6 +54,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
text: true,
|
text: true,
|
||||||
final: true,
|
final: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
|
lang: true,
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.teamMember.delete({
|
await prisma.teamMember.delete({
|
||||||
where: {
|
where: {
|
||||||
memberId_teamId: {
|
memberId_teamId: {
|
||||||
memberId: userId,
|
memberId: userId,
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
quest: {
|
quest: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title_de: true,
|
||||||
|
title_en: true,
|
||||||
hunt: {
|
hunt: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true
|
name_de: true,
|
||||||
|
name_en: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ async function getOwnHunts(userId: number | undefined) {
|
|||||||
return prisma.hunt.findMany({
|
return prisma.hunt.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
description: true,
|
name_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
picture: {
|
picture: {
|
||||||
select: {
|
select: {
|
||||||
url: true
|
url: true
|
||||||
@@ -60,8 +62,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
const others = await prisma.hunt.findMany({
|
const others = await prisma.hunt.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
description: true,
|
name_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
picture: {
|
picture: {
|
||||||
select: {
|
select: {
|
||||||
url: true
|
url: true
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
|
name_en: true,
|
||||||
quests: {
|
quests: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title_de: true,
|
||||||
|
title_en: true,
|
||||||
answers: {
|
answers: {
|
||||||
select: {
|
select: {
|
||||||
team: {
|
team: {
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
|
name_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
revealAnswers: true,
|
revealAnswers: true,
|
||||||
revealQuests: true,
|
revealQuests: true,
|
||||||
picture: {
|
picture: {
|
||||||
@@ -27,7 +30,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
url: true
|
url: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: true,
|
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
start: true,
|
start: true,
|
||||||
end: true,
|
end: true,
|
||||||
@@ -80,13 +82,15 @@ export default defineEventHandler(async (event) => {
|
|||||||
quests: {
|
quests: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title_de: true,
|
||||||
|
title_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
picture: {
|
picture: {
|
||||||
select: {
|
select: {
|
||||||
url: true
|
url: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
description: true,
|
|
||||||
textRequired: true,
|
textRequired: true,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
points: true,
|
points: true,
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
|
name_en: true,
|
||||||
start: true,
|
start: true,
|
||||||
end: true,
|
end: true,
|
||||||
|
|
||||||
@@ -97,8 +98,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
quests: {
|
quests: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title_de: true,
|
||||||
question: true,
|
title_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
textSolution: true,
|
textSolution: true,
|
||||||
points: true,
|
points: true,
|
||||||
extraPoints: true,
|
extraPoints: true,
|
||||||
|
|||||||
@@ -18,14 +18,17 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title_de: true,
|
||||||
description: true,
|
title_en: true,
|
||||||
|
description_de: true,
|
||||||
|
description_en: true,
|
||||||
picture: {
|
picture: {
|
||||||
select: {
|
select: {
|
||||||
url: true
|
url: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
question: true,
|
question_de: true,
|
||||||
|
question_en: true,
|
||||||
optional: true,
|
optional: true,
|
||||||
pictureRequired: true,
|
pictureRequired: true,
|
||||||
textRequired: true,
|
textRequired: true,
|
||||||
@@ -33,11 +36,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
textUnit: true,
|
textUnit: true,
|
||||||
points: true,
|
points: true,
|
||||||
extraPoints: true,
|
extraPoints: true,
|
||||||
extraText: true,
|
extraText_de: true,
|
||||||
|
extraText_en: true,
|
||||||
hunt: {
|
hunt: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name_de: true,
|
||||||
|
name_en: true,
|
||||||
revealQuests: true
|
revealQuests: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
.object({
|
.object({
|
||||||
updatedAt: z.iso.datetime().optional(),
|
updatedAt: z.iso.datetime().optional(),
|
||||||
answer: z.string().optional(),
|
answer: z.string().optional(),
|
||||||
|
lang: z.enum(['de', 'en']).default('de'),
|
||||||
image: z
|
image: z
|
||||||
.file()
|
.file()
|
||||||
.mime(['image/png', 'image/jpeg', 'image/jpg'])
|
.mime(['image/png', 'image/jpeg', 'image/jpg'])
|
||||||
@@ -160,7 +161,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
memberId: userId,
|
memberId: userId,
|
||||||
text: data.answer,
|
text: data.answer,
|
||||||
pictureId: file.id,
|
pictureId: file.id,
|
||||||
final: data.final
|
final: data.final,
|
||||||
|
lang: data.lang
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -170,7 +172,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
memberId: userId,
|
memberId: userId,
|
||||||
questId: quest.id,
|
questId: quest.id,
|
||||||
text: data.answer,
|
text: data.answer,
|
||||||
final: data.final
|
final: data.final,
|
||||||
|
lang: data.lang
|
||||||
},
|
},
|
||||||
select: { id: true }
|
select: { id: true }
|
||||||
});
|
});
|
||||||
@@ -194,7 +197,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
data: {
|
data: {
|
||||||
memberId: userId,
|
memberId: userId,
|
||||||
text: data.answer,
|
text: data.answer,
|
||||||
final: data.final
|
final: data.final,
|
||||||
|
lang: data.lang
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -204,7 +208,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
memberId: userId,
|
memberId: userId,
|
||||||
questId: quest.id,
|
questId: quest.id,
|
||||||
text: data.answer,
|
text: data.answer,
|
||||||
final: data.final
|
final: data.final,
|
||||||
|
lang: data.lang
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Source: https://dev.to/jorik/country-code-to-flag-emoji-a21
|
||||||
|
export function getFlagEmoji(countryCode: string) {
|
||||||
|
const codePoints = countryCode
|
||||||
|
.toUpperCase()
|
||||||
|
.split('')
|
||||||
|
.map((char) => 127397 + char.charCodeAt(0));
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user