Add writing reviews for answers
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -63,6 +63,7 @@ titleStore.title = data.value?.title;
|
||||
:key="answer.id"
|
||||
:answer="answer"
|
||||
:quest="data"
|
||||
@update="refresh"
|
||||
/>
|
||||
</VaAccordion>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user