Skip to content

Commit 7e0b636

Browse files
committed
Allow a user to mark as completed a single tutorial
1 parent 18c6297 commit 7e0b636

File tree

3 files changed

+112
-40
lines changed

3 files changed

+112
-40
lines changed

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { CheckCircle2, Circle, RefreshCw } from 'lucide-svelte'
2+
import { CheckCircle2, Circle, RefreshCw, CheckCheck } from 'lucide-svelte'
33
import type { ComponentType } from 'svelte'
44
55
interface Props {
@@ -11,6 +11,7 @@
1111
disabled?: boolean
1212
comingSoon?: boolean
1313
onReset?: () => void
14+
onComplete?: () => void
1415
}
1516
1617
let {
@@ -21,10 +22,38 @@
2122
isCompleted = false,
2223
disabled = false,
2324
comingSoon = false,
24-
onReset
25+
onReset,
26+
onComplete
2527
}: Props = $props()
2628
2729
let isHovered = $state(false)
30+
31+
// Determine which action button to show
32+
const actionButton = $derived(() => {
33+
if (isCompleted && isHovered && onReset) {
34+
return {
35+
icon: RefreshCw,
36+
label: 'Reset',
37+
onClick: onReset
38+
}
39+
}
40+
if (!isCompleted && isHovered && onComplete) {
41+
return {
42+
icon: CheckCheck,
43+
label: 'Mark as completed',
44+
onClick: onComplete
45+
}
46+
}
47+
return null
48+
})
49+
50+
function handleAction(e: MouseEvent | KeyboardEvent) {
51+
const button = actionButton()
52+
if (!button) return
53+
e.stopPropagation()
54+
e.preventDefault()
55+
button.onClick()
56+
}
2857
</script>
2958

3059
<button
@@ -56,26 +85,22 @@
5685

5786
<!-- Status -->
5887
<div class="flex items-center gap-1.5 flex-shrink-0">
59-
{#if isCompleted && isHovered && onReset}
88+
{#if actionButton()}
89+
{@const button = actionButton()!}
90+
{@const ActionIcon = button.icon}
6091
<div
6192
role="button"
6293
tabindex="0"
63-
onclick={(e) => {
64-
e.stopPropagation()
65-
e.preventDefault()
66-
onReset()
67-
}}
94+
onclick={handleAction}
6895
onkeydown={(e) => {
6996
if (e.key === 'Enter' || e.key === ' ') {
70-
e.stopPropagation()
71-
e.preventDefault()
72-
onReset()
97+
handleAction(e)
7398
}
7499
}}
75100
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"
76101
>
77-
<RefreshCw size={14} class="flex-shrink-0" />
78-
Reset
102+
<ActionIcon size={14} class="flex-shrink-0" />
103+
{button.label}
79104
</div>
80105
{:else}
81106
<span
@@ -87,7 +112,7 @@
87112
</span>
88113
{#if isCompleted}
89114
<CheckCircle2 size={14} class="text-green-500 flex-shrink-0" />
90-
{:else if !isCompleted}
115+
{:else}
91116
<Circle size={14} class="text-blue-300 flex-shrink-0" />
92117
{/if}
93118
{/if}

frontend/src/lib/tutorialUtils.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -117,32 +117,54 @@ export async function resetTutorialsByIndexes(tutorialIndexes: number[]) {
117117
}
118118

119119
/**
120-
* Reset (mark as incomplete) a single tutorial by index
120+
* Update a single tutorial's completion status by index
121121
*/
122-
export async function resetTutorialByIndex(tutorialIndex: number) {
122+
async function updateTutorialStatusByIndex(tutorialIndex: number, completed: boolean) {
123123
const currentTodos = get(tutorialsToDo)
124+
const isInTodos = currentTodos.includes(tutorialIndex)
124125

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-
})
126+
// Only update if the status needs to change
127+
// isInTodos = true means NOT completed, isInTodos = false means completed
128+
// So if completed === !isInTodos, we're already in the desired state
129+
if (completed === !isInTodos) {
130+
return // Already in the desired state
145131
}
132+
133+
// Update todos list
134+
const aft = completed
135+
? currentTodos.filter((x) => x !== tutorialIndex)
136+
: [...currentTodos, tutorialIndex]
137+
tutorialsToDo.set(aft)
138+
skippedAll.set(false)
139+
140+
// Get current progress bits
141+
const currentResponse = await UserService.getTutorialProgress()
142+
let bits: number = currentResponse.progress ?? 0
143+
144+
// Update bit for this tutorial index
145+
const mask = 1 << tutorialIndex
146+
bits = completed ? bits | mask : bits & ~mask
147+
148+
await UserService.updateTutorialProgress({
149+
requestBody: {
150+
progress: bits,
151+
skipped_all: false
152+
}
153+
})
154+
}
155+
156+
/**
157+
* Reset (mark as incomplete) a single tutorial by index
158+
*/
159+
export async function resetTutorialByIndex(tutorialIndex: number) {
160+
await updateTutorialStatusByIndex(tutorialIndex, false)
161+
}
162+
163+
/**
164+
* Mark a single tutorial as completed by index
165+
*/
166+
export async function completeTutorialByIndex(tutorialIndex: number) {
167+
await updateTutorialStatusByIndex(tutorialIndex, true)
146168
}
147169

148170
export async function syncTutorialsTodos() {

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
skipAllTodos,
1717
skipTutorialsByIndexes,
1818
resetTutorialsByIndexes,
19-
resetTutorialByIndex
19+
resetTutorialByIndex,
20+
completeTutorialByIndex
2021
} from '$lib/tutorialUtils'
2122
import { Button } from '$lib/components/common'
2223
import { RefreshCw, CheckCheck, CheckCircle2, Circle } from 'lucide-svelte'
@@ -159,10 +160,33 @@
159160
// Reset a single tutorial
160161
async function resetSingleTutorial(tutorialId: string) {
161162
const tutorial = currentTabConfig.tutorials.find((t) => t.id === tutorialId)
162-
if (!tutorial || tutorial.index === undefined) return
163+
if (!tutorial || tutorial.index === undefined) {
164+
console.warn(`Tutorial not found or has no index: ${tutorialId}`)
165+
return
166+
}
163167
164-
await resetTutorialByIndex(tutorial.index)
165-
await syncTutorialsTodos()
168+
try {
169+
await resetTutorialByIndex(tutorial.index)
170+
await syncTutorialsTodos()
171+
} catch (error) {
172+
console.error('Error resetting tutorial:', error)
173+
}
174+
}
175+
176+
// Mark a single tutorial as completed
177+
async function completeSingleTutorial(tutorialId: string) {
178+
const tutorial = currentTabConfig.tutorials.find((t) => t.id === tutorialId)
179+
if (!tutorial || tutorial.index === undefined) {
180+
console.warn(`Tutorial not found or has no index: ${tutorialId}`)
181+
return
182+
}
183+
184+
try {
185+
await completeTutorialByIndex(tutorial.index)
186+
await syncTutorialsTodos()
187+
} catch (error) {
188+
console.error('Error completing tutorial:', error)
189+
}
166190
}
167191
168192
// Calculate progress for each tab
@@ -308,6 +332,7 @@
308332
disabled={tutorial.active === false}
309333
comingSoon={tutorial.comingSoon}
310334
onReset={() => resetSingleTutorial(tutorial.id)}
335+
onComplete={() => completeSingleTutorial(tutorial.id)}
311336
/>
312337
{/each}
313338
</div>

0 commit comments

Comments
 (0)