MOBILE-3371 core: Implement course image component

main
Noel De Martin 2023-06-28 12:17:10 +02:00
parent 55609b5f99
commit 9e1bcaf581
21 changed files with 628 additions and 0 deletions

View File

@ -0,0 +1 @@
[{"id":1,"courseimage":"https://picsum.photos/500/500","shortname":"Moodle and Mountaineering","summary":"This course will introduce you to the basics of Alpine Mountaineering, while at the same time highlighting some of the great features of Moodle."},{"id":2,"courseimage":"assets/storybook/geopattern.svg","shortname":"Digital Literacy","summary":"This course explores Digital Literacy and its importance for teachers and students. The course is optimised for the Moodle App. Please try it out!"},{"id":3,"shortname":"Class and Conflict in World Cinema","summary":"In this module we will analyse two very significant films - City of God and La Haine, both of which depict violent lives in poor conditions, the former in the favelas of Brazil and the latter in a Parisian banlieue. We will look at how conflict and class are portrayed, focusing particularly on the use of mise en scène."}]

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1 @@
{"id":"123456","info":{"version":"2022041900","sitename":"School","username":"barbara","firstname":"Barbara","lastname":"Gardner","fullname":"Barbara Gardner","lang":"en","userid":1,"siteurl":"https://campus.example.edu","userpictureurl":"","functions":[]}}

View File

@ -64,6 +64,7 @@ import { CoreMessageComponent } from './message/message';
import { CoreGroupSelectorComponent } from './group-selector/group-selector'; import { CoreGroupSelectorComponent } from './group-selector/group-selector';
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal'; import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal'; import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
import { CoreCourseImageComponent } from '@components/course-image/course-image';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -75,6 +76,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
CoreContextMenuComponent, CoreContextMenuComponent,
CoreContextMenuItemComponent, CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent, CoreContextMenuPopoverComponent,
CoreCourseImageComponent,
CoreDownloadRefreshComponent, CoreDownloadRefreshComponent,
CoreDynamicComponent, CoreDynamicComponent,
CoreEmptyBoxComponent, CoreEmptyBoxComponent,
@ -128,6 +130,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
CoreContextMenuComponent, CoreContextMenuComponent,
CoreContextMenuItemComponent, CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent, CoreContextMenuPopoverComponent,
CoreCourseImageComponent,
CoreDownloadRefreshComponent, CoreDownloadRefreshComponent,
CoreDynamicComponent, CoreDynamicComponent,
CoreEmptyBoxComponent, CoreEmptyBoxComponent,

View File

@ -0,0 +1,5 @@
<ion-icon *ngIf="!course.courseimage" name="fas-graduation-cap" slot="start" aria-hidden="true">
</ion-icon>
<ion-avatar *ngIf="course.courseimage" slot="start">
<img [src]="course.courseimage" core-external-content alt="" (error)="loadFallbackCourseIcon()" />
</ion-avatar>

View File

@ -0,0 +1,65 @@
@import "~theme/globals";
:host {
--core-image-radius: var(--core-courseimage-radius);
--core-image-size: 60px;
display: flex;
justify-content: center;
align-items: center;
background: var(--course-color, white);
border-radius: var(--core-image-radius);
@for $i from 0 to length($core-course-image-background) {
&.course-color-#{$i} {
--course-color: var(--core-course-color-#{$i});
--course-color-tint: var(--core-course-color-#{$i}-tint);
}
}
ion-icon {
--padding: 12px;
padding: var(--padding);
font-size: calc(var(--core-image-size) - var(--padding) * 2);
color: var(--course-color-tint);
}
ion-avatar {
--border-radius: var(--core-image-radius);
width: var(--core-image-size);
height: var(--core-image-size);
img {
background: transparent;
}
}
img[src$=".svg"] {
min-width: 100%;
}
&.fill-container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
--core-image-radius: 0px;
--core-image-size: 100%;
ion-icon {
opacity: 0.5;
width: 80px;
height: 80px;
}
}
}
:host-context(ion-item) {
@include margin(6px, 8px, 6px, 0px);
}

View File

