Skip to content

Commit 18c6297

Browse files
committed
Allow the user to reset a single tutorial
1 parent 9ee182e commit 18c6297

File tree

3 files changed

+83
-14
lines changed

3 files changed

+83
-14
lines changed

frontend/src/lib/components/home/TutorialButton.svelte

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { CheckCircle2, Circle } from 'lucide-svelte'
2+
import { CheckCircle2, Circle, RefreshCw } from 'lucide-svelte'
33
import type { ComponentType } from 'svelte'
44
55
interface Props {
@@ -10,6 +10,7 @@
1010
isCompleted?: boolean
1111
disabled?: boolean
1212
comingSoon?: boolean
13+
onReset?: () => void
1314
}
1415
1516
let {
@@ -19,13 +20,18 @@
1920
onclick,
2021
isCompleted = false,
2122
disabled = false,
22-
comingSoon = false
23+
comingSoon = false,
24+
onReset
2325
}: Props = $props()
26+
27+
let isHovered = $state(false)
2428
</script>
2529

2630
<button
2731
onclick={disabled || comingSoon ? undefined : onclick}
2832
disabled={disabled || comingSoon}
33+
onmouseenter={() => (isHovered = true)}
34+
onmouseleave={() => (isHovered = false)}
2935
class="group relative flex items-center gap-4 w-full px-4 py-3 first-of-type:!border-t-0 first-of-type:rounded-t-md last-of-type:rounded-b-md [*:not(:last-child)]:border-b border-b border-light transition-colors text-left last:border-b-0 {disabled || comingSoon
3036
? 'opacity-50 cursor-not-allowed'
3137
: 'hover:bg-surface-hover'}"
@@ -50,17 +56,40 @@
5056

5157
<!-- Status -->
5258
<div class="flex items-center gap-1.5 flex-shrink-0">
53-
<span
54-
class="text-xs font-normal {isCompleted
55-
? 'text-green-500'
56-
: 'text-blue-300'}"
57-
>
58-
{isCompleted ? 'Completed' : 'Not started'}
59-
</span>
60-
{#if isCompleted}
61-
<CheckCircle2 size={14} class="text-green-500 flex-shrink-0" />
62-
{:else if !isCompleted}
63-
<Circle size={14} class="text-blue-300 flex-shrink-0" />
59+
{#if isCompleted && isHovered && onReset}
60+
<div
61+
role="button"
62+
tabindex="0"
63+
onclick={(e) => {
64+
e.stopPropagation()
65+
e.preventDefault()
66+
onReset()
67+
}}
68+
onkeydown={(e) => {
69+
if (e.key === 'Enter' || e.key === ' ') {
70+
e.stopPropagation()
71+
e.preventDefault()
72+
onReset()
73+
}
74+
}}
75+
class="flex items-center gap-1.5 px-2 py-1 text-xs font-normal text-secondary hover:text-primary hover:bg-surface-hover rounded transition-colors cursor-pointer"
76+
>
77+
<RefreshCw size={14} class="flex-shrink-0" />
78+
Reset
79+
</div>
80+
{:else}
81+
<span
82+
class="text-xs font-normal {isCompleted
83+
? 'text-green-500'
84+
: 'text-blue-300'}"
85+
>
86+
{isCompleted ? 'Completed' : 'Not started'}
87+
</span>
88+
{#if isCompleted}
89+
<CheckCircle2 size={14} class="text-green-500 flex-shrink-0" />
90+
{:else if !isCompleted}
91+
<Circle size={14} class="text-blue-300 flex-shrink-0" />
92+
{/if}
6493
{/if}
6594
</div>
6695
</button>

frontend/src/lib/tutorialUtils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,35 @@ export async function resetTutorialsByIndexes(tutorialIndexes: number[]) {
116116
})
117117
}
118118

119+
/**
120+
* Reset (mark as incomplete) a single tutorial by index
121+
*/
122+
export async function resetTutorialByIndex(tutorialIndex: number) {
123+
const currentTodos = get(tutorialsToDo)
124+
125+
// Add the tutorial index back to todos if not already present
126+
if (!currentTodos.includes(tutorialIndex)) {
127+
const aft = [...currentTodos, tutorialIndex]
128+
tutorialsToDo.set(aft)
129+
skippedAll.set(false)
130+
131+
// Get current progress bits
132+
const currentResponse = await UserService.getTutorialProgress()
133+
let bits: number = currentResponse.progress ?? 0
134+
135+
// Clear bit for this tutorial index
136+
const mask = 1 << tutorialIndex
137+
bits = bits & ~mask
138+
139+
await UserService.updateTutorialProgress({
140+
requestBody: {
141+
progress: bits,
142+
skipped_all: false
143+
}
144+
})
145+
}
146+
}
147+
119148
export async function syncTutorialsTodos() {
120149
const response = await UserService.getTutorialProgress()
121150
const bits: number = response.progress!

frontend/src/routes/(root)/(logged)/tutorials/+page.svelte

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
getTutorialProgressCompleted,
1616
skipAllTodos,
1717
skipTutorialsByIndexes,
18-
resetTutorialsByIndexes
18+
resetTutorialsByIndexes,
19+
resetTutorialByIndex
1920
} from '$lib/tutorialUtils'
2021
import { Button } from '$lib/components/common'
2122
import { RefreshCw, CheckCheck, CheckCircle2, Circle } from 'lucide-svelte'
@@ -155,6 +156,15 @@
155156
await syncTutorialsTodos()
156157
}
157158
159+
// Reset a single tutorial
160+
async function resetSingleTutorial(tutorialId: string) {
161+
const tutorial = currentTabConfig.tutorials.find((t) => t.id === tutorialId)
162+
if (!tutorial || tutorial.index === undefined) return
163+
164+
await resetTutorialByIndex(tutorial.index)
165+
await syncTutorialsTodos()
166+
}
167+
158168
// Calculate progress for each tab
159169
function getTabProgress(tabId: TabId) {
160170
const tabConfig = TUTORIALS_CONFIG[tabId]
@@ -297,6 +307,7 @@
297307
isCompleted={isTutorialCompleted(tutorial.id)}
298308
disabled={tutorial.active === false}
299309
comingSoon={tutorial.comingSoon}
310+
onReset={() => resetSingleTutorial(tutorial.id)}
300311
/>
301312
{/each}
302313
</div>

0 commit comments

Comments
 (0)