Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a (optional) newly redesigned ScoreBoard #2043

Merged
merged 98 commits into from Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
4017f32
WIP redesigned score-board
J12934 May 29, 2023
4ce4f87
Cleanup, split up ChallengeCard and split into own module
J12934 May 29, 2023
d3dab8a
Start extracting generic score-card component
J12934 May 29, 2023
6c15434
Move coding challenge score to generic component
J12934 May 29, 2023
4527b7a
Fix hardcoded description
J12934 May 29, 2023
4b26f76
Fix missing closing bracket
J12934 May 29, 2023
2923d3c
Migrate DifficultyOverview to generic component
J12934 May 29, 2023
0b1e0f9
Fix outdated width restriction
J12934 May 29, 2023
ee8ed42
Properly calculate difficulty summaries
J12934 May 29, 2023
a7736ff
lint:fix
J12934 May 29, 2023
f5a542e
Mock out challenge
J12934 May 29, 2023
5cc56f9
lint:fix
J12934 May 29, 2023
0204d51
Fix frontend tests
J12934 May 30, 2023
669cfda
Auto-fix linting issues
JuiceShopBot May 30, 2023
cae1fc6
Add missing mitigationUrl to challenge
J12934 May 30, 2023
049e948
WIP category filtering
J12934 Jun 3, 2023
d36a183
Prevent tags/pills from jumping when the proper border is displayed
J12934 Jun 3, 2023
c99c152
Extract filtering settings into their own components
J12934 Jun 3, 2023
39a33c2
Try to make use colors from themes dynamically
J12934 Jun 3, 2023
fceccb2
More theming
J12934 Jun 5, 2023
1c72141
Remove unneeded scss file
J12934 Jun 5, 2023
c74ba1d
Remove frontend lodash requirement
J12934 Jun 5, 2023
a3f5f87
Remove currently unused activated route
J12934 Jun 5, 2023
61e1e46
Fix some frontend testing issues
J12934 Jun 5, 2023
5d5df3e
lint:fix
J12934 Jun 5, 2023
88b6a4a
Add frontend test for category filter component
J12934 Jun 6, 2023
99d7cb9
Remove test for difficulty summary for the moment
J12934 Jun 6, 2023
1a87194
Update border colors to match newest design iteration
J12934 Jun 10, 2023
ddf6212
Add cors for socket.io local dev
J12934 Jun 12, 2023
7feb77b
Add filter settings for difficulty, status and tags
J12934 Jun 12, 2023
a198110
Extract challenge filtering into it's own more easily testable function
J12934 Jun 12, 2023
cadbd92
Fix incorrect access level
J12934 Jun 12, 2023
09aaa8a
Add search
J12934 Jun 13, 2023
983debc
Improve filter handling and display
J12934 Jun 16, 2023
d9b9b91
Fix typescript for rsn:verbose
J12934 Jun 16, 2023
af817a3
Hide score-board preview component from vuln code snippets
J12934 Jun 16, 2023
e47c470
Add partially solved state
J12934 Jun 17, 2023
ff2215e
Fill stars based on current progress
J12934 Jun 17, 2023
22f08a2
Update score-board when challenge solved websocket is recieved
J12934 Jun 17, 2023
ae7d64e
Add tests back. Seem to work properly with structuredClone
J12934 Jun 17, 2023
c14038a
lint:fix
J12934 Jun 17, 2023
f194e0f
Put challenge difficulty number into the star
J12934 Jul 1, 2023
38adbb3
fix linter
J12934 Jul 1, 2023
5454bec
Add progress bar
J12934 Jul 1, 2023
9268c05
Add comment to somewhat confusing svg definition
J12934 Jul 1, 2023
5f57f95
Add search icon to input
J12934 Jul 1, 2023
5868977
Adjust todo comment to reflect whats actually wrong
J12934 Jul 1, 2023
b2ea248
fix linter
J12934 Jul 1, 2023
06ad22d
Add tooltips
J12934 Jul 1, 2023
f99631e
WIP hacking instructor integration
J12934 Jul 2, 2023
6955808
Update challenge card designs
J12934 Jul 2, 2023
d598882
Tutorial is now included in the bundle by default
J12934 Jul 2, 2023
0c382af
Make it clearer that badges are clickable
J12934 Jul 3, 2023
3754f20
Make coding challenges functional on the new score-board
J12934 Jul 3, 2023
39d0d7d
lint fixes
J12934 Jul 3, 2023
6a6f647
properly import dialog module
J12934 Jul 3, 2023
213c055
Properly set return type of repeat notification service
J12934 Jul 3, 2023
02dbe0b
Implement repeat notification
J12934 Jul 3, 2023
deaefc5
Remove commented out badges
J12934 Jul 3, 2023
2021e96
Add disabledEnv note
J12934 Jul 3, 2023
a60f6cc
Change media queries to more quickly display challenges in 3 instead …
J12934 Jul 5, 2023
4b4a6e3
Change return type to be better handled by the test mocking functions
J12934 Jul 5, 2023
8293718
Ensure translation module is present in tests
J12934 Jul 5, 2023
181b99f
Add translation to new score-board
J12934 Jul 5, 2023
9d5b0bd
Write & Read filter settings to url
J12934 Jul 15, 2023
772ebb9
Translate no challenges found message
J12934 Jul 16, 2023
4b7563d
Hide challenges if they are disabled in the current environment
J12934 Jul 16, 2023
9bcf050
Show hints
J12934 Jul 16, 2023
efaa6d9
Adjust padding for additional left padding introduced by the icon
J12934 Jul 19, 2023
1843249
Style cookie consent banner using css variables
J12934 Jul 19, 2023
dd62778
Correctly hide "show/hide button" when no challenges are disabled
J12934 Jul 19, 2023
12b6e5e
Fix title of challenges solved card
J12934 Jul 19, 2023
93ce09d
Add tooltips for categories
J12934 Jul 19, 2023
dc48123
Allow new score-board to also solve the score-board challenge
J12934 Jul 19, 2023
746c4a6
Fix missing module import in test
J12934 Jul 23, 2023
342b0b1
Correct code snippet selector
J12934 Jul 23, 2023
ef660c2
Implement tutorial mode for new score board
J12934 Jul 29, 2023
8d8c89e
Don't show no challenges found warning when loading hasn't completed
J12934 Jul 29, 2023
f52191f
Add link to and from new score-board
J12934 Jul 29, 2023
6cbeece
Fix divide by zero error while challenges are still loading
J12934 Jul 29, 2023
b0ade57
fix linter
J12934 Jul 29, 2023
1dbe25b
Remove debugging log
J12934 Jul 29, 2023
0bae837
Make ScoreBoard preview ad & back buttons i18n ready
J12934 Jul 29, 2023
b9cd6c5
remove debug log
J12934 Jul 29, 2023
ef631be
Sort challenges by difficulty, tutorial order or name
J12934 Jul 29, 2023
2b71984
Fix unknown element warning for score board preview
J12934 Jul 29, 2023
db160a7
Refactor and make default score board configurable
J12934 Aug 20, 2023
805b40f
Type response of CodeSnippet Service
J12934 Aug 20, 2023
3f97226
Cleanup rxjs subscriptions and properly avoid in place update for cha…
J12934 Aug 20, 2023
18bb9e9
Add tests for score-board preview component
J12934 Aug 20, 2023
06dca5a
Auto-fix linting issues
JuiceShopBot Aug 20, 2023
a2c5c58
Convert selected difficulties into a better grouped display
J12934 Aug 20, 2023
9e3a1ff
Extract difficulty stars component
J12934 Aug 20, 2023
b4f217a
fix indentation level
J12934 Aug 20, 2023
354aecc
Improve spacing / grouping on the setting page
J12934 Aug 20, 2023
46999f8
Translate Additional ScoreBoard Setting page
J12934 Aug 20, 2023
7b592aa
Integrate file based backups into additional settings menu
J12934 Aug 20, 2023
0a5b61f
Change overflow to only scroll when overflowing
J12934 Aug 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 0 additions & 8 deletions config.schema.yml
Expand Up @@ -67,14 +67,6 @@ application:
message:
type: string
cookieConsent:
backgroundColor:
type: string
textColor:
type: string
buttonColor:
type: string
buttonTextColor:
type: string
message:
type: string
dismissText:
Expand Down
4 changes: 0 additions & 4 deletions config/7ms.yml
Expand Up @@ -28,10 +28,6 @@ application:
welcomeBanner:
showOnFirstStart: false
cookieConsent:
backgroundColor: '#0395d5'
textColor: '#ffffff'
buttonColor: '#b3b3b3'
buttonTextColor: '#000000'
message: 'If you stay on this website for more than 7 minutes our cookies will start tracking you.'
dismissText: 'I`ll be long gone by then!'
linkText: 'But I want to stay an arbitrary number of minutes!'
Expand Down
4 changes: 0 additions & 4 deletions config/addo.yml
Expand Up @@ -25,10 +25,6 @@ application:
welcomeBanner:
showOnFirstStart: false
cookieConsent:
backgroundColor: '#c2185b'
textColor: '#ffffff'
buttonColor: '#b0bec5'
buttonTextColor: '#000000'
message: 'Taste our 150 practitioner-baked cookies with 5 tracking flavors!'
dismissText: 'Register for 24/7 cookies!'
linkText: 'Yum, tell me more!'
Expand Down
4 changes: 0 additions & 4 deletions config/bodgeit.yml
Expand Up @@ -25,10 +25,6 @@ application:
welcomeBanner:
showOnFirstStart: false
cookieConsent:
backgroundColor: '#000000'
textColor: '#ffffff'
buttonColor: '#ffffff'
buttonTextColor: '#000000'
message: 'This website is so legacy, it might even run without cookies.'
dismissText: 'Bodge it!'
linkText: 'Lega-what?'
Expand Down
4 changes: 0 additions & 4 deletions config/default.yml
Expand Up @@ -36,10 +36,6 @@ application:
title: 'Welcome to OWASP Juice Shop!'
message: "<p>Being a web application with a vast number of intended security vulnerabilities, the <strong>OWASP Juice Shop</strong> is supposed to be the opposite of a best practice or template application for web developers: It is an awareness, training, demonstration and exercise tool for security risks in modern web applications. The <strong>OWASP Juice Shop</strong> is an open-source project hosted by the non-profit <a href='https://owasp.org' target='_blank'>Open Web Application Security Project (OWASP)</a> and is developed and maintained by volunteers. Check out the link below for more information and documentation on the project.</p><h1><a href='https://owasp-juice.shop' target='_blank'>https://owasp-juice.shop</a></h1>"
cookieConsent:
backgroundColor: '#546e7a'
textColor: '#ffffff'
buttonColor: '#558b2f'
buttonTextColor: '#ffffff'
message: 'This website uses fruit cookies to ensure you get the juiciest tracking experience.'
dismissText: 'Me want it!'
linkText: 'But me wait!'
Expand Down
4 changes: 0 additions & 4 deletions config/mozilla.yml
Expand Up @@ -26,10 +26,6 @@ application:
welcomeBanner:
showOnFirstStart: false
cookieConsent:
backgroundColor: '#e95420'
textColor: '#ffffff'
buttonColor: '#2778c5'
buttonTextColor: '#ffffff'
message: 'This website uses a myriad of 3rd-party cookies for your convenience and tracking pleasure.'
dismissText: 'Never mind!'
linkText: 'How can I turn this off?'
Expand Down
2 changes: 0 additions & 2 deletions config/oss.yml
Expand Up @@ -5,8 +5,6 @@ application:
showOnFirstStart: false
theme: blue-lightblue
cookieConsent:
backgroundColor: '#23527c'
textColor: '#ffffff'
message: 'We are not only using cookies but also recorded this session on YouTube!'
dismissText: "I've been there live, so thanks!"
linkText: 'I want to watch that!'
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Expand Up @@ -46,6 +46,7 @@
"flag-icons": "^6.9.2",
"font-mfizz": "^2.4.1",
"jwt-decode": "^2.2.0",
"lodash-es": "^4.17.21",
"material-icons": "^0.3.1",
"ng-mat-search-bar": "^12.0.1",
"ng-simple-slideshow": "^1.3.0-beta.11",
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/app/Models/challenge.model.ts
Expand Up @@ -7,16 +7,18 @@ import { SafeHtml } from '@angular/platform-browser'