@ -0,0 +1,81 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, ElementRef, OnInit, OnChanges, HostBinding } from '@angular/core';
import { CoreCourseListItem } from '@features/courses/services/courses';
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
import { CoreColors } from '@singletons/colors';
@Component({
selector: 'core-course-image',
templateUrl: 'course-image.html',
styleUrls: ['./course-image.scss'],
})
export class CoreCourseImageComponent implements OnInit, OnChanges {
@Input() course!: CoreCourseListItem;
@Input() fill = false;
protected element: HTMLElement;
constructor(element: ElementRef) {
this.element = element.nativeElement;
}
@HostBinding('class.fill-container')
get fillContainer(): boolean {
return this.fill;
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.setCourseColor();
}
/**
* @inheritdoc
*/
ngOnChanges(): void {
this.setCourseColor();
}
/**
* Removes the course image set because it cannot be loaded and set the fallback icon color.
*/
loadFallbackCourseIcon(): void {
this.course.courseimage = undefined;
// Set the color because it won't be set at this point.
this.setCourseColor();
}
/**
* Set course color.
*/
protected async setCourseColor(): Promise<void> {
await CoreCoursesHelper.loadCourseColorAndImage(this.course);
if (this.course.color) {
this.element.style.setProperty('--course-color', this.course.color);
const tint = CoreColors.lighter(this.course.color, 50);
this.element.style.setProperty('--course-color-tint', tint);
} else if(this.course.colorNumber !== undefined) {
this.element.classList.add('course-color-' + this.course.colorNumber);
}
}
}

View File

@ -19,9 +19,13 @@ import { StorybookModule } from '@/storybook/storybook.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module'; import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreComponentsModule } from '@components/components.module'; import { CoreComponentsModule } from '@components/components.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CoreCourseImageCardsPageComponent } from '@components/stories/components/course-image-cards-page/course-image-cards-page';
import { CoreCourseImageListPageComponent } from '@components/stories/components/course-image-list-page/course-image-list-page';
@NgModule({ @NgModule({
declarations: [ declarations: [
CoreCourseImageCardsPageComponent,
CoreCourseImageListPageComponent,
CoreEmptyBoxPageComponent, CoreEmptyBoxPageComponent,
CoreEmptyBoxWrapperComponent, CoreEmptyBoxWrapperComponent,
], ],

View File

@ -0,0 +1,22 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
<h1>Course Cards</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card *ngFor="let course of courses">
<div class="course-image-wrapper">
<core-course-image [course]="course" [fill]="true"></core-course-image>
</div>
<ion-card-header>
<ion-card-title>{{ course.shortname }}</ion-card-title>
</ion-card-header>
<ion-card-content>
{{ course.summary }}
</ion-card-content>
</ion-card>
</ion-content>
</ion-app>

View File

@ -0,0 +1,16 @@
:host {
ion-card {
max-width: 350px;
margin-right: auto;
margin-left: auto;
}
.course-image-wrapper {
width: 100%;
height: 0;
padding-top: 40%;
position: relative;
}
}

View File

@ -0,0 +1,28 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreCourseListItem } from '@features/courses/services/courses';
import courses from '@/assets/storybook/courses.json';
@Component({
selector: 'core-course-image-cards-page',
templateUrl: 'course-image-cards-page.html',
styleUrls: ['./course-image-cards-page.scss'],
})
export class CoreCourseImageCardsPageComponent {
courses: Partial<CoreCourseListItem>[] = courses;
}

View File

@ -0,0 +1,19 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
<h1>Courses List</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
<core-course-image [course]="course" slot="start"></core-course-image>
<ion-label>
{{ course.shortname }}
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-app>

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreCourseListItem } from '@features/courses/services/courses';
import courses from '@/assets/storybook/courses.json';
@Component({
selector: 'core-course-image-list-page',
templateUrl: 'course-image-list-page.html',
})
export class CoreCourseImageListPageComponent {
courses: Partial<CoreCourseListItem>[] = courses;
}

View File

@ -0,0 +1,104 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Meta, moduleMetadata } from '@storybook/angular';
import { story } from '@/storybook/utils/helpers';
import { CoreCourseImageComponent } from '@components/course-image/course-image';
import { APP_INITIALIZER } from '@angular/core';
import { CoreSitesStub } from '@/storybook/stubs/services/sites';
import { CoreCourseImageListPageComponent } from '@components/stories/components/course-image-list-page/course-image-list-page';
import { CoreComponentsStorybookModule } from '@components/stories/components/components.module';
import { CoreCourseImageCardsPageComponent } from '@components/stories/components/course-image-cards-page/course-image-cards-page';
interface Args {
type: 'image' | 'geopattern' | 'color';
fill: boolean;
}
export default <Meta> {
title: 'Core/Course Image',
component: CoreCourseImageComponent,
decorators: [
moduleMetadata({
imports: [CoreComponentsStorybookModule],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
const site = CoreSitesStub.getRequiredCurrentSite();
site.stubWSResponse('tool_mobile_get_config', {
settings: [
{ name: 'core_admin_coursecolor1', value: '#F9B000' },
{ name: 'core_admin_coursecolor2', value: '#EF4B00' },
{ name: 'core_admin_coursecolor3', value: '#4338FB' },
{ name: 'core_admin_coursecolor4', value: '#E142FB' },
{ name: 'core_admin_coursecolor5', value: '#FF0064' },
{ name: 'core_admin_coursecolor6', value: '#FF0F18' },
{ name: 'core_admin_coursecolor7', value: '#039B06' },
{ name: 'core_admin_coursecolor8', value: '#039B88' },
{ name: 'core_admin_coursecolor9', value: '#EF009B' },
{ name: 'core_admin_coursecolor10', value: '#020B6E' },
],
warnings: [],
});
},
},
],
}),
],
argTypes: {
type: {
control: {
type: 'select',
options: ['image', 'geopattern', 'color'],
},
},
},
args: {
type: 'image',
fill: false,
},
};
const Template = story<Args>(({ type, ...args }) => {
const getImageSource = () => {
switch (type) {
case 'image':
return 'https://picsum.photos/500/500';
case 'geopattern':
return 'assets/storybook/geopattern.svg';
case 'color':
return undefined;
}
};
return {
component: CoreCourseImageComponent,
props: {
...args,
course: {
id: 1,
courseimage: getImageSource(),
},
},
};
});
export const Primary = story(Template);
export const ListPage = story(() => ({ component: CoreCourseImageListPageComponent }));
export const CardsPage = story(() => ({ component: CoreCourseImageCardsPageComponent }));

