src/app/modules/public/module/course/components/course-consumption/public-course-consumption-page/public-course-consumption-page.component.ts
OnInit
OnDestroy
styleUrls | ./public-course-consumption-page.component.scss |
templateUrl | ./public-course-consumption-page.component.html |
constructor(navigationHelperService: NavigationHelperService, activatedRoute: ActivatedRoute, courseConsumptionService: CourseConsumptionService, toasterService: ToasterService, resourceService: ResourceService, router: Router, contentUtilsServiceService: ContentUtilsServiceService, configService: ConfigService, telemetryService: TelemetryService, generaliseLabelService: GeneraliseLabelService, layoutService: LayoutService, utilService: UtilService, connectionService: ConnectionService, contentManagerService: ContentManagerService, offlineCardService: OfflineCardService, courseBatchService: CourseBatchService)
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
Parameters :
|
checkDownloadStatus |
checkDownloadStatus()
|
Returns :
void
|
checkStatus | ||||
checkStatus(status)
|
||||
Parameters :
Returns :
any
|
closeSharePopup | ||||
closeSharePopup(id)
|
||||
Parameters :
Returns :
void
|
deleteCollection | ||||
deleteCollection(collectionData)
|
||||
Parameters :
Returns :
void
|
downloadCollection | ||||
downloadCollection(collection)
|
||||
Parameters :
Returns :
void
|
exportCollection | ||||
exportCollection(collection)
|
||||
Parameters :
Returns :
void
|
getAllBatchDetails |
getAllBatchDetails()
|
Returns :
void
|
isEnrollmentAllowed | ||||
isEnrollmentAllowed(enrollmentEndDate)
|
||||
Parameters :
Returns :
any
|
isValidEnrollmentEndDate | ||||
isValidEnrollmentEndDate(enrollmentEndDate)
|
||||
Parameters :
Returns :
boolean
|
isYoutubeContentPresent | ||||
isYoutubeContentPresent(collection)
|
||||
Parameters :
Returns :
void
|
logTelemetry | |||||||||
logTelemetry(id, content?: literal type)
|
|||||||||
Parameters :
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
onShareLink |
onShareLink()
|
Returns :
void
|
redirectToExplore |
redirectToExplore()
|
Returns :
void
|
setTelemetryShareData | ||||
setTelemetryShareData(param)
|
||||
Parameters :
Returns :
void
|
showJoinModal | ||||
showJoinModal(event)
|
||||
Parameters :
Returns :
void
|
updateCollection | ||||
updateCollection(collection)
|
||||
Parameters :
Returns :
void
|
batchList |
Type : []
|
Default value : []
|
contentDownloadStatus |
Type : object
|
Default value : {}
|
Public contentUtilsServiceService |
Type : ContentUtilsServiceService
|
Public courseBatchService |
Type : CourseBatchService
|
Public courseHierarchy |
Type : any
|
Public courseId |
Type : string
|
disableDelete |
Default value : false
|
enrollmentEndDate |
Type : string
|
Public generaliseLabelService |
Type : GeneraliseLabelService
|
isAvailableLocally |
Default value : false
|
isConnected |
Default value : false
|
isDesktopApp |
Default value : false
|
layoutConfiguration |
Type : any
|
Public layoutService |
Type : LayoutService
|
Public navigationHelperService |
Type : NavigationHelperService
|
Public resourceService |
Type : ResourceService
|
Public router |
Type : Router
|
shareLink |
Type : string
|
sharelinkModal |
Type : boolean
|
showDeleteModal |
Default value : false
|
showDownloadLoader |
Default value : false
|
Public showError |
Default value : false
|
showExportLoader |
Default value : false
|
Public showLoader |
Default value : true
|
showModal |
Default value : false
|
showUpdate |
Default value : false
|
Public telemetryCourseImpression |
Type : IImpressionEventInput
|
telemetryShareData |
Type : Array<ITelemetryShare>
|
telemetryShareData |
Public toasterService |
Type : ToasterService
|
todayDate |
Default value : dayjs(new Date()).format('YYYY-MM-DD')
|
Public unsubscribe |
Default value : new Subject<void>()
|
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ResourceService, ToasterService, ConfigService, ContentUtilsServiceService, ITelemetryShare,
LayoutService, UtilService } from '@sunbird/shared';
import { CourseBatchService, CourseConsumptionService } from '@sunbird/learn';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import * as _ from 'lodash-es';
import { IImpressionEventInput, TelemetryService } from '@sunbird/telemetry';
import { NavigationHelperService, ConnectionService, OfflineCardService, ServerResponse } from '@sunbird/shared';
import { GeneraliseLabelService } from '@sunbird/core';
import { ContentManagerService } from '../../../../offline/services/content-manager/content-manager.service';
import dayjs from 'dayjs';
@Component({
templateUrl: './public-course-consumption-page.component.html',
styleUrls: ['./public-course-consumption-page.component.scss']
})
export class PublicCourseConsumptionPageComponent implements OnInit, OnDestroy {
public courseId: string;
public showLoader = true;
public showError = false;
public courseHierarchy: any;
layoutConfiguration: any;
sharelinkModal: boolean;
shareLink: string;
public telemetryCourseImpression: IImpressionEventInput;
batchList = [];
enrollmentEndDate: string;
todayDate = dayjs(new Date()).format('YYYY-MM-DD');
// Desktop App
isDesktopApp = false;
isConnected = false;
contentDownloadStatus = {};
showUpdate = false;
showExportLoader = false;
showModal = false;
showDownloadLoader = false;
disableDelete = false;
isAvailableLocally = false;
showDeleteModal = false;
/**
* telemetryShareData
*/
telemetryShareData: Array<ITelemetryShare>;
public unsubscribe = new Subject<void>();
constructor(public navigationHelperService: NavigationHelperService, private activatedRoute: ActivatedRoute,
private courseConsumptionService: CourseConsumptionService, public toasterService: ToasterService,
public resourceService: ResourceService, public router: Router, public contentUtilsServiceService: ContentUtilsServiceService,
private configService: ConfigService, private telemetryService: TelemetryService,
public generaliseLabelService: GeneraliseLabelService, public layoutService: LayoutService,
private utilService: UtilService,
private connectionService: ConnectionService,
private contentManagerService: ContentManagerService,
private offlineCardService: OfflineCardService,
public courseBatchService: CourseBatchService
) {
}
showJoinModal(event) {
this.courseConsumptionService.showJoinCourseModal.emit(event);
}
ngOnInit() {
this.layoutConfiguration = this.layoutService.initlayoutConfig();
this.layoutService.switchableLayout().
pipe(takeUntil(this.unsubscribe)).subscribe(layoutConfig => {
if (layoutConfig != null) {
this.layoutConfiguration = layoutConfig.layout;
}
});
this.isDesktopApp = this.utilService.isDesktopApp;
if (this.isDesktopApp) {
this.connectionService.monitor().pipe(takeUntil(this.unsubscribe)).subscribe(isConnected => {
this.isConnected = isConnected;
});
this.contentManagerService.contentDownloadStatus$.pipe(takeUntil(this.unsubscribe)).subscribe( contentDownloadStatus => {
this.contentDownloadStatus = contentDownloadStatus;
this.checkDownloadStatus();
});
}
this.showLoader = true;
const routeParams: any = {...this.activatedRoute.snapshot.firstChild.params };
this.courseId = routeParams.courseId;
this.getAllBatchDetails();
if (!this.courseId) {
return this.redirectToExplore();
}
const inputParams = {params: this.configService.appConfig.CourseConsumption.contentApiQueryParams};
this.courseConsumptionService.getCourseHierarchy(this.courseId, inputParams).pipe(takeUntil(this.unsubscribe))
.subscribe((courseHierarchy: any) => {
this.courseHierarchy = courseHierarchy;
this.layoutService.updateSelectedContentType.emit(this.courseHierarchy.contentType);
this.showLoader = false;
}, (error) => {
if (_.isEqual(_.get(error, 'error.responseCode'), 'RESOURCE_NOT_FOUND')) {
this.toasterService.error(this.generaliseLabelService.messages.emsg.m0002);
} else {
this.toasterService.error(this.resourceService.messages.emsg.m0005);
}
this.redirectToExplore();
});
}
getAllBatchDetails() {
this.batchList = [];
const searchParams: any = {
filters: {
status: '1',
courseId: this.courseId
},
offset: 0,
sort_by: { createdDate: 'desc' }
};
searchParams.filters.enrollmentType = 'open';
this.courseBatchService.getAllBatchDetails(searchParams).pipe(
takeUntil(this.unsubscribe))
.subscribe((data: ServerResponse) => {
if (data.result.response.content && data.result.response.content.length > 0) {
this.batchList = data.result.response.content;
this.enrollmentEndDate = _.get(this.batchList[0], 'enrollmentEndDate');
}
},
(err: ServerResponse) => {
this.showError = true;
this.toasterService.error(this.resourceService.messages.fmsg.m0004);
});
}
isEnrollmentAllowed(enrollmentEndDate) {
return dayjs(enrollmentEndDate).isBefore(this.todayDate);
}
isValidEnrollmentEndDate(enrollmentEndDate) {
return !!enrollmentEndDate;
}
onShareLink() {
this.sharelinkModal = true;
this.shareLink = this.contentUtilsServiceService.getCoursePublicShareUrl(this.courseHierarchy.identifier);
this.setTelemetryShareData(this.courseHierarchy);
}
setTelemetryShareData(param) {
this.telemetryShareData = [{
id: param.identifier,
type: param.contentType,
ver: param.pkgVersion ? param.pkgVersion.toString() : '1.0'
}];
}
redirectToExplore() {
this.navigationHelperService.navigateToResource('explore?selectedTab=course');
}
closeSharePopup(id) {
this.sharelinkModal = false;
const interactData = {
context: {
env: _.get(this.activatedRoute.snapshot.data.telemetry, 'env') || 'explore',
cdata: []
},
edata: {
id: id,
type: 'click',
pageid: _.get(this.activatedRoute.snapshot.data.telemetry, 'pageid') || 'explore-course-toc',
},
object: {
id: _.get(this.courseHierarchy, 'identifier'),
type: _.get(this.courseHierarchy, 'contentType') || 'Course',
ver: `${_.get(this.courseHierarchy, 'pkgVersion')}` || `1.0`,
rollup: { l1: this.courseId }
}
};
this.telemetryService.interact(interactData);
}
logTelemetry(id, content?: {}) {
let objectRollUp;
if (content) {
objectRollUp = this.courseConsumptionService.getContentRollUp(this.courseHierarchy, _.get(content, 'identifier'));
}
const interactData = {
context: {
env: _.get(this.activatedRoute.snapshot.data.telemetry, 'env') || 'content',
cdata: []
},
edata: {
id: id,
type: 'click',
pageid: _.get(this.activatedRoute.snapshot.data.telemetry, 'pageid') || 'explore-course',
},
object: {
id: content ? _.get(content, 'identifier') : this.activatedRoute.snapshot.params.courseId,
type: content ? _.get(content, 'contentType') : 'Course',
ver: content ? `${_.get(content, 'pkgVersion')}` : `1.0`,
rollup: objectRollUp ? this.courseConsumptionService.getRollUp(objectRollUp) : {}
}
};
this.telemetryService.interact(interactData);
}
checkStatus(status) {
this.checkDownloadStatus();
return this.utilService.getPlayerDownloadStatus(status, this.courseHierarchy);
}
checkDownloadStatus() {
if (this.courseHierarchy) {
const downloadStatus = ['CANCELED', 'CANCEL', 'FAILED', 'DOWNLOAD'];
const status = this.contentDownloadStatus[this.courseHierarchy.identifier];
this.courseHierarchy['downloadStatus'] = _.isEqual(downloadStatus, status) ? 'DOWNLOAD' :
(_.includes(['INPROGRESS', 'RESUME', 'INQUEUE'], status) ? 'DOWNLOADING' : _.isEqual(status, 'COMPLETED') ? 'DOWNLOADED' : status);
}
}
updateCollection(collection) {
collection['downloadStatus'] = this.resourceService.messages.stmsg.m0140;
this.logTelemetry('update-collection');
const request = {
contentId: collection.identifier
};
this.contentManagerService.updateContent(request).pipe(takeUntil(this.unsubscribe)).subscribe(data => {
collection['downloadStatus'] = this.resourceService.messages.stmsg.m0140;
this.showUpdate = false;
}, (err) => {
this.showUpdate = true;
const errorMessage = !this.isConnected ? _.replace(this.resourceService.messages.smsg.m0056, '{contentName}', collection.name) :
this.resourceService.messages.fmsg.m0096;
this.toasterService.error(errorMessage);
});
}
exportCollection(collection) {
this.logTelemetry('export-collection');
this.showExportLoader = true;
this.contentManagerService.exportContent(collection.identifier)
.pipe(takeUntil(this.unsubscribe))
.subscribe(data => {
this.showExportLoader = false;
this.toasterService.success(this.resourceService.messages.smsg.m0059);
}, error => {
this.showExportLoader = false;
if (_.get(error, 'error.responseCode') !== 'NO_DEST_FOLDER') {
this.toasterService.error(this.resourceService.messages.fmsg.m0091);
}
});
}
isYoutubeContentPresent(collection) {
this.logTelemetry('is-youtube-in-collection');
this.showModal = this.offlineCardService.isYoutubeContent(collection);
if (!this.showModal) {
this.downloadCollection(collection);
}
}
downloadCollection(collection) {
this.showDownloadLoader = true;
this.disableDelete = false;
collection['downloadStatus'] = this.resourceService.messages.stmsg.m0140;
this.logTelemetry('download-collection');
this.contentManagerService.downloadContentId = collection.identifier;
this.contentManagerService.downloadContentData = collection;
this.contentManagerService.failedContentName = collection.name;
this.contentManagerService.startDownload({}).pipe(takeUntil(this.unsubscribe)).subscribe(data => {
this.contentManagerService.downloadContentId = '';
this.contentManagerService.downloadContentData = {};
this.showDownloadLoader = false;
collection['downloadStatus'] = this.resourceService.messages.stmsg.m0140;
}, error => {
this.disableDelete = true;
this.showDownloadLoader = false;
this.contentManagerService.downloadContentId = '';
this.contentManagerService.downloadContentData = {};
this.contentManagerService.failedContentName = '';
collection['downloadStatus'] = this.resourceService.messages.stmsg.m0138;
if (!(error.error.params.err === 'LOW_DISK_SPACE')) {
this.toasterService.error(this.resourceService.messages.fmsg.m0090);
}
});
}
deleteCollection(collectionData) {
this.disableDelete = true;
this.logTelemetry('delete-collection');
const request = {request: {contents: [collectionData.identifier]}};
this.contentManagerService.deleteContent(request).pipe(takeUntil(this.unsubscribe)).subscribe(data => {
this.toasterService.success(this.resourceService.messages.stmsg.desktop.deleteCourseSuccessMessage);
collectionData['downloadStatus'] = 'DOWNLOAD';
collectionData['desktopAppMetadata.isAvailable'] = false;
this.redirectToExplore();
}, err => {
this.disableDelete = false;
this.toasterService.error(this.resourceService.messages.etmsg.desktop.deleteCourseErrorMessage);
});
}
ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
}
<!-- Back Button Previous Module & Next Module button -->
<div
[ngClass]="layoutConfiguration ? 'sb-back-actionbar' : 'sb-bg-color-white back-btn-container cc-player__btn-back relative9'"
class="relative position mt-0">
<div class="ui container py-0 px-0 d-flex flex-ai-center">
<div class="py-0 d-flex flex-ai-center w-100">
<!-- /* Back button */ -->
<button type="button" [ngClass]="layoutConfiguration ? 'sb-btn-primary sb-btn-round' : 'sb-btn-link sb-btn-link-primary sb-left-icon-btn px-0'" class="sb-btn sb-btn-normal" tabindex="0" (click)="redirectToExplore()" attr.aria-label="{{resourceService?.frmelmnts?.btn?.back}}">
<i class="icon-svg icon-svg--xxs icon-back mr-4"><svg class="icon icon-svg--primary">
<use xlink:href="assets/images/sprite.svg#arrow-long-left"></use>
</svg></i>
<span>{{resourceService?.frmelmnts?.btn?.back}}</span>
</button>
<!-- Textbook details with continue playing and join course button -->
<div class="textbook d-flex flex-ai-center w-100 ml-16" *ngIf="!showLoader && !showError">
<!-- Textbook details -->
<div class="textbook__details d-flex flex-ai-center">
<!-- Textbook image -->
<div class="textbook__bookimg">
<img src="{{courseHierarchy?.appIcon || 'assets/images/book.png'}}" alt="{{courseHierarchy?.name}}">
</div>
<div class="ml-8 textbook__heading">
<!-- Textbook title -->
<h5 class="textbook__title sb-color-primary font-weight-bold mb-0" tabindex="0">{{courseHierarchy?.name}}
</h5>
<!-- Textbook Rating with share icon -->
<div class="textbook__rating d-flex flex-ai-center">
<sui-rating class="ui star rating" [isReadonly]="true" [ngModel]="courseHierarchy.me_averageRating || 0"
[maximum]="5"></sui-rating>
<button tabindex="0" (click)="onShareLink()"
class="sb-btn sb-btn-normal sb-btn-link sb-btn-link-primary sb-left-icon-btn ml-8 sb-btn-icon-fix">
<i class="icon-svg icon-svg--xs icon-share mr-8"><svg class="icon icon-svg--primary">
<use xlink:href="assets/images/sprite.svg#share"></use>
</svg></i>
{{resourceService?.frmelmnts?.lbl?.share}}
</button>
</div>
</div>
</div>
<div class="batch-details__created mt-8 fsmall mx-8" *ngIf="isValidEnrollmentEndDate(enrollmentEndDate)">
<span class="sb-label-12size"
*ngIf="!isEnrollmentAllowed(enrollmentEndDate)">{{resourceService?.messages?.stmsg?.m0136}}
{{enrollmentEndDate | dateFormat}}</span>
<span class="sb-label-status-error"
*ngIf="isEnrollmentAllowed(enrollmentEndDate)">{{resourceService?.messages?.emsg?.m008 | interpolate:'{endDate}':enrollmentEndDate}}</span>
</div>
<div class="certified-course mx-8" [ngClass]="{'d-flex': isDesktopApp}">
<div class="certified-course__btn" *ngIf="!enrolledCourse && !courseMentor">
<button class="sb-btn sb-btn-secondary sb-btn-normal ml-auto textbook__addbtn" tabindex="0"
[disabled]="isEnrollmentAllowed(enrollmentEndDate)"
[ngClass]="{'sb-btn-disabled': isEnrollmentAllowed(enrollmentEndDate)}"
(click)="showJoinModal(true); logTelemetry(courseHierarchy?.primaryCategory.toLowerCase() === 'course' ? 'join-course' : 'join', courseHierarchy);">{{generaliseLabelService?.frmelmnts?.btn?.enroll}}</button>
</div>
<div *ngIf="isDesktopApp && !enrolledCourse && !courseMentor" class="d-flex">
<button type="button" class="sb-btn sb-btn-outline-primary sb-btn-normal mr-8"
*ngIf="isConnected && (checkStatus('DOWNLOADED')) && showUpdate" tabindex="0"
(click)="updateCollection(courseHierarchy)">{{resourceService?.frmelmnts?.btn?.update}}</button>
<button type="button" class="sb-btn sb-btn-outline-primary sb-btn-normal mr-8"
*ngIf="checkStatus('DOWNLOADED')" [disabled]="disableDelete" tabindex="0"
(click)="logTelemetry('confirm-delete-collection'); showDeleteModal = !showDeleteModal;">{{resourceService?.frmelmnts?.btn?.delete}}</button>
<button type="button" class="sb-btn sb-btn-outline-primary sb-btn-normal mr-8"
*ngIf="checkStatus('DOWNLOADED')" tabindex="0"
(click)="exportCollection(courseHierarchy)">{{resourceService?.frmelmnts?.lbl?.saveToPenDrive}}</button>
<button type="button" class="sb-btn sb-btn-outline-primary sb-btn-normal mr-8"
*ngIf="checkStatus('DOWNLOAD')" appOnlineOnly tabindex="0"
(click)="isYoutubeContentPresent(courseHierarchy)">{{resourceService?.frmelmnts?.btn?.download}}</button>
<button type="button" class="sb-btn sb-btn-outline-primary sb-btn-normal mr-8"
*ngIf="checkStatus('DOWNLOADING')">{{resourceService?.frmelmnts?.lbl?.downloading}}</button>
<button type="button" class="sb-btn sb-btn-outline-primary sb-btn-normal mr-8"
*ngIf="checkStatus('PAUSED')">{{resourceService.frmelmnts?.lbl?.downloadingPaused}}</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div [ngClass]="layoutConfiguration ? 'sbt-inside-page-container' : 'pt-16'">
<router-outlet *ngIf="!showLoader && !showError"></router-outlet>
<div *ngIf="showLoader" class="ui container">
<app-loader></app-loader>
</div>
</div>
<app-modal-wrapper *ngIf="sharelinkModal" [config]="{disableClose: false, panelClass: 'material-modal'}"
(dismiss)="closeSharePopup('close-share-link-popup')">
<ng-template sbModalContent>
<app-share-link [shareLink]="shareLink" [telemetryShareData]="telemetryShareData">
</app-share-link>
</ng-template>
</app-modal-wrapper>
<div *ngIf="isDesktopApp">
<app-modal-wrapper *ngIf="showModal" [config]="{disableClose: true, size: 'normal'}" (dismiss)="showModal = !showModal;"
#modal>
<ng-template sbModalContent>
<div class="sb-modal">
<div class="transition ui dimmer page modals active visible">
<div class="ui modal transition active visible normal">
<div class="sb-modal-header">
{{resourceService.frmelmnts?.btn?.download}}
</div>
<div class="sb-modal-content">
<p>{{resourceService?.messages?.stmsg?.m0137}}</p>
</div>
<div class="sb-modal-actions">
<button class="sb-btn sb-btn-normal sb-btn-primary" tabindex="0"
(click)="downloadCollection(courseHierarchy); showModal = !showModal;">
{{resourceService.frmelmnts?.btn?.download}}
</button>
<button class="sb-btn sb-btn-normal sb-btn-outline-primary" tabindex="0"
(click)="logTelemetry('cancel-download-collection'); showModal = !showModal;">
{{resourceService.frmelmnts?.btn?.cancel}}
</button>
</div>
</div>
</div>
</div>
</ng-template>
</app-modal-wrapper>
<app-modal-wrapper *ngIf="showDeleteModal" [config]="{disableClose: true, size: 'normal'}"
(dismiss)="showDeleteModal = !showDeleteModal;" #modal>
<ng-template sbModalContent>
<div class="sb-modal">
<div class="transition ui dimmer page modals active visible">
<div class="ui modal transition active visible normal">
<div class="sb-modal-header">
{{resourceService?.frmelmnts?.lbl?.delete}}
</div>
<div class="sb-modal-content">
<p>{{resourceService?.frmelmnts?.lbl?.desktop?.deleteCourse | interpolate:'{name}': courseHierarchy?.name}}
</p>
</div>
<div class="sb-modal-actions">
<button class="sb-btn sb-btn-normal sb-btn-primary" tabindex="0"
(click)="deleteCollection(courseHierarchy); showDeleteModal = !showDeleteModal;">
{{resourceService?.frmelmnts?.lbl?.delete}}
</button>
<button class="sb-btn sb-btn-normal sb-btn-outline-primary" tabindex="0"
(click)="logTelemetry('cancel-delete-collection'); showDeleteModal = !showDeleteModal;">
{{resourceService.frmelmnts?.btn?.cancel}}
</button>
</div>
</div>
</div>
</div>
</ng-template>
</app-modal-wrapper>
</div>
./public-course-consumption-page.component.scss
@use "@project-sunbird/sb-styles/assets/mixins/mixins" as *;
::ng-deep {
.ui.tiny.progress .bar {
min-width: 0
}
}
.back-btn-container {
z-index: 1;
.modulebtn-divider {
width: calculateRem(1px);
height: calculateRem(24px);
background: var(--gray-200);
display: inline-block;
margin: 0 calculateRem(8px);
}
}
.certified-course {
justify-self: end;
align-self: center;
}
.textbook {
&-container {
box-shadow: 0 0 calculateRem(10px) 0 rgba(0, 0, 0, 0.25);
position: relative;
background-color: var(--gray-0);
}
&__details {
flex: 1;
min-width: 0;
}
&__bookimg {
width: calculateRem(48px);
height: calculateRem(48px);
img {
width: calculateRem(48px);
height: calculateRem(48px);
}
}
&__heading {
min-width: 0;
.textbook__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&__addbtn {
white-space: nowrap;
}
}
@include respond-below(sm) {
.certified-course {
justify-self: start;
}
.certified-course__btn {
margin-top: calculateRem(8px);
}
}