Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/components/course-page/current-step-complete-modal.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ModalBody @allowManualClose={{false}} @onClose={{@onClose}} data-test-current-step-complete-modal class="max-w-xl" ...attributes>
<ModalBody @allowManualClose={{false}} @onClose={{@onClose}} data-test-current-step-complete-modal class="w-full max-w-xs" ...attributes>
<div class="flex flex-col items-center w-full">
<div class="text-[60px]">
🎉
Expand All @@ -13,7 +13,8 @@
{{#if this.currentStepAsCourseStageStep.repository.language}}
<CoursePage::CurrentStepCompleteModal::LanguageLeaderboardRankSection
@language={{this.currentStepAsCourseStageStep.repository.language}}
class="mt-3 mb-3"
@shouldShowCTA={{eq this.coursePageState.activeStep this.coursePageState.nextStep}}
class="mt-3 mb-3 w-full"
/>
{{/if}}
{{else}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
class="w-full p-5 pb-6"
role="button"
{{on "click" this.handleClick}}
...attributes
data-test-language-leaderboard-rank-section
...attributes
>
<div class="flex flex-col items-center w-full">
{{svg-jar "presentation-chart-line" class="w-8 h-8 fill-current text-teal-500/90 mb-2"}}

<span class="text-teal-700 dark:text-teal-400 text-xs mb-0.5 text-center">
<span class="text-teal-700 dark:text-teal-400 text-xs mb-2 text-center">
Your
{{@language.name}}
leaderboard rank is
</span>

<AnimatedContainer>
<div class="flex items-center w-full justify-center" data-test-rank>
{{#animated-if this.refreshRankTask.isRunning use=this.transition duration=300}}
{{#animated-if this.isLoadingRank use=this.transition duration=300}}
<b
class="text-2xl text-teal-600 dark:text-teal-400 border-b border-teal-600 dark:border-teal-400 group-hover:text-teal-500 border-dashed animate-pulse"
>
Expand All @@ -35,5 +35,17 @@
{{/animated-if}}
</div>
</AnimatedContainer>

{{#if @shouldShowCTA}}
<AnimatedContainer class="w-full mt-4">
<div class="flex items-center justify-center">
{{#animated-value this.ctaText use=this.transition duration=500 as |ctaText|}}
<div class="text-xs text-teal-600 dark:text-teal-400">
{{ctaText}}
</div>
{{/animated-value}}
</div>
</AnimatedContainer>
{{/if}}
</div>
</Alert>
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import type Owner from '@ember/owner';
import type RouterService from '@ember/routing/router-service';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { task, timeout } from 'ember-concurrency';
import { tracked } from '@glimmer/tracking';
import type CourseStageModel from 'codecrafters-frontend/models/course-stage';
import type RepositoryModel from 'codecrafters-frontend/models/repository';
import computeLeaderboardCTA from 'codecrafters-frontend/utils/compute-leaderboard-cta';

interface Signature {
Element: HTMLDivElement;

Args: {
currentCourseStage: CourseStageModel;
language: LanguageModel;
repository: RepositoryModel;
shouldShowCTA: boolean;
};
}

Expand All @@ -23,12 +30,28 @@ export default class LanguageLeaderboardRankSection extends Component<Signature>
@service declare leaderboardEntriesCacheRegistry: LeaderboardEntriesCacheRegistryService;
@service declare router: RouterService;

@tracked isLoadingRank: boolean = true;
@tracked ctaText: string = 'Loading...';

constructor(owner: Owner, args: Signature['Args']) {
super(owner, args);

this.refreshRankTask.perform();
}

get computedCtaText(): string | null {
if (!this.leaderboardEntriesCache.userEntry || !this.leaderboardEntriesCache.userRankCalculation) {
return null;
}

return computeLeaderboardCTA(
this.leaderboardEntriesCache.userEntry,
this.leaderboardEntriesCache.userRankCalculation,
this.args.repository,
this.args.currentCourseStage,
);
}

get leaderboardEntriesCache(): LeaderboardEntriesCache {
return this.leaderboardEntriesCacheRegistry.getOrCreate(this.args.language.leaderboard!);
}
Expand All @@ -43,7 +66,22 @@ export default class LanguageLeaderboardRankSection extends Component<Signature>
}

refreshRankTask = task({ restartable: true }, async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await this.leaderboardEntriesCacheRegistry.getOrCreate(this.args.language.leaderboard!).loadOrRefresh();

this.isLoadingRank = false;
this.ctaText = ' '; // No break space

await timeout(500);
this.ctaText = 'Nice work!';

if (this.computedCtaText) {
await timeout(2500);
this.ctaText = ' '; // No break space

await timeout(500);
this.ctaText = this.computedCtaText;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition between CTA check and assignment

The computedCtaText getter is called twice in refreshRankTask - once to check if it's truthy, then again 3000ms later for assignment. If the underlying leaderboard data changes during this window, the getter could return null on the second call even though the condition passed. This would assign null to ctaText (which is typed as string), causing the CTA to unexpectedly disappear. The value returned from the first check could be captured and reused instead.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading indicator stuck forever if data fetch fails

The original code used refreshRankTask.isRunning for the loading state, which automatically becomes false when the task ends regardless of success or failure. The new code uses a manual isLoadingRank tracked property that's only set to false after loadOrRefresh() succeeds. If the fetch throws an error, isLoadingRank remains true and the loading animation displays indefinitely with no way to recover.

Fix in Cursor Fix in Web

});
}

Expand Down
Loading