Skip to content

Commit 6531e51

Browse files
committed
change start challenge to browse challenge for completed challenges
1 parent d58901d commit 6531e51

4 files changed

Lines changed: 76 additions & 5 deletions

File tree

src/api/challenge/single.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,25 @@ describe('challengeSingle.getRandomTask', () => {
381381
})
382382
})
383383

384+
describe('challengeSingle.getFirstTask', () => {
385+
it('fetches the first task in the challenge and caches it', async () => {
386+
const tasks = [{ id: 33 }] as unknown as Task[]
387+
const fetchMock = stubFetch(new Response(JSON.stringify(tasks), { status: 200 }))
388+
const queryClient = createTestQueryClient()
389+
390+
const result = await challengeSingle.getFirstTask(9, queryClient)
391+
392+
expect(result).toEqual(tasks)
393+
expect(queryClient.getQueryData(['task', 33])).toEqual(tasks[0])
394+
395+
const [request] = fetchMock.mock.calls[0]
396+
expect(request.url).toContain('api/v2/challenge/9/tasks')
397+
const params = new URL(request.url).searchParams
398+
expect(params.get('limit')).toBe('1')
399+
expect(params.get('page')).toBe('0')
400+
})
401+
})
402+
384403
describe('challengeSingle.fetchTasksNearby', () => {
385404
it('fetches nearby tasks using the default limit', async () => {
386405
const tasks = [{ id: 5 }] as unknown as Task[]

src/api/challenge/single.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ export const challengeSingle = {
211211
return tasks
212212
},
213213

214+
// Any task in the challenge, regardless of status - used to drop into read-only
215+
// browsing when the challenge has no startable tasks left.
216+
getFirstTask: async (challengeId: number, queryClient: QueryClient) => {
217+
const tasks = await apiRequest
218+
.get(`api/v2/challenge/${challengeId}/tasks`, { searchParams: { limit: 1, page: 0 } })
219+
.json<Task[]>()
220+
for (const task of tasks) {
221+
queryClient.setQueryData(['task', task.id], task)
222+
}
223+
return tasks
224+
},
225+
214226
fetchTasksNearby: async (challengeId: number, taskId: number, limit = 5) => {
215227
const tasks = await apiRequest
216228
.get(`api/v2/challenge/${challengeId}/tasksNearby/${taskId}`, {

src/components/Pages/BrowsedChallengePage/ChallengePanel/ChallengeFooter.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useQueryClient } from '@tanstack/react-query'
2-
import { Flag, Map as MapIcon, Play } from 'lucide-react'
2+
import { Eye, Flag, Map as MapIcon, Play } from 'lucide-react'
33
import { useState } from 'react'
44
import { toast } from 'sonner'
55
import { api } from '@/api'
66
import { useBrowsedChallengeContext } from '@/components/Pages/BrowsedChallengePage/contexts/BrowsedChallengeContext'
77
import { ChallengePausedNotice } from '@/components/shared/ChallengePausedNotice'
88
import { Button } from '@/components/ui/Button'
9+
import { useChallengeProgress } from '@/hooks/useChallengeProgress'
910
import { useNavigateToTask } from '@/hooks/useNavigateToTask'
1011
import { useIntl } from '@/i18n'
1112
import { logger } from '@/lib/logger'
@@ -19,8 +20,18 @@ export const ChallengeFooter = () => {
1920
const { showMap, setShowMap } = useMapToggle()
2021
const { t } = useIntl()
2122

23+
const { hasActions, tasksRemaining } = useChallengeProgress(
24+
challenge.id ?? 0,
25+
challenge.completionMetrics
26+
)
27+
2228
const [isLoadingTask, setIsLoadingTask] = useState(false)
2329

30+
// Nothing left to work on (every task is completed, or the challenge has no
31+
// tasks at all), so offer read-only browsing instead of a start that can only
32+
// fail with "no tasks available".
33+
const isBrowseOnly = hasActions && tasksRemaining === 0
34+
2435
const handleStartTask = async () => {
2536
if (!challenge.id) return
2637

@@ -49,6 +60,31 @@ export const ChallengeFooter = () => {
4960
}
5061
}
5162

63+
// Opens a task without claiming it, so completed challenges can still be read through.
64+
const handleBrowseTask = async () => {
65+
if (!challenge.id) return
66+
67+
try {
68+
setIsLoadingTask(true)
69+
const tasks = await api.challenge.getFirstTask(challenge.id, queryClient)
70+
71+
if (tasks && tasks.length > 0) {
72+
await navigateToTask(tasks[0].id, { claim: false })
73+
} else {
74+
toast.error(
75+
t('browsedChallengePage.footer.noTasksToBrowse', undefined, 'This challenge has no tasks')
76+
)
77+
}
78+
} catch (error) {
79+
logger.error('Error browsing challenge', { error })
80+
toast.error(
81+
t('browsedChallengePage.footer.failedToLoadTask', undefined, 'Failed to load task')
82+
)
83+
} finally {
84+
setIsLoadingTask(false)
85+
}
86+
}
87+
5288
return (
5389
<div className="shrink-0 rounded-b-xl border-zinc-200/50 border-t bg-white px-6 py-6 dark:border-slate-700/50 dark:bg-slate-800">
5490
<ChallengeProgress />
@@ -79,7 +115,7 @@ export const ChallengeFooter = () => {
79115
)}
80116

81117
<div className="mt-4 flex flex-col gap-4">
82-
{challenge.paused ? (
118+
{challenge.paused && !isBrowseOnly ? (
83119
<ChallengePausedNotice
84120
message={t(
85121
'browsedChallengePage.footer.challengePausedMessage',
@@ -91,13 +127,15 @@ export const ChallengeFooter = () => {
91127
<Button
92128
size="lg"
93129
className="w-full gap-2 rounded-full bg-teal-600 text-white shadow-md transition-all hover:bg-teal-700 hover:shadow-md"
94-
onClick={handleStartTask}
130+
onClick={isBrowseOnly ? handleBrowseTask : handleStartTask}
95131
disabled={isLoadingTask}
96132
>
97-
<Play className="size-5" />
133+
{isBrowseOnly ? <Eye className="size-5" /> : <Play className="size-5" />}
98134
{isLoadingTask
99135
? t('common.loading2', undefined, 'Loading...')
100-
: t('browsedChallengePage.footer.startChallenge', undefined, 'Start Challenge')}
136+
: isBrowseOnly
137+
? t('browsedChallengePage.footer.browseChallenge', undefined, 'Browse Challenge')
138+
: t('browsedChallengePage.footer.startChallenge', undefined, 'Start Challenge')}
101139
</Button>
102140
)}
103141
</div>

src/i18n/messages/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,12 @@
107107
"browsedChallengePage.challengeModals.reportModal.submitError": "Failed to submit report. Please try again.",
108108
"browsedChallengePage.challengeModals.reportModal.submitSuccess": "Report submitted successfully",
109109
"browsedChallengePage.challengeModals.reportModal.textPlaceholder": "Enter text here",
110+
"browsedChallengePage.footer.browseChallenge": "Browse Challenge",
110111
"browsedChallengePage.footer.challengePausedMessage": "This challenge is currently paused. New tasks cannot be started until it is resumed.",
111112
"browsedChallengePage.footer.failedToLoadTask": "Failed to load task",
112113
"browsedChallengePage.footer.hideMap": "Hide Map",
113114
"browsedChallengePage.footer.noTasksAvailable": "No tasks available for this challenge",
115+
"browsedChallengePage.footer.noTasksToBrowse": "This challenge has no tasks",
114116
"browsedChallengePage.footer.reportedIssueMessage": "This challenge has been reported. Click here to view the issue.",
115117
"browsedChallengePage.footer.showMap": "Show Map",
116118
"browsedChallengePage.footer.startChallenge": "Start Challenge",

0 commit comments

Comments
 (0)