diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-11-23 10:45:42 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-12-03 10:15:49 +0100 |
commit | d4132d3f56b392a2e4e632db59e6644e4851228e (patch) | |
tree | be903734aa9ce3d669c40656a0f81bd3e9c7978c /client | |
parent | 4a54a93941d1c095bf249331df799c51e39c3774 (diff) | |
download | PeerTube-d4132d3f56b392a2e4e632db59e6644e4851228e.tar.gz PeerTube-d4132d3f56b392a2e4e632db59e6644e4851228e.tar.zst PeerTube-d4132d3f56b392a2e4e632db59e6644e4851228e.zip |
more explicit error messages for file uploads
Diffstat (limited to 'client')
9 files changed, 125 insertions, 39 deletions
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index 7ea4610d4..d5d019b35 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { ViewportScroller } from '@angular/common' | 1 | import { ViewportScroller } from '@angular/common' |
2 | import { HttpErrorResponse } from '@angular/common/http' | ||
2 | import { AfterViewChecked, Component, OnInit } from '@angular/core' | 3 | import { AfterViewChecked, Component, OnInit } from '@angular/core' |
3 | import { AuthService, Notifier, User, UserService } from '@app/core' | 4 | import { AuthService, Notifier, User, UserService } from '@app/core' |
5 | import { uploadErrorHandler } from '@app/helpers' | ||
4 | 6 | ||
5 | @Component({ | 7 | @Component({ |
6 | selector: 'my-account-settings', | 8 | selector: 'my-account-settings', |
@@ -44,7 +46,11 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
44 | this.user.updateAccountAvatar(data.avatar) | 46 | this.user.updateAccountAvatar(data.avatar) |
45 | }, | 47 | }, |
46 | 48 | ||
47 | err => this.notifier.error(err.message) | 49 | (err: HttpErrorResponse) => uploadErrorHandler({ |
50 | err, | ||
51 | name: $localize`avatar`, | ||
52 | notifier: this.notifier | ||
53 | }) | ||
48 | ) | 54 | ) |
49 | } | 55 | } |
50 | } | 56 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 677fa1197..88ee4e32a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -44,17 +44,30 @@ | |||
44 | </div> | 44 | </div> |
45 | </div> | 45 | </div> |
46 | 46 | ||
47 | <!-- Upload progress/cancel/error/success header --> | ||
47 | <div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel"> | 48 | <div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel"> |
48 | <div class="progress" i18n-title title="Total video quota"> | 49 | <div class="progress" i18n-title title="Total video uploaded"> |
49 | <div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"> | 50 | <div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"> |
50 | <span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span> | 51 | <span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span> |
51 | <span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span> | 52 | <span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span> |
52 | </div> | 53 | </div> |
53 | </div> | 54 | </div> |
54 | <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> | 55 | <input *ngIf="videoUploaded === false" type="button" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" /> |
55 | </div> | 56 | </div> |
56 | 57 | ||
57 | <div *ngIf="error" class="alert alert-danger"> | 58 | <div *ngIf="error && enableRetryAfterError" class="upload-progress-retry"> |
59 | <div class="progress"> | ||
60 | <div class="progress-bar red" role="progressbar" [style]="{ width: '100%' }" [attr.aria-valuenow]="100" aria-valuemin="0" [attr.aria-valuemax]="100"> | ||
61 | <span>{{ error }}</span> | ||
62 | </div> | ||
63 | </div> | ||
64 | <div class="btn-group" role="group"> | ||
65 | <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" /> | ||
66 | <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" /> | ||
67 | </div> | ||
68 | </div> | ||
69 | |||
70 | <div *ngIf="error && !enableRetryAfterError" class="alert alert-danger"> | ||
58 | <div i18n>Sorry, but something went wrong</div> | 71 | <div i18n>Sorry, but something went wrong</div> |
59 | {{ error }} | 72 | {{ error }} |
60 | </div> | 73 | </div> |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss index 9ebfa2f2f..9549257f6 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss | |||
@@ -16,27 +16,27 @@ | |||
16 | } | 16 | } |
17 | } | 17 | } |
18 | 18 | ||
19 | .upload-progress-retry, | ||
19 | .upload-progress-cancel { | 20 | .upload-progress-cancel { |
20 | display: flex; | 21 | display: flex; |
21 | margin-top: 25px; | ||
22 | margin-bottom: 40px; | 22 | margin-bottom: 40px; |
23 | 23 | ||
24 | .progress { | 24 | .progress { |
25 | @include progressbar; | 25 | @include progressbar; |
26 | flex-grow: 1; | 26 | flex-grow: 1; |
27 | height: 30px; | 27 | height: 30px; |
28 | font-size: 15px; | 28 | font-size: 14px; |
29 | background-color: rgba(11, 204, 41, 0.16); | 29 | background-color: rgba(11, 204, 41, 0.16); |
30 | 30 | ||
31 | .progress-bar { | 31 | .progress-bar { |
32 | background-color: $green; | 32 | background-color: $green; |
33 | line-height: 30px; | 33 | line-height: 30px; |
34 | text-align: left; | 34 | text-align: left; |
35 | font-weight: $font-bold; | 35 | font-weight: $font-semibold; |
36 | 36 | ||
37 | span { | 37 | span { |
38 | color: pvar(--mainBackgroundColor); | 38 | color: pvar(--mainBackgroundColor); |
39 | margin-left: 18px; | 39 | margin-left: 13px; |
40 | } | 40 | } |
41 | } | 41 | } |
42 | } | 42 | } |
@@ -47,4 +47,8 @@ | |||
47 | 47 | ||
48 | margin-left: 10px; | 48 | margin-left: 10px; |
49 | } | 49 | } |
50 | |||
51 | .btn-group > input:not(:first-child) { | ||
52 | margin-left: 0; | ||
53 | } | ||
50 | } | 54 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index 258f5c7a0..fdd0a56e5 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { Subscription } from 'rxjs' | 1 | import { Subscription } from 'rxjs' |
2 | import { HttpEventType, HttpResponse } from '@angular/common/http' | 2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' |
3 | import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 3 | import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
4 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
5 | import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' |
6 | import { scrollToTop } from '@app/helpers' | 6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { LoadingBarService } from '@ngx-loading-bar/core' |
@@ -41,11 +41,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
41 | id: 0, | 41 | id: 0, |
42 | uuid: '' | 42 | uuid: '' |
43 | } | 43 | } |
44 | formData: FormData | ||
44 | 45 | ||
45 | waitTranscodingEnabled = true | 46 | waitTranscodingEnabled = true |
46 | previewfileUpload: File | 47 | previewfileUpload: File |
47 | 48 | ||
48 | error: string | 49 | error: string |
50 | enableRetryAfterError: boolean | ||
49 | 51 | ||
50 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | 52 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC |
51 | 53 | ||
@@ -118,6 +120,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
118 | this.uploadFirstStep() | 120 | this.uploadFirstStep() |
119 | } | 121 | } |
120 | 122 | ||
123 | retryUpload () { | ||
124 | this.enableRetryAfterError = false | ||
125 | this.error = '' | ||
126 | this.uploadVideo() | ||
127 | } | ||
128 | |||
121 | cancelUpload () { | 129 | cancelUpload () { |
122 | if (this.videoUploadObservable !== null) { | 130 | if (this.videoUploadObservable !== null) { |
123 | this.videoUploadObservable.unsubscribe() | 131 | this.videoUploadObservable.unsubscribe() |
@@ -127,6 +135,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
127 | this.videoUploadObservable = null | 135 | this.videoUploadObservable = null |
128 | 136 | ||
129 | this.firstStepError.emit() | 137 | this.firstStepError.emit() |
138 | this.enableRetryAfterError = false | ||
139 | this.error = '' | ||
130 | 140 | ||
131 | this.notifier.info($localize`Upload cancelled`) | 141 | this.notifier.info($localize`Upload cancelled`) |
132 | } | 142 | } |
@@ -163,20 +173,20 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
163 | const downloadEnabled = true | 173 | const downloadEnabled = true |
164 | const channelId = this.firstStepChannelId.toString() | 174 | const channelId = this.firstStepChannelId.toString() |
165 | 175 | ||
166 | const formData = new FormData() | 176 | this.formData = new FormData() |
167 | formData.append('name', name) | 177 | this.formData.append('name', name) |
168 | // Put the video "private" -> we are waiting the user validation of the second step | 178 | // Put the video "private" -> we are waiting the user validation of the second step |
169 | formData.append('privacy', VideoPrivacy.PRIVATE.toString()) | 179 | this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) |
170 | formData.append('nsfw', '' + nsfw) | 180 | this.formData.append('nsfw', '' + nsfw) |
171 | formData.append('commentsEnabled', '' + commentsEnabled) | 181 | this.formData.append('commentsEnabled', '' + commentsEnabled) |
172 | formData.append('downloadEnabled', '' + downloadEnabled) | 182 | this.formData.append('downloadEnabled', '' + downloadEnabled) |
173 | formData.append('waitTranscoding', '' + waitTranscoding) | 183 | this.formData.append('waitTranscoding', '' + waitTranscoding) |
174 | formData.append('channelId', '' + channelId) | 184 | this.formData.append('channelId', '' + channelId) |
175 | formData.append('videofile', videofile) | 185 | this.formData.append('videofile', videofile) |
176 | 186 | ||
177 | if (this.previewfileUpload) { | 187 | if (this.previewfileUpload) { |
178 | formData.append('previewfile', this.previewfileUpload) | 188 | this.formData.append('previewfile', this.previewfileUpload) |
179 | formData.append('thumbnailfile', this.previewfileUpload) | 189 | this.formData.append('thumbnailfile', this.previewfileUpload) |
180 | } | 190 | } |
181 | 191 | ||
182 | this.isUploadingVideo = true | 192 | this.isUploadingVideo = true |
@@ -190,7 +200,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
190 | previewfile: this.previewfileUpload | 200 | previewfile: this.previewfileUpload |
191 | }) | 201 | }) |
192 | 202 | ||
193 | this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( | 203 | this.uploadVideo() |
204 | } | ||
205 | |||
206 | uploadVideo () { | ||
207 | this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe( | ||
194 | event => { | 208 | event => { |
195 | if (event.type === HttpEventType.UploadProgress) { | 209 | if (event.type === HttpEventType.UploadProgress) { |
196 | this.videoUploadPercents = Math.round(100 * event.loaded / event.total) | 210 | this.videoUploadPercents = Math.round(100 * event.loaded / event.total) |
@@ -203,13 +217,18 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
203 | } | 217 | } |
204 | }, | 218 | }, |
205 | 219 | ||
206 | err => { | 220 | (err: HttpErrorResponse) => { |
207 | // Reset progress | 221 | // Reset progress (but keep isUploadingVideo true) |
208 | this.isUploadingVideo = false | ||
209 | this.videoUploadPercents = 0 | 222 | this.videoUploadPercents = 0 |
210 | this.videoUploadObservable = null | 223 | this.videoUploadObservable = null |
211 | this.firstStepError.emit() | 224 | this.enableRetryAfterError = true |
212 | this.notifier.error(err.message) | 225 | |
226 | this.error = uploadErrorHandler({ | ||
227 | err, | ||
228 | name: $localize`video`, | ||
229 | notifier: this.notifier, | ||
230 | sticky: false | ||
231 | }) | ||
213 | } | 232 | } |
214 | ) | 233 | ) |
215 | } | 234 | } |
diff --git a/client/src/app/core/notification/notifier.service.ts b/client/src/app/core/notification/notifier.service.ts index f736672bb..165bb0c76 100644 --- a/client/src/app/core/notification/notifier.service.ts +++ b/client/src/app/core/notification/notifier.service.ts | |||
@@ -7,31 +7,35 @@ export class Notifier { | |||
7 | 7 | ||
8 | constructor (private messageService: MessageService) { } | 8 | constructor (private messageService: MessageService) { } |
9 | 9 | ||
10 | info (text: string, title?: string, timeout?: number) { | 10 | info (text: string, title?: string, timeout?: number, sticky?: boolean) { |
11 | if (!title) title = $localize`Info` | 11 | if (!title) title = $localize`Info` |
12 | 12 | ||
13 | return this.notify('info', text, title, timeout) | 13 | console.info(`${title}: ${text}`) |
14 | return this.notify('info', text, title, timeout, sticky) | ||
14 | } | 15 | } |
15 | 16 | ||
16 | error (text: string, title?: string, timeout?: number) { | 17 | error (text: string, title?: string, timeout?: number, sticky?: boolean) { |
17 | if (!title) title = $localize`Error` | 18 | if (!title) title = $localize`Error` |
18 | 19 | ||
19 | return this.notify('error', text, title, timeout) | 20 | console.error(`${title}: ${text}`) |
21 | return this.notify('error', text, title, timeout, sticky) | ||
20 | } | 22 | } |
21 | 23 | ||
22 | success (text: string, title?: string, timeout?: number) { | 24 | success (text: string, title?: string, timeout?: number, sticky?: boolean) { |
23 | if (!title) title = $localize`Success` | 25 | if (!title) title = $localize`Success` |
24 | 26 | ||
25 | return this.notify('success', text, title, timeout) | 27 | console.log(`${title}: ${text}`) |
28 | return this.notify('success', text, title, timeout, sticky) | ||
26 | } | 29 | } |
27 | 30 | ||
28 | private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number) { | 31 | private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number, sticky?: boolean) { |
29 | this.messageService.add({ | 32 | this.messageService.add({ |
30 | severity, | 33 | severity, |
31 | summary: title, | 34 | summary: title, |
32 | detail: text, | 35 | detail: text, |
33 | closable: true, | 36 | closable: true, |
34 | life: timeout || this.TIMEOUT | 37 | life: timeout || this.TIMEOUT, |
38 | sticky | ||
35 | }) | 39 | }) |
36 | } | 40 | } |
37 | } | 41 | } |
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index 9c805b4ca..f96f26fff 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | import { DatePipe } from '@angular/common' | 1 | import { DatePipe } from '@angular/common' |
2 | import { HttpErrorResponse } from '@angular/common/http' | ||
3 | import { Notifier } from '@app/core' | ||
2 | import { SelectChannelItem } from '@app/shared/shared-forms' | 4 | import { SelectChannelItem } from '@app/shared/shared-forms' |
3 | import { environment } from '../../environments/environment' | 5 | import { environment } from '../../environments/environment' |
4 | import { AuthService } from '../core/auth' | 6 | import { AuthService } from '../core/auth' |
7 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
5 | 8 | ||
6 | // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript | 9 | // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript |
7 | function getParameterByName (name: string, url: string) { | 10 | function getParameterByName (name: string, url: string) { |
@@ -172,6 +175,33 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) { | |||
172 | ) | 175 | ) |
173 | } | 176 | } |
174 | 177 | ||
178 | function uploadErrorHandler (parameters: { | ||
179 | err: HttpErrorResponse | ||
180 | name: string | ||
181 | notifier: Notifier | ||
182 | sticky?: boolean | ||
183 | }) { | ||
184 | const { err, name, notifier, sticky } = { sticky: false, ...parameters } | ||
185 | const title = $localize`The upload failed` | ||
186 | let message = err.message | ||
187 | |||
188 | if (err instanceof ErrorEvent) { // network error | ||
189 | message = $localize`The connection was interrupted` | ||
190 | notifier.error(message, title, null, sticky) | ||
191 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | ||
192 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` | ||
193 | notifier.error(message, title, null, sticky) | ||
194 | } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
195 | const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G' | ||
196 | message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})` | ||
197 | notifier.error(message, title, null, sticky) | ||
198 | } else { | ||
199 | notifier.error(err.message, title) | ||
200 | } | ||
201 | |||
202 | return message | ||
203 | } | ||
204 | |||
175 | export { | 205 | export { |
176 | sortBy, | 206 | sortBy, |
177 | durationToString, | 207 | durationToString, |
@@ -187,5 +217,6 @@ export { | |||
187 | removeElementFromArray, | 217 | removeElementFromArray, |
188 | scrollToTop, | 218 | scrollToTop, |
189 | isInViewport, | 219 | isInViewport, |
190 | isXPercentInViewport | 220 | isXPercentInViewport, |
221 | uploadErrorHandler | ||
191 | } | 222 | } |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 70be5d7d2..59860c5cb 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Observable } from 'rxjs' | 1 | import { Observable, of, throwError } from 'rxjs' |
2 | import { catchError, map, switchMap } from 'rxjs/operators' | 2 | import { catchError, map, mergeMap, switchMap } from 'rxjs/operators' |
3 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | 3 | import { HttpClient, HttpErrorResponse, HttpParams, HttpRequest } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' | 5 | import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' |
6 | import { objectToFormData } from '@app/helpers' | 6 | import { objectToFormData } from '@app/helpers' |
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index b90bffbfc..208c7f582 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss | |||
@@ -44,6 +44,11 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; | |||
44 | z-index: inherit !important; | 44 | z-index: inherit !important; |
45 | } | 45 | } |
46 | 46 | ||
47 | .btn-group > .btn:not(:first-child) { | ||
48 | border-top-left-radius: 0 !important; | ||
49 | border-bottom-left-radius: 0 !important; | ||
50 | } | ||
51 | |||
47 | .dropdown-menu { | 52 | .dropdown-menu { |
48 | z-index: z(dropdown) + 1 !important; | 53 | z-index: z(dropdown) + 1 !important; |
49 | 54 | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 1a94de5b2..fecae9fbc 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -732,6 +732,10 @@ | |||
732 | &.secondary { | 732 | &.secondary { |
733 | background-color: pvar(--secondaryColor); | 733 | background-color: pvar(--secondaryColor); |
734 | } | 734 | } |
735 | |||
736 | &.red { | ||
737 | background-color: lighten($color: #c54130, $amount: 10); | ||
738 | } | ||
735 | } | 739 | } |
736 | } | 740 | } |
737 | 741 | ||