Switch to translatable data

This commit is contained in:
2025-08-20 23:18:40 +02:00
parent c35fb06830
commit cc544322b2
36 changed files with 431 additions and 146 deletions
+6 -2
View File
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { useI18nKey } from '~/composables/useI18nKey';
const props = defineProps<{
quest: {
title: string;
title_de: string;
title_en: string;
textRequired: boolean;
pictureRequired: boolean;
optional: boolean;
@@ -12,12 +15,13 @@ const props = defineProps<{
} | null;
};
}>();
const { tKey } = useI18nKey();
</script>
<template>
<span>{{ quest.pictureRequired ? '📸' : '📷' }}</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"
>{{ quest.answer?.review?.score || 0 }} / {{ quest.points
}}{{ quest.extraPoints ? `+${quest.extraPoints}` : '' }}</VaChip
+21 -4
View File
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { getFlagEmoji } from '#shared/utils/flag';
import { fullDate } from '~~/server/utils/date';
const { d, t } = useI18n();
const { d, t, locale } = useI18n();
const { tKey } = useI18nKey();
const props = defineProps<{
answer: {
id: number;
@@ -9,6 +12,7 @@ const props = defineProps<{
updatedAt: string;
team: { id: number; name: string };
final: boolean;
lang: string;
review: {
internalNote: string | null;
rankingValue: number | null;
@@ -25,12 +29,14 @@ const props = defineProps<{
} | null;
};
quest: {
title: string;
title_de: string;
title_en: string;
pictureRequired: boolean;
textRequired: boolean;
points: number;
extraPoints: number | null;
extraText: string | null;
extraText_de: string | null;
extraText_en: string | null;
textSolution: string | null;
};
}>();
@@ -139,7 +145,7 @@ async function submit() {
<ClickableImage
v-if="answer.picture?.url"
:src="answer.picture.url"
:title="`${quest.title} ${answer.team.name}`"
:title="`${tKey(quest, 'title')} ${answer.team.name}`"
class="h-48"
/>
<h3 v-if="quest.textSolution">Solution:</h3>
@@ -172,6 +178,17 @@ async function submit() {
messages="Special showcase at the end, before the final leaderboard"
/>
</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
v-model="formData.cloned.value.review"
label="Review"
+3
View File
@@ -69,6 +69,7 @@ async function kick(member: Team['members'][number]) {
init({
title: 'Error while kicking',
color: 'danger',
// @ts-expect-error weird error stuff
message: e?.message || e?.statusMessage || t(parseError(e))
});
}
@@ -97,6 +98,7 @@ async function addUser() {
init({
title: 'Error while adding',
color: 'danger',
// @ts-expect-error weird error stuff
message: e?.message || e?.statusMessage || t(parseError(e))
});
}
@@ -135,6 +137,7 @@ async function deleteTeam() {
init({
title: 'Error while deleting',
color: 'danger',
// @ts-expect-error weird error stuff
message: e?.message || e?.statusMessage || t(parseError(e))
});
}
+51
View File
@@ -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 -1
View File
@@ -160,4 +160,4 @@
"uploadFile": "Datei hochladen",
"voteRating": "vote rating {value} of {max}"
}
}
}
+1 -1
View File
@@ -160,4 +160,4 @@
"uploadFile": "Upload file",
"voteRating": "vote rating {value} of {max}"
}
}
}
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18nKey } from '~/composables/useI18nKey';
import { useTitleStore } from '~/stores/title';
definePageMeta({
@@ -9,6 +10,7 @@ const app = useNuxtApp();
const { loggedIn } = useUserSession();
const localePath = useLocalePath();
const { t } = useI18n();
const { tKey, hasKey, tSluggy } = useI18nKey();
const router = useRouter();
const { huntSlug } = app.$slugData;
@@ -21,7 +23,7 @@ const { data, pending, refresh, error } = await useFetch(
useHead({
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;
// TODO: handle data!
titleStore.title = data.value ? tKey(data.value, 'title').value : undefined;
</script>
<template>
@@ -40,7 +41,11 @@ titleStore.title = data.value?.title;
<VaButtonGroup>
<VaButton
icon="arrow_back"
:to="$localePath(`/hunt/${sluggy(data?.hunt || huntSlug!)}`)"
:to="
$localePath(
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
)
"
>Back to Hunt</VaButton
>
<VaButton icon="replay" color="success" @click="refresh">Refresh</VaButton>
@@ -54,7 +59,7 @@ titleStore.title = data.value?.title;
<div v-if="data">
<p>{{ data.points }} 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 v-if="data && data.answers.length > 0">
<VaAccordion multiple stateful>
+34 -17
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import ClickableImage from '~/components/ClickableImage.vue';
import { useI18nKey } from '~/composables/useI18nKey';
import { useTitleStore } from '~/stores/title';
import { fullDate } from '~~/server/utils/date';
@@ -12,7 +13,8 @@ const app = useNuxtApp();
const { user } = useUserSession();
const { huntSlug, questSlug } = app.$slugData;
const { t, d } = useI18n();
const { t, d, locale } = useI18n();
const { tKey, hasKey, tSluggy } = useI18nKey();
const titleStore = useTitleStore();
@@ -20,13 +22,18 @@ const { data, pending, refresh, error } = await useFetch(
`/api/quest/${questSlug?.id}`
);
titleStore.title = data.value?.title;
titleStore.title = data.value ? tKey(data.value, 'title').value : undefined;
useHead({
title: t('layouts.title', {
title:
[
data.value?.title || questSlug?.name,
data.value?.hunt?.name || huntSlug?.name
hasKey(data.value, 'title')
? 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)
.join(' | ') || t('layouts.default_tile')
@@ -52,6 +59,7 @@ async function submit() {
submitError.value = '';
const body = new FormData();
body.set('lang', locale.value);
if (data.value?.answer?.updatedAt) {
body.set('updatedAt', data.value?.answer?.updatedAt);
@@ -100,7 +108,11 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
<VaButtonGroup>
<VaButton
icon="arrow_back"
:to="$localePath(`/hunt/${sluggy(data?.hunt || huntSlug!)}`)"
:to="
$localePath(
`/hunt/${data ? tSluggy(data.hunt).value : sluggy(huntSlug!)}`
)
"
>Back to Hunt</VaButton
>
<VaButton
@@ -118,7 +130,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
color="danger"
:to="
$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
@@ -138,19 +150,21 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
<ClickableImage
v-if="data?.picture"
:src="data?.picture?.url"
:title="data?.title || questSlug?.name || ''"
:title="(data ? tKey(data, 'title').value : questSlug?.name) || ''"
/>
<VaCardContent>
<div class="flex flex-col gap-4">
<p class="text-lg">{{ data.description }}</p>
<p v-if="data.question" class="text-xl font-bold">
{{ data.textRequired ? '❓' : '❔' }}: {{ data.question }}
<p class="text-lg">{{ tKey(data, 'description') }}</p>
<p v-if="hasKey(data, 'question')" class="text-xl font-bold">
{{ data.textRequired ? '❓' : '❔' }}:
{{ tKey(data, 'question') }}
</p>
<p v-if="data.question">
<p v-if="hasKey(data, 'question')">
🖋: {{ t(`enums.text_type.${data.textType}`) }}
</p>
<p v-if="data.extraText">
<span class="uppercase">Extra points:</span> {{ data.extraText }}
<p v-if="hasKey(data, 'extraText')">
<span class="uppercase">Extra points:</span>
{{ tKey(data, 'extraText') }}
</p>
</div>
</VaCardContent>
@@ -166,7 +180,10 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
{{ d(data.answer.review.updatedAt, fullDate) }}</span
>
</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
v-for="(line, i) in data.answer.review.review.split('\n')"
:key="i"
@@ -186,9 +203,9 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
<VaCardContent>
<div class="flex flex-col gap-4">
<VaInput
:label="data.question"
:label="tKey(data, 'question').value"
:disabled="uploading || data.answer?.final"
v-if="data.question"
v-if="hasKey(data, 'question')"
placeholder="Your answer"
:required-mark="data.textRequired"
v-model="answer"
@@ -198,7 +215,7 @@ const createImageURL = (file: File) => URL.createObjectURL(file);
:type="textType[data.textType]"
:messages="[
t(`enums.text_type.${data.textType}`),
data.textRequired ? 'Required Answer' : undefined
data.textRequired ? 'Required Answer' : ''
]"
>
<template v-if="data.textUnit" #appendInner>
+26 -8
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import superjson, { type SuperJSONResult } from 'superjson';
import { useI18nKey } from '~/composables/useI18nKey';
import { useTitleStore } from '~/stores/title';
import { fullDate } from '~~/server/utils/date';
@@ -11,6 +12,7 @@ const app = useNuxtApp();
const { loggedIn } = useUserSession();
const localePath = useLocalePath();
const { t, d } = useI18n();
const { tKey, hasKey } = useI18nKey();
const router = useRouter();
const { confirm } = useModal();
@@ -36,7 +38,7 @@ const formData = useCloned(data, {
useHead({
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);
@@ -114,6 +116,7 @@ async function kick(member: {
init({
title: 'Error while kicking',
color: 'danger',
// @ts-expect-error weird error stuff
message: e?.message || e?.statusMessage || t(parseError(e))
});
}
@@ -143,6 +146,7 @@ async function addUser() {
init({
title: 'Error while adding',
color: 'danger',
// @ts-expect-error weird error stuff
message: e?.message || e?.statusMessage || t(parseError(e))
});
}
@@ -157,17 +161,31 @@ async function addUser() {
<h1>admin</h1>
<VaForm v-if="formData.cloned.value && data" class="flex flex-col gap-4">
<VaInput
label="Name"
v-model="formData.cloned.value.name"
label="Name DE"
v-model="formData.cloned.value.name_de"
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"
/>
<VaTextarea
label="Description"
v-model="formData.cloned.value.description"
label="Description DE"
v-model="formData.cloned.value.description_de"
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"
/>
<VaCheckbox label="Virtual" v-model="formData.cloned.value.virtual" />
+3 -2
View File
@@ -7,13 +7,14 @@ definePageMeta({
const app = useNuxtApp();
const { loggedIn } = useUserSession();
const { t } = useI18n();
const { tKey } = useI18nKey();
const { name, id } = app.$slugData.huntSlug!;
const titleStore = useTitleStore();
type Picture = {
quest: { id: number; title: string };
quest: { id: number; title_de: string; title_en: string };
team: {
name: string;
id: number;
@@ -81,7 +82,7 @@ titleStore.title = data.value?.name;
<p
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 }}
</p></template
></VaCarousel
+27 -16
View File
@@ -5,8 +5,8 @@ import { fullDate } from '~~/server/utils/date';
const app = useNuxtApp();
const { loggedIn } = useUserSession();
const localePath = useLocalePath();
const { t, d } = useI18n();
const { tKey, hasKey, tSluggy } = useI18nKey();
const { name, id } = app.$slugData.huntSlug!;
@@ -18,13 +18,13 @@ const hideAnswers = ref(true);
useHead({
title: t('layouts.title', {
title: data.value?.name || name
title: hasKey(data.value, 'name') ? tKey(data.value!, 'name').value : name
})
});
watch(loggedIn, () => refresh());
titleStore.title = data.value?.name;
titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
async function invite() {
if (!data.value?.ownTeam) {
@@ -34,7 +34,7 @@ async function invite() {
const clipboardItemData = {
// 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': `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);
await navigator.clipboard.write([clipboardItem]);
@@ -43,7 +43,7 @@ async function invite() {
<template>
<h1 class="text-2xl">
{{ data?.name || name }}
{{ data ? tKey(data, 'name') : name }}
<VaChip v-if="data?.totalScore" class="float-right" color="success"
>Total points: {{ data!!.totalScore }}</VaChip
@@ -58,40 +58,43 @@ async function invite() {
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
color="success"
icon="leaderboard"
:to="$localePath(`/hunt/${sluggy(data)}/showcase`)"
:to="$localePath(`/hunt/${tSluggy(data).value}/showcase`)"
>Showcase</VaButton
>
<VaButton
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
color="success"
icon="collections"
:to="$localePath(`/hunt/${sluggy(data)}/diashow`)"
:to="$localePath(`/hunt/${tSluggy(data).value}/diashow`)"
>Diashow</VaButton
>
<VaButton
v-if="(data?.revealAnswers && data?.revealQuests) || data?.isMember"
color="success"
icon="auto_awesome_mosaic"
:to="$localePath(`/hunt/${sluggy(data)}/pictures`)"
:to="$localePath(`/hunt/${tSluggy(data).value}/pictures`)"
>All pictures</VaButton
>
<VaButton
v-if="data?.isMember"
color="danger"
icon="edit"
:to="$localePath(`/hunt/${sluggy(data)}/admin`)"
:to="$localePath(`/hunt/${tSluggy(data).value}/admin`)"
>Admin</VaButton
><VaButton
v-if="data?.isMember"
color="danger"
icon="groups"
:to="$localePath(`/hunt/${sluggy(data)}/teams`)"
:to="$localePath(`/hunt/${tSluggy(data).value}/teams`)"
>Teams</VaButton
>
</div>
<div v-if="data">
<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 }}
</p>
</div>
@@ -99,7 +102,7 @@ async function invite() {
v-if="data?.picture"
:src="data?.picture?.url"
class="h-48"
:title="data?.name || name"
:title="data ? tKey(data, 'name').value : name"
/>
<p v-if="data.start">
Start:
@@ -142,7 +145,7 @@ async function invite() {
<VaButton
color="info"
icon="login"
:to="$localePath(`/login?to=/hunt/${sluggy(data)}`)"
:to="$localePath(`/login?to=/hunt/${tSluggy(data).value}`)"
>Login first</VaButton
>
</VaCardActions>
@@ -180,7 +183,7 @@ async function invite() {
<template #loader> <VaProgressCircle indeterminate /> </template
></VaImage>
<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">
<VaDivider />
@@ -189,7 +192,11 @@ async function invite() {
</VaCardContent>
<VaCardActions>
<VaButton
:to="$localePath(`/hunt/${sluggy(data)}/${sluggy(quest)}`)"
:to="
$localePath(
`/hunt/${tSluggy(data).value}/${tSluggy(quest).value}`
)
"
color="primary"
icon="rate_review"
class="flex-grow"
@@ -199,7 +206,11 @@ async function invite() {
v-if="data.isMember"
color="danger"
: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
>
</VaCardActions>
+6 -5
View File
@@ -7,13 +7,14 @@ definePageMeta({
const app = useNuxtApp();
const { loggedIn } = useUserSession();
const { t } = useI18n();
const { tKey, hasKey } = useI18nKey();
const { name, id } = app.$slugData.huntSlug!;
const titleStore = useTitleStore();
type Picture = {
quest: { id: number; title: string };
quest: { id: number; title_de: string; title_en: string };
team: {
name: string;
id: number;
@@ -47,7 +48,7 @@ const { data, refresh, error } = await useFetch(`/api/hunt/${id}/diashow`, {
const cols = pictures.reduce(
(c, p, i) => {
c[i % 4].push(p);
c[i % 4]!.push(p);
return c;
},
new Array(4).fill(0).map(() => [] as Picture[])
@@ -59,14 +60,14 @@ const { data, refresh, error } = await useFetch(`/api/hunt/${id}/diashow`, {
useHead({
title: t('layouts.two_title', {
title: data.value?.name || name,
title: hasKey(data.value, 'name') ? tKey(data.value!, 'name').value : name,
subtitle: 'Diashow'
})
});
watch(loggedIn, () => refresh());
titleStore.title = data.value?.name;
titleStore.title = data.value ? tKey(data.value, 'name').value : undefined;
</script>
<template>
@@ -81,7 +82,7 @@ titleStore.title = data.value?.name;
:key="pic.picture.url"
:src="pic.picture.url"
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>
<VaAlert
+13 -4
View File
@@ -7,6 +7,7 @@ definePageMeta({
layout: 'full'
});
const { t, d } = useI18n();
const { tKey, hasKey, tSluggy } = useI18nKey();
const app = useNuxtApp();
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"
>
<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">
<span v-if="data.start">{{ d(data.start, fullDate) }}</span
><span v-if="data.start && data.end"> </span
@@ -94,11 +97,17 @@ const columns = leaderboardColumns.map((c) =>
/>
</div>
</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="my-8 flex w-full flex-row justify-around">
<h3 class="text-3xl">{{ quest.title }}</h3>
<p class="text-xl font-bold">{{ quest.question }}</p>
<h3 class="text-3xl">{{ tKey(quest, 'title') }}</h3>
<p class="text-xl font-bold" v-if="hasKey(quest, 'question')">
{{ tKey(quest, 'question') }}
</p>
<p v-if="quest.textSolution" class="text-3xl">
{{ quest.textSolution }}
</p>
+3 -2
View File
@@ -10,6 +10,7 @@ const app = useNuxtApp();
const { loggedIn } = useUserSession();
const localePath = useLocalePath();
const { t } = useI18n();
const { tKey, hasKey } = useI18nKey();
const router = useRouter();
const { name, id } = app.$slugData.huntSlug!;
@@ -21,7 +22,7 @@ const { data, pending, refresh } = await useFetch(
useHead({
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']>;
export type Team = Data['teams'][number];
+12 -10
View File
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { useI18nKey } from '~/composables/useI18nKey';
import { fullDate } from '~~/server/utils/date';
const { loggedIn } = useUserSession();
const { data, pending, refresh } = await useFetch('/api/home');
const { t, d } = useI18n();
const { tKey, tSluggy } = useI18nKey();
watch(loggedIn, () => refresh());
</script>
@@ -23,18 +25,18 @@ watch(loggedIn, () => refresh());
v-for="hunt in data.own"
:key="hunt.id"
stripe
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
>
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
<ClickableImage
v-if="hunt.picture"
:src="hunt.picture?.url"
:title="hunt.name"
:title="tKey(hunt, 'name').value"
/>
<VaCardContent>
<h3 class="text-xl">{{ hunt.name }}</h3>
{{ hunt.description }}
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
{{ tKey(hunt, 'description') }}
<VaDivider />
<div class="flex flex-col gap-2">
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
@@ -78,17 +80,17 @@ watch(loggedIn, () => refresh());
<VaCard
v-for="hunt in data.others"
:key="hunt.id"
:to="$localePath(`/hunt/${sluggy(hunt)}`)"
:to="$localePath(`/hunt/${tSluggy(hunt).value}`)"
>
<VaCardTitle>{{ hunt.name }}</VaCardTitle>
<VaCardTitle>{{ tKey(hunt, 'name') }}</VaCardTitle>
<ClickableImage
v-if="hunt.picture"
:src="hunt.picture?.url"
:title="hunt.name"
:title="tKey(hunt, 'name').value"
/>
<VaCardContent>
<h3 class="text-xl">{{ hunt.name }}</h3>
{{ hunt.description }}
<h3 class="text-xl">{{ tKey(hunt, 'name') }}</h3>
{{ tKey(hunt, 'description') }}
<VaDivider />
<div class="flex flex-col gap-2">
<p>{{ t('page.home.n_quests', hunt._count.quests) }}</p>
+9 -6
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { useI18nKey } from '~/composables/useI18nKey';
definePageMeta({
middleware: ['auth']
});
@@ -8,6 +10,7 @@ const { t } = useI18n();
const { loggedIn, fetch } = useUserSession();
const localePath = useLocalePath();
const validation = useValidation();
const { tKey, tSluggy } = useI18nKey();
watch(loggedIn, (isLoggedIn) => {
if (!isLoggedIn) {
router.push(localePath('/login'));
@@ -234,9 +237,8 @@ async function submitPassword() {
</VaCard>
<VaCard stripe stripe-color="primary">
<VaCardTitle>Uploaded pictures</VaCardTitle>
<VaCardContent>
<VaCardContent v-if="data && data.uploadedFiles.length > 0">
<VaCarousel
v-if="data"
stateful
:items="data.uploadedFiles"
arrows
@@ -248,22 +250,23 @@ async function submitPassword() {
<template #default="{ item }"
><ClickableImage
: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
:to="
$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"
>
{{ item.answer.quest.title }}
{{ item.answer.quest.hunt.name }}
{{ tKey(item.answer.quest, 'title') }}
{{ tKey(item.answer.quest.hunt, 'name') }}
</NuxtLink></template
>
</VaCarousel>
</VaCardContent>
<VaCardContent v-else> No pictures uploaded yet. </VaCardContent>
</VaCard>
</div>
</template>