Files
pichunt/app/components/admin/Answer.vue
T

220 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { fullDate } from '~~/server/utils/date';
const { d, t } = useI18n();
const props = defineProps<{
answer: {
id: number;
text: string | null;
updatedAt: string;
team: { id: number; name: string };
final: boolean;
review: {
internalNote: string | null;
rankingValue: number | null;
showcase: boolean;
score: number | null;
review: string | null;
updatedAt: string;
reviewer: { name: string; id: number } | null;
} | null;
member: { name: string; id: number } | null;
picture: {
url: string;
createdAt: string;
} | null;
};
quest: {
title: string;
pictureRequired: boolean;
textRequired: boolean;
points: number;
extraPoints: number | null;
extraText: string | null;
textSolution: string | null;
};
}>();
const emits = defineEmits<{
update: [];
}>();
const formData = useCloned(
() =>
props.answer.review ?? {
internalNote: '',
rankingValue: 0,
showcase: false,
score: props.quest.points,
review: ''
}
);
const header = computed(() => {
const parts = [
props.answer.team.name,
props.answer.picture?.url ? '📸✅' : '📷❌',
props.answer.text ? '📝✅' : '📝❌',
props.answer.review ? '🔍✅' : '🔍❌',
props.answer.review?.score ?? 'No points',
d(props.answer.updatedAt, fullDate),
props.answer.review?.rankingValue,
props.answer.review?.showcase ? '★' : null
];
return parts.filter((s) => !!s).join(' ');
});
const style = computed<[string, string]>(() => {
if (props.answer.review?.review && props.answer.review?.score !== null) {
return ['success', 'check'];
}
if (!props.answer.final) {
return ['', ''];
}
if (props.answer.review?.review || props.answer.review?.score !== null) {
return ['danger', 'priority_high'];
}
if (
(props.quest.pictureRequired ? !!props.answer.picture?.url : true) &&
(props.quest.textRequired ? !!props.answer.text : true)
) {
return ['primary', 'info'];
}
return ['', ''];
});
const pending = ref(false);
const submitError = ref('');
async function submit() {
pending.value = true;
submitError.value = '';
try {
await $fetch(`/api/admin/answer/${props.answer.id}/review`, {
method: 'POST',
body: formData.cloned.value
});
emits('update');
formData.sync();
} catch (e) {
if (e instanceof Error && 'statusCode' in e && e.statusCode === 409) {
submitError.value = 'Conflict: Please refresh';
} else {
submitError.value = parseError(e);
}
}
pending.value = false;
}
</script>
<template>
<VaCollapse :header="header" :icon="style[1]" :color="style[0]">
<template #content>
<div class="flex flex-col gap-2">
<div
class="mb-2 grid grid-cols-3 justify-between gap-8 rounded-lg bg-gray-200 p-4 font-bold md:grid-cols-6"
>
<p v-if="!answer.final">NOT FINAL</p>
<p>
{{
answer.picture?.url ? '📸✅ Picture done' : '📷❌ Picture missing'
}}
</p>
<p>{{ answer.text ? '📝✅ Answer done' : '📝❌ Answer missing' }}</p>
<p>
{{ answer.review ? '🔍✅ Review done' : '🔍❌ Review missing' }}
</p>
<p v-if="answer.review?.rankingValue">
<VaIcon name="leaderboard" color="primary" />
{{ answer.review?.rankingValue }}
</p>
<p v-if="answer.review?.showcase">
<VaIcon name="auto_awesome" color="warning" /> Showcase
</p>
<p>{{ answer.review?.score ?? 'No' }} points</p>
</div>
<ClickableImage
v-if="answer.picture?.url"
:src="answer.picture.url"
:title="`${quest.title} ${answer.team.name}`"
class="h-48"
/>
<h3 v-if="quest.textSolution">Solution:</h3>
<p v-if="quest.textSolution" class="bg-green-300 p-2 font-mono text-xl">
{{ quest.textSolution }}
</p>
<h3>Teams Answer:</h3>
<p class="bg-gray-300 p-2 font-mono text-xl">{{ answer.text }}</p>
<VaDivider />
<div
class="flex flex-col justify-between gap-4 md:flex-row md:items-center"
>
<VaInput
v-model="formData.cloned.value.score"
label="Score"
type="number"
:min="0"
messages="Including extra points"
/>
<VaInput
v-model="formData.cloned.value.rankingValue"
label="Ranking value"
type="number"
:min="0"
messages="Useful for sorting metric-based answers (e.g. distance, time), sorted descending"
/>
<VaCheckbox
v-model="formData.cloned.value.showcase"
label="Showcase"
messages="Special showcase at the end, before the final leaderboard"
/>
</div>
<VaTextarea
v-model="formData.cloned.value.review"
label="Review"
messages="Will be shown publicly!"
/>
<VaDivider />
<VaTextarea
v-model="formData.cloned.value.internalNote"
label="Internal note"
messages="Only internal, the team won't see this!"
/>
<p v-if="answer.review?.reviewer">
Reviewed by {{ answer.review?.reviewer?.name }} at
{{ d(answer.review.updatedAt, fullDate) }}
</p>
<VaAlert
v-if="submitError.length > 0"
color="danger"
class="w-full text-center"
>{{ t(submitError) }}</VaAlert
>
<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="emits('update')"
>Refresh</VaButton
>
<VaButton
:disabled="!formData.isModified.value"
@click.prevent="submit"
>Save</VaButton
>
</VaButtonGroup>
</div>
</template>
</VaCollapse>
</template>
<style scoped></style>