parent
							
								
									0d32c93787
								
							
						
					
					
						commit
						1ab979b5b4
					
				
					 5 changed files with 103 additions and 52 deletions
				
			
		| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
		primary: 'bg-sky-200 hover:bg-sky-300 text-black border border-black rounded',
 | 
			
		||||
		secondary: 'bg-slate-200 hover:bg-slate-300 text-black border border-black rounded',
 | 
			
		||||
		tertiary: 'bg-white hover:bg-slate-100 text-black border border-slate-500 rounded',
 | 
			
		||||
		ghost: 'bg-white hover:bg-slate-100 text-sky-600'
 | 
			
		||||
		ghost: 'bg-none hover:bg-slate-100 text-sky-600'
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,12 @@
 | 
			
		|||
	import { flatGroup } from 'd3-array';
 | 
			
		||||
	import { Axis, Chart, Points, Spline, Svg } from 'layerchart';
 | 
			
		||||
 | 
			
		||||
	const {
 | 
			
		||||
		data
 | 
			
		||||
	}: {
 | 
			
		||||
	type Props = {
 | 
			
		||||
		reviewMode: boolean;
 | 
			
		||||
		data: { skill: string; participant: number; rating: number | undefined }[];
 | 
			
		||||
	} = $props();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const { data, reviewMode }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	const averageValues = flatGroup(data, (d) => d.skill)
 | 
			
		||||
		.map(([skill, ratings]) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +35,15 @@
 | 
			
		|||
		{ stroke: 'stroke-cyan-300', fill: 'fill-cyan-400' },
 | 
			
		||||
		{ stroke: 'stroke-pink-300', fill: 'fill-pink-400' }
 | 
			
		||||
	];
 | 
			
		||||
 | 
			
		||||
	const anonymousColor = { stroke: 'stroke-slate-300', fill: 'fill-slate-400' };
 | 
			
		||||
 | 
			
		||||
	function getColor(idx: number) {
 | 
			
		||||
		if (reviewMode) {
 | 
			
		||||
			return anonymousColor;
 | 
			
		||||
		}
 | 
			
		||||
		return colors[idx % colors.length];
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="chart-container">
 | 
			
		||||
| 
						 | 
				
			
			@ -59,13 +69,9 @@
 | 
			
		|||
				<Spline
 | 
			
		||||
					data={[...ratings, ratings[0]]}
 | 
			
		||||
					curve={curveLinear}
 | 
			
		||||
					class="{colors[idx % colors.length].stroke} stroke-[2px]"
 | 
			
		||||
				/>
 | 
			
		||||
				<Points
 | 
			
		||||
					data={ratings}
 | 
			
		||||
					class="{colors[idx % colors.length].stroke} {colors[idx % colors.length]
 | 
			
		||||
						.fill} stroke-[2px]"
 | 
			
		||||
					class="{getColor(idx).stroke} stroke-[2px]"
 | 
			
		||||
				/>
 | 
			
		||||
				<Points data={ratings} class="{getColor(idx).stroke} {getColor(idx).fill} stroke-[2px]" />
 | 
			
		||||
			{/each}
 | 
			
		||||
			<Spline
 | 
			
		||||
				data={[...averageValues, averageValues[0]]}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										15
									
								
								src/lib/components/icons/Eye.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/lib/components/icons/Eye.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
 | 
			
		||||
	/>
 | 
			
		||||
	<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 493 B  | 
							
								
								
									
										14
									
								
								src/lib/components/icons/EyeSlash.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/components/icons/EyeSlash.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="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
 | 
			
		||||
	/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 544 B  | 
| 
						 | 
				
			
			@ -17,6 +17,9 @@
 | 
			
		|||
	import ShareIcon from '$lib/components/icons/ShareIcon.svelte';
 | 
			
		||||
	import { checkAccess } from '$lib/helpers/shared/permissions';
 | 
			
		||||
	import { AccessLevel, type ParticipantId } from '$lib/types';
 | 
			
		||||
	import Button from '$lib/components/Button.svelte';
 | 
			
		||||
	import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
 | 
			
		||||
	import Eye from '$lib/components/icons/Eye.svelte';
 | 
			
		||||
 | 
			
		||||
	let { data }: { data: PageData } = $props();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +48,7 @@
 | 
			
		|||
	let deleteAnswersDialogRef: HTMLDialogElement | null = $state(null);
 | 
			
		||||
 | 
			
		||||
	let participantAnswersDeletionCandidateId = $state<number | null>(null);
 | 
			
		||||
 | 
			
		||||
	$inspect(data);
 | 
			
		||||
	let reviewMode = $state(false);
 | 
			
		||||
 | 
			
		||||
	async function deleteSurvey() {
 | 
			
		||||
		await fetch('', {
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +63,18 @@
 | 
			
		|||
		<a href="/" class="flex items-center" aria-label="Home" title="Home">
 | 
			
		||||
			<HomeIcon />
 | 
			
		||||
		</a>
 | 
			
		||||
		<Button
 | 
			
		||||
			kind="ghost"
 | 
			
		||||
			class="text-white hover:bg-indigo-900"
 | 
			
		||||
			onclick={() => (reviewMode = !reviewMode)}
 | 
			
		||||
			title={reviewMode ? 'Show details' : 'Hide details'}
 | 
			
		||||
		>
 | 
			
		||||
			{#if reviewMode}
 | 
			
		||||
				<Eye />
 | 
			
		||||
			{:else}
 | 
			
		||||
				<EyeSlash />
 | 
			
		||||
			{/if}
 | 
			
		||||
		</Button>
 | 
			
		||||
		{#if checkAccess(data, AccessLevel.Clone)}
 | 
			
		||||
			<a
 | 
			
		||||
				href="../survey/new?from={data.id}"
 | 
			
		||||
| 
						 | 
				
			
			@ -104,49 +118,51 @@
 | 
			
		|||
	<MarkdownBlock class="ml-4 mt-4 text-xs text-slate-500" value={data.description} />
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<h2 class="ml-2 mt-4 text-2xl">Participants</h2>
 | 
			
		||||
<ul class="disc ml-4">
 | 
			
		||||
	{#each data.participants as participant}
 | 
			
		||||
		<li>
 | 
			
		||||
			<span class="mr-5">
 | 
			
		||||
				{participant.email}
 | 
			
		||||
				{#if participant.answers.length > 0}
 | 
			
		||||
					<CheckIcon />
 | 
			
		||||
{#if !reviewMode}
 | 
			
		||||
	<h2 class="ml-2 mt-4 text-2xl">Participants</h2>
 | 
			
		||||
	<ul class="disc ml-4">
 | 
			
		||||
		{#each data.participants as participant}
 | 
			
		||||
			<li>
 | 
			
		||||
				<span class="mr-5">
 | 
			
		||||
					{participant.email}
 | 
			
		||||
					{#if participant.answers.length > 0}
 | 
			
		||||
						<CheckIcon />
 | 
			
		||||
					{/if}
 | 
			
		||||
				</span>
 | 
			
		||||
				{#if participant.accessToken}
 | 
			
		||||
					<button
 | 
			
		||||
						aria-label="Copy link to clipboard"
 | 
			
		||||
						class="text-sky-700"
 | 
			
		||||
						title="Copy link to clipboard"
 | 
			
		||||
						onclick={() => {
 | 
			
		||||
							copyLinkToClipboard(`${window.location.origin}/${participant.accessToken}`);
 | 
			
		||||
							success('Link copied to clipboard');
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<LinkIcon />
 | 
			
		||||
					</button>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</span>
 | 
			
		||||
			{#if participant.accessToken}
 | 
			
		||||
				<button
 | 
			
		||||
					aria-label="Copy link to clipboard"
 | 
			
		||||
					class="text-sky-700"
 | 
			
		||||
					title="Copy link to clipboard"
 | 
			
		||||
					onclick={() => {
 | 
			
		||||
						copyLinkToClipboard(`${window.location.origin}/${participant.accessToken}`);
 | 
			
		||||
						success('Link copied to clipboard');
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<LinkIcon />
 | 
			
		||||
				</button>
 | 
			
		||||
			{/if}
 | 
			
		||||
			{#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>
 | 
			
		||||
				{#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>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
{#if checkAccess(data, AccessLevel.ReadResult)}
 | 
			
		||||
	<div class="grid grid-cols-1 justify-items-center">
 | 
			
		||||
		<Diagram data={diagramData} />
 | 
			
		||||
		<Diagram data={diagramData} {reviewMode}/>
 | 
			
		||||
	</div>
 | 
			
		||||
{:else}
 | 
			
		||||
	<div class="text-center">You do not have permission to see the survey results</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue