Add writing reviews for answers

This commit is contained in:
2025-08-14 20:33:39 +02:00
parent 5146e8e490
commit 9cb405be22
7 changed files with 116 additions and 13 deletions
+82 -7
View File
@@ -1,5 +1,7 @@
<script setup lang="ts">
const { d } = useI18n();
import { fullDate } from '~~/server/utils/date';
const { d, t } = useI18n();
const props = defineProps<{
answer: {
id: number;
@@ -13,6 +15,7 @@ const props = defineProps<{
showcase: boolean;
score: number | null;
review: string | null;
updatedAt: string;
reviewer: { name: string; id: number } | null;
} | null;
member: { name: string; id: number } | null;
@@ -32,6 +35,21 @@ const props = defineProps<{
};
}>();
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,
@@ -39,10 +57,7 @@ const header = computed(() => {
props.answer.text ? '📝✅' : '📝❌',
props.answer.review ? '🔍✅' : '🔍❌',
props.answer.review?.score ?? 'No points',
d(props.answer.updatedAt, {
dateStyle: 'medium',
timeStyle: 'medium'
}),
d(props.answer.updatedAt, fullDate),
props.answer.review?.rankingValue,
props.answer.review?.showcase ? '★' : null
];
@@ -68,6 +83,31 @@ const style = computed<[string, string]>(() => {
}
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>
@@ -113,29 +153,64 @@ const style = computed<[string, string]>(() => {
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 label="Review" messages="Will be shown publicly!" />
<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!"
/>
<VaButton>Save</VaButton>
<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>
+1 -1
View File
@@ -147,7 +147,7 @@ async function deleteTeam() {
<template>
<VaCollapse
:key="team.id"
:header="`${team.name} — ${team.members.length} members`"
:header="`${team.name} — ${team.members.length} members — ${team._count.answers} answers — ${team.score} points`"
>
<div class="flex flex-col gap-2">
<h2 class="text-xl">