diff --git a/src/assets/storybook/courses.json b/src/assets/storybook/courses.json
new file mode 100644
index 000000000..81b9390e0
--- /dev/null
+++ b/src/assets/storybook/courses.json
@@ -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."}]
diff --git a/src/assets/storybook/geopattern.svg b/src/assets/storybook/geopattern.svg
new file mode 100644
index 000000000..60e7e8a9d
--- /dev/null
+++ b/src/assets/storybook/geopattern.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/assets/storybook/sites/school.json b/src/assets/storybook/sites/school.json
new file mode 100644
index 000000000..1260527ba
--- /dev/null
+++ b/src/assets/storybook/sites/school.json
@@ -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":[]}}
diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts
index 4609c0f04..fc64175b1 100644
--- a/src/core/components/components.module.ts
+++ b/src/core/components/components.module.ts
@@ -64,6 +64,7 @@ import { CoreMessageComponent } from './message/message';
import { CoreGroupSelectorComponent } from './group-selector/group-selector';
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
+import { CoreCourseImageComponent } from '@components/course-image/course-image';
@NgModule({
declarations: [
@@ -75,6 +76,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
CoreContextMenuComponent,
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent,
+ CoreCourseImageComponent,
CoreDownloadRefreshComponent,
CoreDynamicComponent,
CoreEmptyBoxComponent,
@@ -128,6 +130,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
CoreContextMenuComponent,
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent,
+ CoreCourseImageComponent,
CoreDownloadRefreshComponent,
CoreDynamicComponent,
CoreEmptyBoxComponent,
diff --git a/src/core/components/course-image/course-image.html b/src/core/components/course-image/course-image.html
new file mode 100644
index 000000000..6251e1eea
--- /dev/null
+++ b/src/core/components/course-image/course-image.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/core/components/course-image/course-image.scss b/src/core/components/course-image/course-image.scss
new file mode 100644
index 000000000..5bd16990d
--- /dev/null
+++ b/src/core/components/course-image/course-image.scss
@@ -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);
+}
diff --git a/src/core/components/course-image/course-image.ts b/src/core/components/course-image/course-image.ts
new file mode 100644
index 000000000..dc440b81a
--- /dev/null
+++ b/src/core/components/course-image/course-image.ts
@@ -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 {
+ 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);
+ }
+ }
+
+}
diff --git a/src/core/components/stories/components/components.module.ts b/src/core/components/stories/components/components.module.ts
index af6de43cb..e70834d30 100644
--- a/src/core/components/stories/components/components.module.ts
+++ b/src/core/components/stories/components/components.module.ts
@@ -19,9 +19,13 @@ import { StorybookModule } from '@/storybook/storybook.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreComponentsModule } from '@components/components.module';
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({
declarations: [
+ CoreCourseImageCardsPageComponent,
+ CoreCourseImageListPageComponent,
CoreEmptyBoxPageComponent,
CoreEmptyBoxWrapperComponent,
],
diff --git a/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.html b/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.html
new file mode 100644
index 000000000..90514a8e3
--- /dev/null
+++ b/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.html
@@ -0,0 +1,22 @@
+
+
+
+
+
Course Cards
+
+
+
+
+
+
+
+
+
+ {{ course.shortname }}
+
+
+ {{ course.summary }}
+
+
+
+
diff --git a/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.scss b/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.scss
new file mode 100644
index 000000000..23f07c6fd
--- /dev/null
+++ b/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.scss
@@ -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;
+ }
+
+}
diff --git a/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.ts b/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.ts
new file mode 100644
index 000000000..f2b6b0323
--- /dev/null
+++ b/src/core/components/stories/components/course-image-cards-page/course-image-cards-page.ts
@@ -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[] = courses;
+
+}
diff --git a/src/core/components/stories/components/course-image-list-page/course-image-list-page.html b/src/core/components/stories/components/course-image-list-page/course-image-list-page.html
new file mode 100644
index 000000000..ec47e63fb
--- /dev/null
+++ b/src/core/components/stories/components/course-image-list-page/course-image-list-page.html
@@ -0,0 +1,19 @@
+
+
+
+
+
Courses List
+
+
+
+
+
+
+
+
+ {{ course.shortname }}
+
+
+
+
+
diff --git a/src/core/components/stories/components/course-image-list-page/course-image-list-page.ts b/src/core/components/stories/components/course-image-list-page/course-image-list-page.ts
new file mode 100644
index 000000000..cf66b61f0
--- /dev/null
+++ b/src/core/components/stories/components/course-image-list-page/course-image-list-page.ts
@@ -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[] = courses;
+
+}
diff --git a/src/core/components/stories/course-image.stories.ts b/src/core/components/stories/course-image.stories.ts
new file mode 100644
index 000000000..cd919a6fe
--- /dev/null
+++ b/src/core/components/stories/course-image.stories.ts
@@ -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 {
+ 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(({ 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 }));
diff --git a/src/storybook/storybook.module.ts b/src/storybook/storybook.module.ts
index cd8679cd5..f4bf199c6 100644
--- a/src/storybook/storybook.module.ts
+++ b/src/storybook/storybook.module.ts
@@ -20,6 +20,14 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import englishTranslations from '@/assets/lang/en.json';
import { CoreApplicationInitStatus } from '@classes/application-init-status';
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.
export class StaticTranslateLoader extends TranslateLoader {
@@ -45,12 +53,17 @@ export class StaticTranslateLoader extends TranslateLoader {
],
providers: [
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus },
+ { provide: CoreSitesProvider, useClass: CoreSitesProviderStub },
+ { provide: CoreDbProvider, useClass: CoreDbProviderStub },
+ { provide: CoreFilepoolProvider, useClass: CoreFilepoolProviderStub },
+ { provide: HttpClient, useClass: HttpClientStub },
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
Translate.setDefaultLang('en');
Translate.use('en');
+ CoreSitesStub.stubCurrentSite();
},
},
],
diff --git a/src/storybook/stubs/classes/site.ts b/src/storybook/stubs/classes/site.ts
new file mode 100644
index 000000000..12d8f89c2
--- /dev/null
+++ b/src/storybook/stubs/classes/site.ts
@@ -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 = {};
+
+ constructor (fixture: CoreSiteFixture) {
+ super(fixture.id, fixture.info.siteurl, undefined, fixture.info);
+
+ this.stubWSResponse('tool_mobile_get_config', {
+ settings: [],
+ warnings: [],
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ readObservable(wsFunction: string, data: unknown, preSets?: CoreSiteWSPreSets): WSObservable {
+ if (wsFunction in this.wsStubs) {
+ return of(this.wsStubs[wsFunction] as T);
+ }
+
+ return super.readObservable(wsFunction, data, preSets);
+ }
+
+ /**
+ * Prepare as stubbed response for a given WS.
+ *
+ * @param wsFunction WS function.
+ * @param response Response.
+ */
+ stubWSResponse(wsFunction: string, response: T): void {
+ this.wsStubs[wsFunction] = response;
+ }
+
+}
diff --git a/src/storybook/stubs/classes/sqlitedb.ts b/src/storybook/stubs/classes/sqlitedb.ts
new file mode 100644
index 000000000..94d81b5a1
--- /dev/null
+++ b/src/storybook/stubs/classes/sqlitedb.ts
@@ -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 {
+ return new Proxy({
+ executeSql: () => Promise.resolve({ insertId: Math.random().toString() }),
+ }, {}) as unknown as SQLiteObject;
+ }
+
+}
diff --git a/src/storybook/stubs/services/db.ts b/src/storybook/stubs/services/db.ts
new file mode 100644
index 000000000..e74c90bb2
--- /dev/null
+++ b/src/storybook/stubs/services/db.ts
@@ -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];
+ }
+
+}
diff --git a/src/storybook/stubs/services/filepool.ts b/src/storybook/stubs/services/filepool.ts
new file mode 100644
index 000000000..d5c604aad
--- /dev/null
+++ b/src/storybook/stubs/services/filepool.ts
@@ -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 {
+ return fileUrl;
+ }
+
+}
+
+export const CoreFilepoolStub = makeSingleton(CoreFilepoolProvider);
diff --git a/src/storybook/stubs/services/http.ts b/src/storybook/stubs/services/http.ts
new file mode 100644
index 000000000..55e397d72
--- /dev/null
+++ b/src/storybook/stubs/services/http.ts
@@ -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 {
+ return from(fetch(url).then(response => response.text()));
+ }
+
+}
+
+export const HttpStub = makeSingleton(HttpClient);
diff --git a/src/storybook/stubs/services/sites.ts b/src/storybook/stubs/services/sites.ts
new file mode 100644
index 000000000..25f32541a
--- /dev/null
+++ b/src/storybook/stubs/services/sites.ts
@@ -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(CoreSitesProvider);