View File

@ -20,6 +20,14 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import englishTranslations from '@/assets/lang/en.json'; import englishTranslations from '@/assets/lang/en.json';
import { CoreApplicationInitStatus } from '@classes/application-init-status'; import { CoreApplicationInitStatus } from '@classes/application-init-status';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreSitesProviderStub, CoreSitesStub } from '@/storybook/stubs/services/sites';
import { CoreSitesProvider } from '@services/sites';
import { CoreDbProviderStub } from '@/storybook/stubs/services/db';
import { CoreDbProvider } from '@services/db';
import { CoreFilepoolProviderStub } from '@/storybook/stubs/services/filepool';
import { CoreFilepoolProvider } from '@services/filepool';
import { HttpClientStub } from '@/storybook/stubs/services/http';
import { HttpClient } from '@angular/common/http';
// For translate loader. AoT requires an exported function for factories. // For translate loader. AoT requires an exported function for factories.
export class StaticTranslateLoader extends TranslateLoader { export class StaticTranslateLoader extends TranslateLoader {
@ -45,12 +53,17 @@ export class StaticTranslateLoader extends TranslateLoader {
], ],
providers: [ providers: [
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus }, { provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus },
{ provide: CoreSitesProvider, useClass: CoreSitesProviderStub },
{ provide: CoreDbProvider, useClass: CoreDbProviderStub },
{ provide: CoreFilepoolProvider, useClass: CoreFilepoolProviderStub },
{ provide: HttpClient, useClass: HttpClientStub },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
multi: true, multi: true,
useValue: () => { useValue: () => {
Translate.setDefaultLang('en'); Translate.setDefaultLang('en');
Translate.use('en'); Translate.use('en');
CoreSitesStub.stubCurrentSite();
}, },
}, },
], ],

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSite, CoreSiteConfigResponse, CoreSiteInfo, CoreSiteWSPreSets, WSObservable } from '@classes/site';
import { of } from 'rxjs';
export interface CoreSiteFixture {
id: string;
info: CoreSiteInfo;
}
export class CoreSiteStub extends CoreSite {
protected wsStubs: Record<string, unknown> = {};
constructor (fixture: CoreSiteFixture) {
super(fixture.id, fixture.info.siteurl, undefined, fixture.info);
this.stubWSResponse<CoreSiteConfigResponse>('tool_mobile_get_config', {
settings: [],
warnings: [],
});
}
/**
* @inheritdoc
*/
readObservable<T = unknown>(wsFunction: string, data: unknown, preSets?: CoreSiteWSPreSets): WSObservable<T> {
if (wsFunction in this.wsStubs) {
return of(this.wsStubs[wsFunction] as T);
}
return super.readObservable<T>(wsFunction, data, preSets);
}
/**
* Prepare as stubbed response for a given WS.
*
* @param wsFunction WS function.
* @param response Response.
*/
stubWSResponse<T=unknown>(wsFunction: string, response: T): void {
this.wsStubs[wsFunction] = response;
}
}