export interface Challenge {
name: string
key: string
category: string
tags?: string
description?: string | SafeHtml
difficulty: number
difficulty: 1 | 2 | 3 | 4 | 5 | 6
hint?: string
hintUrl?: string
disabledEnv?: string
solved?: boolean
tutorialOrder?: number
hasTutorial?: boolean
hasSnippet?: boolean
codingChallengeStatus?: number
codingChallengeStatus?: 0 | 1 | 2
mitigationUrl?: string
}
6 changes: 4 additions & 2 deletions frontend/src/app/Services/challenge.service.ts
Expand Up @@ -6,7 +6,9 @@
import { environment } from '../../environments/environment'
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { catchError, map } from 'rxjs/operators'
import { Challenge } from '../Models/challenge.model'

@Injectable({
providedIn: 'root'
Expand All @@ -16,12 +18,12 @@ export class ChallengeService {
private readonly host = this.hostServer + '/api/Challenges'
constructor (private readonly http: HttpClient) { }

find (params?: any) {
find (params?: any): Observable<Challenge[]> {
return this.http.get(this.host + '/', { params: params }).pipe(map((response: any) => response.data), catchError((err) => { throw err }))
}

repeatNotification (challengeName: string) {
return this.http.get(this.hostServer + '/rest/repeat-notification', { params: { challenge: challengeName } }).pipe(catchError((err) => { throw err }))
return this.http.get(this.hostServer + '/rest/repeat-notification', { params: { challenge: challengeName }, responseType: 'text' as const }).pipe(catchError((err) => { throw err }))
}

continueCode () {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/Services/code-snippet.service.ts
Expand Up @@ -31,7 +31,7 @@ export class CodeSnippetService {
return this.http.get<CodeSnippet>(`${this.host}/${key}`).pipe(map((response: CodeSnippet) => response), catchError((err) => { throw err }))
}

challenges () {
challenges (): Observable<string[]> {
return this.http.get(`${this.host}`).pipe(map((response: any) => response.challenges), catchError((err) => { throw err }))
}
}
6 changes: 1 addition & 5 deletions frontend/src/app/Services/configuration.service.ts
Expand Up @@ -12,7 +12,7 @@ import { Observable } from 'rxjs'
interface ConfigResponse {
config: Config
}
interface Config {
export interface Config {
server: {
port: number
}
Expand Down Expand Up @@ -47,10 +47,6 @@ interface Config {
message: string
}
cookieConsent: {
backgroundColor: string
textColor: string
buttonColor: string
buttonTextColor: string
message: string
dismissText: string
linkText: string
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/app/Services/feature-flag.service.spec.ts
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors.
* SPDX-License-Identifier: MIT
*/

import { FeatureFlagService, SCORE_BOARD_FEATURE_FLAG_KEY } from './feature-flag.service'

describe('FeatureFlagService', () => {
beforeEach(() => {
localStorage.removeItem(SCORE_BOARD_FEATURE_FLAG_KEY)
})

describe('defaultScoreBoard', () => {
it('should default to v1', (done) => {
const service = new FeatureFlagService()
service.defaultScoreBoard$.subscribe((value) => {
expect(value).toBe('v1')
done()
})
})
it('should read value from localStorage', (done) => {
localStorage.setItem(SCORE_BOARD_FEATURE_FLAG_KEY, 'v2')
const service = new FeatureFlagService()
service.defaultScoreBoard$.subscribe((value) => {
expect(value).toBe('v2')
done()
})
})
it('should update observable when values is updated', (done) => {
const service = new FeatureFlagService()
service.setDefaultScoreBoard('v2')
service.defaultScoreBoard$.subscribe((value) => {
expect(value).toBe('v2')
done()
})
})
})
})
29 changes: 29 additions & 0 deletions frontend/src/app/Services/feature-flag.service.ts
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors.
* SPDX-License-Identifier: MIT
*/

import { Injectable } from '@angular/core'
import { BehaviorSubject, Subject } from 'rxjs'

export type ScoreBoardVersion = 'v1' | 'v2'

export const SCORE_BOARD_FEATURE_FLAG_KEY = 'score-board-version'

@Injectable({
providedIn: "root",
})
export class FeatureFlagService {
constructor () {
const scoreBoardVersion = localStorage.getItem(SCORE_BOARD_FEATURE_FLAG_KEY)
if (scoreBoardVersion) {
this.defaultScoreBoard$.next(scoreBoardVersion as ScoreBoardVersion)
}
}

public defaultScoreBoard$: Subject<ScoreBoardVersion> = new BehaviorSubject<ScoreBoardVersion>('v1')
public setDefaultScoreBoard (scoreBoardVersion: ScoreBoardVersion) {
this.defaultScoreBoard$.next(scoreBoardVersion)
localStorage.setItem(SCORE_BOARD_FEATURE_FLAG_KEY, scoreBoardVersion)
}
}
8 changes: 6 additions & 2 deletions frontend/src/app/app.module.ts
Expand Up @@ -72,6 +72,7 @@ import { ImageCaptchaService } from './Services/image-captcha.service'
import { KeysService } from './Services/keys.service'
import { AddressService } from './Services/address.service'
import { QuantityService } from './Services/quantity.service'
import { FeatureFlagService } from './Services/feature-flag.service'
import { FlexLayoutModule } from '@angular/flex-layout'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { MatToolbarModule } from '@angular/material/toolbar'
Expand Down Expand Up @@ -102,6 +103,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'
import { MatRadioModule } from '@angular/material/radio'
import { MatBadgeModule } from '@angular/material/badge'
import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs'
import { ScoreBoardPreviewModule } from './score-board-preview/score-board-preview.module'
import { TwoFactorAuthComponent } from './two-factor-auth/two-factor-auth.component'
import { DataExportComponent } from './data-export/data-export.component'
import { LastLoginIpComponent } from './last-login-ip/last-login-ip.component'
Expand Down Expand Up @@ -260,7 +262,8 @@ export function HttpLoaderFactory (http: HttpClient) {
MatSlideToggleModule,
MatChipsModule,
NgxTextDiffModule,
HighlightModule
HighlightModule,
ScoreBoardPreviewModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
Expand Down Expand Up @@ -311,7 +314,8 @@ export function HttpLoaderFactory (http: HttpClient) {
WalletService,
OrderHistoryService,
DeliveryService,
PhotoWallService
PhotoWallService,
FeatureFlagService
],
bootstrap: [AppComponent]
})
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/app/app.routing.ts
Expand Up @@ -42,6 +42,7 @@ import { PhotoWallComponent } from './photo-wall/photo-wall.component'
import { DeluxeUserComponent } from './deluxe-user/deluxe-user.component'
import { AccountingGuard, AdminGuard, LoginGuard } from './app.guard'
import { NFTUnlockComponent } from './nft-unlock/nft-unlock.component'
import { ScoreBoardPreviewComponent } from './score-board-preview/score-board-preview.component'

const loadFaucetModule = async () => {
const module = await import('./faucet/faucet.module')
Expand Down Expand Up @@ -164,6 +165,10 @@ const routes: Routes = [
path: 'score-board', // vuln-code-snippet vuln-line scoreBoardChallenge
component: ScoreBoardComponent // vuln-code-snippet neutral-line scoreBoardChallenge
}, // vuln-code-snippet neutral-line scoreBoardChallenge
{ // vuln-code-snippet hide-line
path: 'score-board-preview', // vuln-code-snippet hide-line
component: ScoreBoardPreviewComponent // vuln-code-snippet hide-line
}, // vuln-code-snippet hide-line
{
path: 'track-result',
component: TrackResultComponent
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/code-snippet/code-snippet.component.ts
Expand Up @@ -21,13 +21,13 @@ enum ResultState {
Wrong,
}

interface Solved {
export interface Solved {
findIt: boolean
fixIt: boolean
}

@Component({
selector: 'app-user-details',
selector: 'code-snippet',
templateUrl: './code-snippet.component.html',
styleUrls: ['./code-snippet.component.scss']
})
Expand Down
@@ -0,0 +1,95 @@
<span class="category-row">{{ challenge.category }}</span>

<div class="name-row">
<div class="dot"></div>
<span class="name">{{ challenge.name }}</span>
<difficulty-stars
[difficulty]="challenge.difficulty"
></difficulty-stars>
</div>
<div class="description-row" [innerHtml]="challenge.description"></div>
<div class="bottom-row">
<div class="tags">
<span *ngFor="let tag of challenge.tagList" class="tag">{{ tag }}</span>
</div>
<div class="badge-group">
<!-- info text if the challenge is unavailable -->
<button
class="badge"
*ngIf="challenge.disabledEnv !== null"
[matTooltip]="'CHALLENGE_UNAVAILABLE' | translate:{ env: challenge.disabledEnv }"
>
<mat-icon [style.color]="'var(--theme-warn)'">info_outline</mat-icon>
</button>
<!-- coding challenge badge -->
<button
class="badge"
*ngIf="challenge.hasCodingChallenge"
(click)="openCodingChallengeDialog(challenge.key)"
[disabled]="challenge.solved === false"
[ngClass]="{
'partially-completed': challenge.codingChallengeStatus === 1,
'completed': challenge.codingChallengeStatus === 2
}"
[matTooltip]="(challenge.solved ? 'LAUNCH_CODING_CHALLENGE' : 'SOLVE_HACKING_CHALLENGE') | translate"
>
<span class="badge-status" *ngIf="challenge.codingChallengeStatus !== 0">{{ challenge.codingChallengeStatus }}/2</span>
<mat-icon>code</mat-icon>
</button>
<!-- cheat cheat link-->
<a
class="badge not-completable"
*ngIf="challenge.mitigationUrl"
[href]="challenge.mitigationUrl"
target="_blank"
rel="noopener noreferrer"
[matTooltip]="'INFO_VULNERABILITY_MITIGATION_LINK' | translate"
>
<mat-icon>policy_outline</mat-icon>
</a>
<!-- ctf mode flag repeat-->
<button
class="badge"
[ngClass]="{ 'completed': challenge.solved }"
*ngIf="challenge.solved && applicationConfiguration.ctf.showFlagsInNotifications"
(click)="repeatChallengeNotification(challenge.key)"
[matTooltip]="'NOTIFICATION_RESEND_INSTRUCTIONS' | translate"
>
<mat-icon>flag_outline</mat-icon>
</button>
<!-- hacking instructor-->
<button
class="badge not-completable"
*ngIf="hasInstructions(challenge.name)"
[matTooltip]="'INFO_HACKING_INSTRUCTOR' | translate"
(click)="startHackingInstructorFor(challenge.name)"
>
<mat-icon>school_outline</mat-icon>
</button>
<!-- challenge hint -->
<!-- with hintUrl -->
<a
*ngIf="challenge.hint && challenge.hintUrl"
class="badge not-completable"
[style.padding]="'0 6px 0 4px'"
target="_blank"
rel="noopener noreferrer"
[href]="challenge.hintUrl"
[matTooltip]="challenge.hint | challengeHint:{hintUrl: challenge.hintUrl} | async"
>
<mat-icon>lightbulb</mat-icon>
Hint
</a>
<!-- challenge hint -->
<!-- without hintUrl -->
<span
*ngIf="challenge.hint && !challenge.hintUrl"
class="badge not-completable"
[style.padding]="'0 6px 0 4px'"
[matTooltip]="challenge.hint"
>
<mat-icon>lightbulb</mat-icon>
Hint
</span>
</div>
</div>