Skip to content

Commit b4f7e3f

Browse files
authored
Merge pull request #2957 from codecrafters-io/arpan/cc-1755-using-githubcom-to-view-full-code-for-any-code-example
Using github.com to view full code for any code example (Frontend PR)
2 parents cd2e164 + e4095f3 commit b4f7e3f

File tree

15 files changed

+311
-67
lines changed

15 files changed

+311
-67
lines changed

app/components/course-page/course-stage-step/community-solution-card/changed-file-card.hbs

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,11 @@
77
class="bg-gray-100 dark:bg-gray-800 rounded-t py-2 px-4 border-b border-gray-200 dark:border-white/5 shadow-xs flex items-center justify-between sticky top-10 z-10"
88
>
99
<span class="font-mono text-xs text-gray-600 dark:text-gray-300 bold">{{@changedFile.filename}}</span>
10-
<div>
11-
{{#if @solution.isPublishedToPublicGithubRepository}}
12-
<a
13-
{{! @glint-expect-error call not ts-ified yet }}
14-
href={{call (fn @solution.githubUrlForFile @changedFile.filename)}}
15-
target="_blank"
16-
rel="noopener noreferrer"
17-
class="flex gap-x-1 items-center group"
18-
>
19-
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
20-
View on GitHub
21-
</span>
22-
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
23-
</a>
24-
{{else if this.shouldShowPublishToGithubButton}}
25-
<button type="button" {{on "click" @onPublishToGithubButtonClick}} class="flex gap-x-1 items-center group" data-test-publish-to-github-button>
26-
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
27-
Publish to GitHub
28-
</span>
29-
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
30-
</button>
31-
{{/if}}
32-
</div>
10+
<CoursePage::CourseStageStep::CommunitySolutionCard::GithubFileActions
11+
@filename={{@changedFile.filename}}
12+
@onPublishToGithubButtonClick={{@onPublishToGithubButtonClick}}
13+
@solution={{@solution}}
14+
/>
3315
</div>
3416

3517
<SyntaxHighlightedDiff

app/components/course-page/course-stage-step/community-solution-card/changed-file-card.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import Component from '@glimmer/component';
2-
import { inject as service } from '@ember/service';
3-
import type AuthenticatorService from 'codecrafters-frontend/services/authenticator';
42
import type CommunityCourseStageSolution from 'codecrafters-frontend/models/community-course-stage-solution';
53

64
interface Signature {
@@ -13,13 +11,7 @@ interface Signature {
1311
};
1412
}
1513

16-
export default class ChangedFileCard extends Component<Signature> {
17-
@service declare authenticator: AuthenticatorService;
18-
19-
get shouldShowPublishToGithubButton(): boolean {
20-
return this.args.solution.user.id === this.authenticator.currentUser?.id && !this.args.solution.isPublishedToPublicGithubRepository;
21-
}
22-
}
14+
export default class ChangedFileCard extends Component<Signature> {}
2315

2416
declare module '@glint/environment-ember-loose/registry' {
2517
export default interface Registry {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<div>
2+
{{#if @solution.isPublishedToPublicGithubRepository}}
3+
<a
4+
{{! @glint-expect-error call not ts-ified yet }}
5+
href={{call (fn @solution.githubUrlForFile @filename)}}
6+
target="_blank"
7+
rel="noopener noreferrer"
8+
class="flex gap-x-1 items-center group"
9+
>
10+
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
11+
View on GitHub
12+
</span>
13+
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
14+
</a>
15+
{{else if this.shouldShowPublishToGithubButton}}
16+
<button type="button" {{on "click" @onPublishToGithubButtonClick}} class="flex gap-x-1 items-center group" data-test-publish-to-github-button>
17+
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
18+
Publish to GitHub
19+
</span>
20+
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
21+
</button>
22+
{{else}}
23+
<button
24+
type="button"
25+
{{on "click" this.handleViewOnGithubButtonClick}}
26+
class="flex gap-x-1 items-center group {{if this.isCreatingExport 'opacity-50 cursor-not-allowed'}}"
27+
data-test-view-on-github-button
28+
>
29+
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
30+
{{#if this.isCreatingExport}}
31+
Loading...
32+
{{else}}
33+
View on GitHub
34+
{{/if}}
35+
</span>
36+
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
37+
</button>
38+
{{/if}}
39+
</div>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import Component from '@glimmer/component';
2+
import { action } from '@ember/object';
3+
import { inject as service } from '@ember/service';
4+
import { tracked } from '@glimmer/tracking';
5+
import * as Sentry from '@sentry/ember';
6+
import window from 'ember-window-mock';
7+
import type AuthenticatorService from 'codecrafters-frontend/services/authenticator';
8+
import type CommunityCourseStageSolution from 'codecrafters-frontend/models/community-course-stage-solution';
9+
import type CommunitySolutionExportModel from 'codecrafters-frontend/models/community-solution-export';
10+
import type Store from '@ember-data/store';
11+
12+
interface Signature {
13+
Element: HTMLDivElement;
14+
15+
Args: {
16+
filename: string;
17+
onPublishToGithubButtonClick: () => void;
18+
solution: CommunityCourseStageSolution;
19+
};
20+
}
21+
22+
export default class GithubFileActionsComponent extends Component<Signature> {
23+
@service declare authenticator: AuthenticatorService;
24+
@service declare store: Store;
25+
26+
@tracked isCreatingExport = false;
27+
28+
get shouldShowPublishToGithubButton(): boolean {
29+
return this.args.solution.user.id === this.authenticator.currentUser?.id && !this.args.solution.isPublishedToPublicGithubRepository;
30+
}
31+
32+
private async createExport(): Promise<CommunitySolutionExportModel | null> {
33+
if (this.isCreatingExport) return null;
34+
35+
this.isCreatingExport = true;
36+
37+
try {
38+
const exportRecord = await this.args.solution.createExport();
39+
this.isCreatingExport = false;
40+
41+
return exportRecord;
42+
} catch (error) {
43+
Sentry.captureException(error);
44+
this.isCreatingExport = false;
45+
46+
return null;
47+
}
48+
}
49+
50+
private getLatestProvisionedExport(): CommunitySolutionExportModel | null {
51+
return this.args.solution.exports.rejectBy('isExpired').filterBy('isProvisioned').sortBy('expiresAt').lastObject || null;
52+
}
53+
54+
@action
55+
async handleViewOnGithubButtonClick() {
56+
const latestExport = this.getLatestProvisionedExport();
57+
58+
if (latestExport) {
59+
const githubUrl = latestExport.githubUrlForFile(this.args.filename);
60+
latestExport.markAsAccessed({});
61+
window.open(githubUrl, '_blank', 'noopener,noreferrer');
62+
} else {
63+
const exportRecord = await this.createExport();
64+
65+
if (exportRecord) {
66+
const githubUrl = exportRecord.githubUrlForFile(this.args.filename);
67+
window.open(githubUrl, '_blank', 'noopener,noreferrer');
68+
}
69+
}
70+
}
71+
}
72+
73+
declare module '@glint/environment-ember-loose/registry' {
74+
export default interface Registry {
75+
'CoursePage::CourseStageStep::CommunitySolutionCard::GithubFileActions': typeof GithubFileActionsComponent;
76+
}
77+
}

app/components/course-page/course-stage-step/community-solution-card/highlighted-file-card.hbs

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,11 @@
77
class="bg-gray-100 dark:bg-gray-800 rounded-t py-2 px-4 border-b border-gray-200 dark:border-white/5 shadow-xs flex items-center justify-between sticky top-10 z-10"
88
>
99
<span class="font-mono text-xs text-gray-600 dark:text-gray-300 bold">{{@highlightedFile.filename}}</span>
10-
<div>
11-
{{#if @solution.isPublishedToPublicGithubRepository}}
12-
<a
13-
{{! @glint-expect-error call not ts-ified yet }}
14-
href={{call (fn @solution.githubUrlForFile @highlightedFile.filename)}}
15-
target="_blank"
16-
rel="noopener noreferrer"
17-
class="flex gap-x-1 items-center group"
18-
>
19-
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
20-
View on GitHub
21-
</span>
22-
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
23-
</a>
24-
{{else if this.shouldShowPublishToGithubButton}}
25-
<button type="button" {{on "click" @onPublishToGithubButtonClick}} class="flex gap-x-1 items-center group" data-test-publish-to-github-button>
26-
<span class="text-[10px] text-gray-700 dark:text-gray-300 group-hover:underline">
27-
Publish to GitHub
28-
</span>
29-
{{svg-jar "github" class="w-3.5 h-3.5 text-gray-600 dark:text-gray-400"}}
30-
</button>
31-
{{/if}}
32-
</div>
10+
<CoursePage::CourseStageStep::CommunitySolutionCard::GithubFileActions
11+
@filename={{@highlightedFile.filename}}
12+
@onPublishToGithubButtonClick={{@onPublishToGithubButtonClick}}
13+
@solution={{@solution}}
14+
/>
3315
</div>
3416

3517
<CodeMirror

app/components/course-page/course-stage-step/community-solution-card/highlighted-file-card.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Component from '@glimmer/component';
22
import { inject as service } from '@ember/service';
3-
import type AuthenticatorService from 'codecrafters-frontend/services/authenticator';
43
import type CommunityCourseStageSolution from 'codecrafters-frontend/models/community-course-stage-solution';
54
import type DarkModeService from 'codecrafters-frontend/services/dark-mode';
65
import { codeCraftersDark, codeCraftersLight } from 'codecrafters-frontend/utils/code-mirror-themes';
@@ -17,7 +16,6 @@ interface Signature {
1716
}
1817

1918
export default class HighlightedFileCard extends Component<Signature> {
20-
@service declare authenticator: AuthenticatorService;
2119
@service declare darkMode: DarkModeService;
2220

2321
get codeMirrorTheme() {
@@ -68,10 +66,6 @@ export default class HighlightedFileCard extends Component<Signature> {
6866
}));
6967
}
7068

71-
get shouldShowPublishToGithubButton(): boolean {
72-
return this.args.solution.user.id === this.authenticator.currentUser?.id && !this.args.solution.isPublishedToPublicGithubRepository;
73-
}
74-
7569
get visibleRangesForCodeMirror(): LineRange[] {
7670
// Add 3 lines above and below the highlighted range
7771
const ranges = this.args.highlightedFile.highlighted_ranges.map((range) => ({

app/controllers/course/stage/code-examples.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default class CodeExamplesController extends Controller {
102102
this.solutions = (await this.store.query('community-course-stage-solution', {
103103
course_stage_id: this.courseStage.id,
104104
language_id: this.currentLanguage.id,
105-
include: 'user,language,course-stage,screencasts,current-user-upvotes,current-user-downvotes',
105+
include: 'user,language,course-stage,screencasts,current-user-upvotes,current-user-downvotes,exports',
106106
order: this.order,
107107
})) as unknown as CommunityCourseStageSolutionModel[]; // TODO: Doesn't store.query support model type inference?
108108

app/models/community-course-stage-solution.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type CourseStageScreencastModel from './course-stage-screencast';
99
import type LanguageModel from './language';
1010
import type TrustedCommunitySolutionEvaluationModel from './trusted-community-solution-evaluation';
1111
import type UserModel from './user';
12+
import type CommunitySolutionExportModel from './community-solution-export';
1213
import { action } from '@ember/object';
1314
import { inject as service } from '@ember/service';
1415
import { memberAction } from 'ember-api-actions';
@@ -29,6 +30,7 @@ export default class CommunityCourseStageSolutionModel extends Model.extend(View
2930
declare trustedEvaluations: TrustedCommunitySolutionEvaluationModel[];
3031

3132
@hasMany('community-course-stage-solution-comment', { async: false, inverse: 'target' }) declare comments: CourseStageCommentModel[];
33+
@hasMany('community-solution-export', { async: false, inverse: 'communitySolution' }) declare exports: CommunitySolutionExportModel[];
3234
@hasMany('course-stage-screencast', { async: false, inverse: null }) declare screencasts: CourseStageScreencastModel[];
3335

3436
// @ts-expect-error empty '' not supported
@@ -101,6 +103,7 @@ export default class CommunityCourseStageSolutionModel extends Model.extend(View
101103
return `https://github.com/${this.githubRepositoryName}/blob/${this.commitSha}/${filename}`;
102104
}
103105

106+
declare createExport: (this: Model) => Promise<CommunitySolutionExportModel>;
104107
declare fetchFileComparisons: (this: Model, payload: unknown) => Promise<FileComparison[]>;
105108
declare unvote: (this: Model, payload: unknown) => Promise<void>;
106109
}
@@ -135,3 +138,11 @@ CommunityCourseStageSolutionModel.prototype.unvote = memberAction({
135138
}
136139
},
137140
});
141+
142+
CommunityCourseStageSolutionModel.prototype.createExport = function () {
143+
const record = this.store.createRecord('community-solution-export', {
144+
communitySolution: this,
145+
});
146+
147+
return record.save();
148+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Model, { attr, belongsTo } from '@ember-data/model';
2+
import { memberAction } from 'ember-api-actions';
3+
import type CommunityCourseStageSolutionModel from './community-course-stage-solution';
4+
5+
export default class CommunitySolutionExportModel extends Model {
6+
@belongsTo('community-course-stage-solution', { async: false, inverse: 'exports' })
7+
declare communitySolution: CommunityCourseStageSolutionModel;
8+
9+
@attr('date') declare expiresAt: Date;
10+
@attr('string') declare githubRepositoryUrl: string | null;
11+
@attr('string') declare status: 'provisioning' | 'provisioned';
12+
13+
get isExpired(): boolean {
14+
return new Date() >= this.expiresAt;
15+
}
16+
17+
get isProvisioned(): boolean {
18+
return this.status === 'provisioned';
19+
}
20+
21+
githubUrlForFile(filename: string): string {
22+
return `${this.githubRepositoryUrl}/blob/main/${filename}`;
23+
}
24+
25+
declare markAsAccessed: (this: Model, payload: unknown) => Promise<void>;
26+
}
27+
28+
CommunitySolutionExportModel.prototype.markAsAccessed = memberAction({
29+
path: 'mark-as-accessed',
30+
type: 'post',
31+
});

mirage/config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import communityCourseStageSolutionComments from './handlers/community-course-st
1010
import communityCourseStageSolutions from './handlers/community-course-stage-solutions';
1111
import communitySolutionEvaluations from './handlers/community-solution-evaluations';
1212
import communitySolutionEvaluators from './handlers/community-solution-evaluators';
13+
import communitySolutionExports from './handlers/community-solution-exports';
1314
import communitySolutionsAnalyses from './handlers/community-solutions-analyses';
1415
import conceptEngagements from './handlers/concept-engagements';
1516
import conceptGroups from './handlers/concept-groups';
@@ -89,6 +90,7 @@ export default function (config) {
8990
currentUserDownvotes: hasMany('downvote', { inverse: 'downvotable' }),
9091
currentUserUpvotes: hasMany('upvote', { inverse: 'upvotable' }),
9192
evaluations: hasMany('community-solution-evaluation', { inverse: 'communitySolution' }),
93+
exports: hasMany('community-solution-export', { inverse: 'communitySolution' }),
9294
language: belongsTo('language', { inverse: null }),
9395
screencasts: hasMany('course-stage-screencast', { inverse: 'solution' }),
9496
trustedEvaluations: hasMany('trusted-community-solution-evaluation', { inverse: 'communitySolution' }),
@@ -146,6 +148,7 @@ function routes() {
146148
communityCourseStageSolutions(this);
147149
communitySolutionEvaluations(this);
148150
communitySolutionEvaluators(this);
151+
communitySolutionExports(this);
149152
communitySolutionsAnalyses(this);
150153
conceptEngagements(this);
151154
conceptGroups(this);

0 commit comments

Comments
 (0)