View File

@ -0,0 +1,32 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { SQLiteDB } from '@classes/sqlitedb';
import { SQLiteObject } from '@ionic-native/sqlite/ngx';
/**
* SQlite database stub.
*/
export class SQLiteDBStub extends SQLiteDB {
/**
* @inheritdoc
*/
async createDatabase(): Promise<SQLiteObject> {
return new Proxy({
executeSql: () => Promise.resolve({ insertId: Math.random().toString() }),
}, {}) as unknown as SQLiteObject;
}
}

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { SQLiteDBStub } from '@/storybook/stubs/classes/sqlitedb';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreDbProvider } from '@services/db';
/**
* Database provider stub.
*/
export class CoreDbProviderStub extends CoreDbProvider {
/**
* @inheritdoc
*/
getDB(name: string, forceNew?: boolean): SQLiteDB {
if (this.dbInstances[name] === undefined || forceNew) {
this.dbInstances[name] = new SQLiteDBStub(name);
}
return this.dbInstances[name];
}
}

View File

@ -0,0 +1,32 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { makeSingleton } from '@singletons';
import { CoreFilepoolProvider } from '@services/filepool';
/**
* Filepool provider stub.
*/
export class CoreFilepoolProviderStub extends CoreFilepoolProvider {
/**
* @inheritdoc
*/
async getSrcByUrl(siteId: string, fileUrl: string): Promise<string> {
return fileUrl;
}
}
export const CoreFilepoolStub = makeSingleton<CoreFilepoolProviderStub>(CoreFilepoolProvider);

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { makeSingleton } from '@singletons';
import { HttpClient, HttpHandler } from '@angular/common/http';
import { from, Observable } from 'rxjs';
/**
* Http service stub.
*/
export class HttpClientStub extends HttpClient {
constructor() {
super(null as unknown as HttpHandler);
}
/**
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(url: string): Observable<any> {
return from(fetch(url).then(response => response.text()));
}
}
export const HttpStub = makeSingleton<HttpClientStub>(HttpClient);

View File

@ -0,0 +1,43 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import school from '@/assets/storybook/sites/school.json';
import { CoreSiteFixture, CoreSiteStub } from '@/storybook/stubs/classes/site';
import { CoreSitesProvider } from '@services/sites';
import { makeSingleton } from '@singletons';
/**
* Sites provider stub.
*/
export class CoreSitesProviderStub extends CoreSitesProvider {
/**
* @inheritdoc
*/
getRequiredCurrentSite!: () => CoreSiteStub;
/**
* @inheritdoc
*/
stubCurrentSite(fixture?: CoreSiteFixture): CoreSiteStub {
if (!this.currentSite) {
this.currentSite = new CoreSiteStub(fixture ?? school);
}
return this.getRequiredCurrentSite();
}
}
export const CoreSitesStub = makeSingleton<CoreSitesProviderStub>(CoreSitesProvider);