#2 - reset user ratings
This commit is contained in:
parent
dfa45436d4
commit
209e05f959
8 changed files with 142 additions and 25 deletions
7
src/db/answers.ts
Normal file
7
src/db/answers.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { db } from ".";
|
||||
import { surveyAnswersTable } from "./schema";
|
||||
|
||||
export async function deleteAnswers(participandId: number) {
|
||||
await db.delete(surveyAnswersTable).where(eq(surveyAnswersTable.participantId, participandId));
|
||||
}
|
33
src/lib/components/WarningDialog.svelte
Normal file
33
src/lib/components/WarningDialog.svelte
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLDialogAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
dialogRef: HTMLDialogElement | null;
|
||||
onAccept: () => void;
|
||||
buttons?: Snippet<[]>;
|
||||
} & HTMLDialogAttributes;
|
||||
|
||||
let { title, dialogRef = $bindable(), onAccept, children, buttons }: Props = $props();
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
bind:this={dialogRef}
|
||||
class="border-2 border-red-300 p-4 backdrop:bg-black/40 backdrop:backdrop-blur-sm"
|
||||
>
|
||||
<h2 class="mb-4 text-2xl">{title}</h2>
|
||||
<div>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{#if buttons}
|
||||
{@render buttons()}
|
||||
{:else}
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<button onclick={() => dialogRef?.close()} class="w-40 justify-self-end bg-slate-400"
|
||||
>Cancel</button
|
||||
>
|
||||
<button class="w-40 bg-red-500" onclick={onAccept}>Delete</button>
|
||||
</div>
|
||||
{/if}
|
||||
</dialog>
|
14
src/lib/components/icons/DeleteIcon.svelte
Normal file
14
src/lib/components/icons/DeleteIcon.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9.75 14.25 12m0 0 2.25 2.25M14.25 12l2.25-2.25M14.25 12 12 14.25m-2.58 4.92-6.374-6.375a1.125 1.125 0 0 1 0-1.59L9.42 4.83c.21-.211.497-.33.795-.33H19.5a2.25 2.25 0 0 1 2.25 2.25v10.5a2.25 2.25 0 0 1-2.25 2.25h-9.284c-.298 0-.585-.119-.795-.33Z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 460 B |
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z"
|
||||
|
|
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 627 B |
14
src/lib/components/icons/MenuIcon.svelte
Normal file
14
src/lib/components/icons/MenuIcon.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 352 B |
|
@ -1,12 +1,13 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad, RouteParams } from './$types';
|
||||
import { loadSurvey } from '$lib/queries';
|
||||
|
||||
import debug from 'debug';
|
||||
import { deleteAnswers } from '../../../../db/answers';
|
||||
|
||||
const log = debug('survey:admin');
|
||||
|
||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
async function loadSurveyData(params: RouteParams, locals: App.Locals) {
|
||||
const surveyId = parseInt(params.surveyId);
|
||||
|
||||
if (isNaN(surveyId)) {
|
||||
|
@ -24,4 +25,26 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
|||
error(404, 'Survey not found');
|
||||
}
|
||||
return surveyData;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||
return await loadSurveyData(params, locals);
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
deleteAnswers: async ({ params, locals, request }) => {
|
||||
const survey = await loadSurveyData(params, locals);
|
||||
|
||||
let formData = await request.formData();
|
||||
const participantId = parseInt(formData.get('participantId')?.toString() ?? '');
|
||||
|
||||
if (isNaN(participantId)) {
|
||||
log('Invalid participant ID when trying to delete answers: %s', formData.get('participandId')?.toString());
|
||||
error(400, 'Invalid participant ID');
|
||||
}
|
||||
|
||||
await deleteAnswers(participantId);
|
||||
|
||||
redirect(303, survey.id.toString());
|
||||
}
|
||||
}
|
|
@ -11,6 +11,9 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { success } from '$lib/toast';
|
||||
import MarkdownBlock from '$lib/components/MarkdownBlock.svelte';
|
||||
import MenuIcon from '$lib/components/icons/MenuIcon.svelte';
|
||||
import DeleteIcon from '$lib/components/icons/DeleteIcon.svelte';
|
||||
import WarningDialog from '$lib/components/WarningDialog.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
|
@ -26,7 +29,10 @@
|
|||
navigator.clipboard.writeText(link);
|
||||
}
|
||||
|
||||
let dialogRef: HTMLDialogElement | null = null;
|
||||
let deleteSurveyDialogRef: HTMLDialogElement | null = $state(null);
|
||||
let deleteAnswersDialogRef: HTMLDialogElement | null = $state(null);
|
||||
|
||||
let participantAnswersDeletionCandidateId = $state<number | null>(null);
|
||||
|
||||
async function deleteSurvey() {
|
||||
await fetch('', {
|
||||
|
@ -50,7 +56,7 @@
|
|||
</a>
|
||||
<button
|
||||
onclick={() => {
|
||||
dialogRef?.showModal();
|
||||
deleteSurveyDialogRef?.showModal();
|
||||
}}
|
||||
class="ml-2 inline-block"
|
||||
aria-label="Delete"
|
||||
|
@ -84,6 +90,19 @@
|
|||
>
|
||||
<LinkIcon />
|
||||
</button>
|
||||
{#if participant.answers.length > 0}
|
||||
<button
|
||||
aria-label="Reset ratings"
|
||||
title="Clear answers"
|
||||
class="text-red-500 hover:bg-slate-200"
|
||||
onclick={() => {
|
||||
participantAnswersDeletionCandidateId = participant.id;
|
||||
deleteAnswersDialogRef?.showModal();
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
@ -92,20 +111,27 @@
|
|||
<Diagram data={diagramData} />
|
||||
</div>
|
||||
|
||||
<dialog
|
||||
bind:this={dialogRef}
|
||||
class="border-2 border-red-300 p-4 backdrop:bg-black/40 backdrop:backdrop-blur-sm"
|
||||
>
|
||||
<h2 class="mb-4 text-2xl">Delete survey</h2>
|
||||
<p>
|
||||
Are you sure you want to delete the survey. <span class="font-bold text-red-400"
|
||||
>This action cannot be undone.</span
|
||||
>
|
||||
</p>
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<button onclick={() => dialogRef?.close()} class="w-40 justify-self-end bg-slate-400"
|
||||
>Cancel</button
|
||||
>
|
||||
<button class="w-40 bg-red-500" onclick={deleteSurvey}>Delete</button>
|
||||
</div>
|
||||
</dialog>
|
||||
<WarningDialog title="Delete survey" bind:dialogRef={deleteSurveyDialogRef} onAccept={deleteSurvey}>
|
||||
<p>Are you sure you want to delete the survey.</p>
|
||||
<p class="font-bold text-red-400">This action cannot be undone.</p>
|
||||
</WarningDialog>
|
||||
|
||||
<WarningDialog title="Delete answers" onAccept={() => {}} bind:dialogRef={deleteAnswersDialogRef}>
|
||||
<form id="delete-answers" method="POST" action="?/deleteAnswers">
|
||||
<input type="hidden" name="participantId" value={participantAnswersDeletionCandidateId} />
|
||||
<p>
|
||||
Are you sure you want to remove this user's ratings? This will delete whatever answers the
|
||||
user has given and allow them to submit new ratings instead.
|
||||
</p>
|
||||
<p class="font-bold text-red-400">This action cannot be undone.</p>
|
||||
</form>
|
||||
{#snippet buttons()}
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onclick={() => deleteAnswersDialogRef?.close()}
|
||||
class="w-40 justify-self-end bg-slate-400">Cancel</button
|
||||
>
|
||||
<button class="w-40 bg-red-500" form="delete-answers">Delete</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</WarningDialog>
|
||||
|
|
|
@ -24,7 +24,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
|||
|
||||
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
|
||||
log_load('Answers already submitted: %s', params.accessToken);
|
||||
error(400, 'Answers already submitted');
|
||||
error(400, 'You have already submitted your answers. If you feel that this is wrong, please contact the survey creator to reset your answers.');
|
||||
}
|
||||
|
||||
const survey = results[0].surveys_table;
|
||||
|
@ -57,7 +57,7 @@ export const actions = {
|
|||
|
||||
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
|
||||
log_store('Answers already submitted: %s', params.accessToken);
|
||||
error(400, 'Answers already submitted');
|
||||
error(400, 'You have already submitted your answers. If you feel that this is wrong, please contact the survey creator to reset your answers.');
|
||||
}
|
||||
|
||||
const survey = results[0].surveys_table;
|
||||
|
|
Loading…
Add table
Reference in a new issue