mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 17:12:43 +00:00
feat: add catalog module
This commit is contained in:
committed by
Jovan Krunić
parent
e628f396e2
commit
03084b1c96
@@ -39,6 +39,7 @@ import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
|||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
import {AppRoutingModule} from './app-routing.module';
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
|
import {CatalogModule} from './modules/catalog/catalog.module';
|
||||||
import {ConfigModule} from './modules/config/config.module';
|
import {ConfigModule} from './modules/config/config.module';
|
||||||
import {ConfigProvider} from './modules/config/config.provider';
|
import {ConfigProvider} from './modules/config/config.provider';
|
||||||
import {DataModule} from './modules/data/data.module';
|
import {DataModule} from './modules/data/data.module';
|
||||||
@@ -137,6 +138,7 @@ const providers: Provider[] = [
|
|||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
|
CatalogModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
|
|||||||
53
src/app/modules/catalog/catalog.component.html
Normal file
53
src/app/modules/catalog/catalog.component.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button></ion-back-button>
|
||||||
|
<ion-menu-button></ion-menu-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<!--TODO: read this from the config (menu item title)-->
|
||||||
|
<ion-title>{{ 'catalog.title' | translate | titlecase }}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
<ion-segment
|
||||||
|
(ionChange)="segmentChanged($event)"
|
||||||
|
[value]="selectedSemesterUID"
|
||||||
|
mode="md"
|
||||||
|
>
|
||||||
|
<ion-segment-button
|
||||||
|
*ngFor="let semester of availableSemesters"
|
||||||
|
[value]="semester.uid"
|
||||||
|
>
|
||||||
|
<ion-label>{{ semester.acronym }}</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
</ion-segment>
|
||||||
|
<ion-list *ngIf="catalogs && catalogs.length > 0">
|
||||||
|
<ion-item
|
||||||
|
*ngFor="let catalog of catalogs"
|
||||||
|
button="true"
|
||||||
|
lines="inset"
|
||||||
|
(click)="notifySelect(catalog)"
|
||||||
|
>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ catalog.name }}</h2>
|
||||||
|
<h3>{{ catalog.acronym }}</h3>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<ion-list *ngIf="!catalogs">
|
||||||
|
<stapps-skeleton-list-item *ngFor="let skeleton of [].constructor(10)">
|
||||||
|
</stapps-skeleton-list-item>
|
||||||
|
</ion-list>
|
||||||
|
<ion-grid *ngIf="catalogs && catalogs.length === 0">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<div class="ion-text-center margin-top">
|
||||||
|
<ion-label>
|
||||||
|
{{ 'catalog.detail.EMPTY_SEMESTER' | translate }}
|
||||||
|
</ion-label>
|
||||||
|
</div>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ion-content>
|
||||||
9
src/app/modules/catalog/catalog.component.scss
Normal file
9
src/app/modules/catalog/catalog.component.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
ion-segment-button {
|
||||||
|
max-width: 100%;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-top {
|
||||||
|
margin-top: 20vh;
|
||||||
|
}
|
||||||
159
src/app/modules/catalog/catalog.component.ts
Normal file
159
src/app/modules/catalog/catalog.component.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {Component, OnInit, OnDestroy} from '@angular/core';
|
||||||
|
import {Router, ActivatedRoute} from '@angular/router';
|
||||||
|
import {SCCatalog, SCSemester} from '@openstapps/core';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {CatalogProvider} from './catalog.provider';
|
||||||
|
import {NGXLogger} from 'ngx-logger';
|
||||||
|
import {Location} from '@angular/common';
|
||||||
|
import {DataRoutingService} from '../data/data-routing.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-catalog',
|
||||||
|
templateUrl: './catalog.component.html',
|
||||||
|
styleUrls: ['./catalog.component.scss'],
|
||||||
|
})
|
||||||
|
export class CatalogComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* SCSemester to show
|
||||||
|
*/
|
||||||
|
activeSemester?: SCSemester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UID of the selected SCSemester
|
||||||
|
*/
|
||||||
|
selectedSemesterUID = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available SCSemesters
|
||||||
|
*/
|
||||||
|
availableSemesters: SCSemester[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalogs (SCCatalog) to show
|
||||||
|
*/
|
||||||
|
catalogs: SCCatalog[] | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of all subscriptions to Observables
|
||||||
|
*/
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supercatalog (SCCatalog) to refer to
|
||||||
|
*/
|
||||||
|
superCatalog: SCCatalog;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly route: ActivatedRoute,
|
||||||
|
private readonly catalogProvider: CatalogProvider,
|
||||||
|
private readonly dataRoutingService: DataRoutingService,
|
||||||
|
private readonly logger: NGXLogger,
|
||||||
|
protected router: Router,
|
||||||
|
public location: Location,
|
||||||
|
) {
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.dataRoutingService.itemSelectListener().subscribe(item => {
|
||||||
|
void this.router.navigate(['data-detail', item.uid]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.selectedSemesterUID = this.route.snapshot.paramMap.get('uid') ?? '';
|
||||||
|
void this.fetchCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove subscriptions when the component is removed
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
for (const sub of this.subscriptions) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCatalog() {
|
||||||
|
try {
|
||||||
|
if (this.availableSemesters.length === 0) {
|
||||||
|
await this.fetchSemesters();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.catalogProvider.getCatalogsWith(
|
||||||
|
0,
|
||||||
|
this.superCatalog,
|
||||||
|
this.activeSemester?.uid,
|
||||||
|
);
|
||||||
|
this.catalogs = (response.data as SCCatalog[]).sort((a, b) => {
|
||||||
|
return new Intl.Collator('en', {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: 'accent',
|
||||||
|
}).compare(a.name, b.name);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchSemesters(): Promise<void> {
|
||||||
|
const semesters = await this.catalogProvider.getRelevantSemesters();
|
||||||
|
this.availableSemesters = semesters.slice(0, 3).reverse();
|
||||||
|
|
||||||
|
if (typeof this.activeSemester !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const today = moment().startOf('day').toISOString();
|
||||||
|
this.activeSemester = this.availableSemesters[0];
|
||||||
|
this.activeSemester =
|
||||||
|
this.selectedSemesterUID === ''
|
||||||
|
? this.availableSemesters.find(
|
||||||
|
semester => semester.startDate <= today && semester.endDate > today,
|
||||||
|
)
|
||||||
|
: this.availableSemesters.find(
|
||||||
|
semester => semester.uid === this.selectedSemesterUID,
|
||||||
|
);
|
||||||
|
if (this.activeSemester && this.selectedSemesterUID === '') {
|
||||||
|
this.selectedSemesterUID = this.activeSemester.uid;
|
||||||
|
this.updateLocation(this.selectedSemesterUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
segmentChanged(event: any) {
|
||||||
|
this.updateLocation(event.detail.value as string);
|
||||||
|
this.activeSemester = this.availableSemesters.find(
|
||||||
|
semester => semester.uid === (event.detail.value as string),
|
||||||
|
);
|
||||||
|
delete this.catalogs;
|
||||||
|
void this.fetchCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation(semesterUID: string) {
|
||||||
|
const url = this.router
|
||||||
|
.createUrlTree(['/catalog/', semesterUID])
|
||||||
|
.toString();
|
||||||
|
this.location.go(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit event that a catalog item was selected
|
||||||
|
*/
|
||||||
|
notifySelect(catalog: SCCatalog) {
|
||||||
|
this.dataRoutingService.emitChildEvent(catalog);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/app/modules/catalog/catalog.module.ts
Normal file
47
src/app/modules/catalog/catalog.module.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
|
import {IonicModule} from '@ionic/angular';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {MomentModule} from 'ngx-moment';
|
||||||
|
import {DataModule} from '../data/data.module';
|
||||||
|
import {SettingsProvider} from '../settings/settings.provider';
|
||||||
|
import {CatalogComponent} from './catalog.component';
|
||||||
|
|
||||||
|
const catalogRoutes: Routes = [
|
||||||
|
{path: 'catalog', component: CatalogComponent},
|
||||||
|
{path: 'catalog/:uid', component: CatalogComponent},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog Module
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
declarations: [CatalogComponent],
|
||||||
|
imports: [
|
||||||
|
IonicModule.forRoot(),
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
RouterModule.forChild(catalogRoutes),
|
||||||
|
CommonModule,
|
||||||
|
MomentModule,
|
||||||
|
DataModule,
|
||||||
|
],
|
||||||
|
providers: [SettingsProvider],
|
||||||
|
})
|
||||||
|
export class CatalogModule {}
|
||||||
122
src/app/modules/catalog/catalog.provider.ts
Normal file
122
src/app/modules/catalog/catalog.provider.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {
|
||||||
|
SCCatalogWithoutReferences,
|
||||||
|
SCSearchFilter,
|
||||||
|
SCSearchResponse,
|
||||||
|
SCSemester,
|
||||||
|
SCThingType,
|
||||||
|
} from '@openstapps/core';
|
||||||
|
import {DataProvider} from '../data/data.provider';
|
||||||
|
/**
|
||||||
|
* Service for providing catalog and semester data
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class CatalogProvider {
|
||||||
|
constructor(private readonly dataProvider: DataProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get news messages
|
||||||
|
* TODO: make dates sortable on the backend side and then adjust this method
|
||||||
|
*
|
||||||
|
* @param offset TODO
|
||||||
|
* @param superCatalog TODO
|
||||||
|
* @param semesterUID TODO
|
||||||
|
*/
|
||||||
|
async getCatalogsWith(
|
||||||
|
offset?: number,
|
||||||
|
superCatalog?: SCCatalogWithoutReferences,
|
||||||
|
semesterUID?: string,
|
||||||
|
): Promise<SCSearchResponse> {
|
||||||
|
const filters: SCSearchFilter[] = [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'type',
|
||||||
|
value: 'catalog',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (typeof semesterUID === 'string') {
|
||||||
|
filters.push({
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'academicTerm.uid',
|
||||||
|
value: semesterUID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof superCatalog?.uid === 'string') {
|
||||||
|
filters.push({
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'superCatalog.uid',
|
||||||
|
value: superCatalog.uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filters.push({
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'level',
|
||||||
|
value: '0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataProvider.search({
|
||||||
|
filter: {
|
||||||
|
arguments: {
|
||||||
|
operation: 'and',
|
||||||
|
filters: filters,
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
size: 40,
|
||||||
|
from: offset,
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
type: 'ducet',
|
||||||
|
order: 'asc',
|
||||||
|
arguments: {
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRelevantSemesters(): Promise<SCSemester[]> {
|
||||||
|
const response = await this.dataProvider.search({
|
||||||
|
filter: {
|
||||||
|
arguments: {
|
||||||
|
field: 'type',
|
||||||
|
value: SCThingType.Semester,
|
||||||
|
},
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
size: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (response.data as SCSemester[]).sort((a, b) =>
|
||||||
|
b.startDate.localeCompare(a.startDate, 'en'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ import {OriginInListComponent} from './elements/origin-in-list.component';
|
|||||||
import {CoordinatedSearchProvider} from './coordinated-search.provider';
|
import {CoordinatedSearchProvider} from './coordinated-search.provider';
|
||||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||||
import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
||||||
|
import {SimpleDataListComponent} from './list/simple-data-list.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module for handling data
|
* Module for handling data
|
||||||
@@ -135,8 +136,9 @@ import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
|||||||
SkeletonSimpleCardComponent,
|
SkeletonSimpleCardComponent,
|
||||||
VideoDetailContentComponent,
|
VideoDetailContentComponent,
|
||||||
VideoListItemComponent,
|
VideoListItemComponent,
|
||||||
|
SimpleDataListComponent,
|
||||||
],
|
],
|
||||||
entryComponents: [DataListComponent],
|
entryComponents: [DataListComponent, SimpleDataListComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
DataRoutingModule,
|
DataRoutingModule,
|
||||||
@@ -177,6 +179,7 @@ import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
|||||||
SkeletonListItemComponent,
|
SkeletonListItemComponent,
|
||||||
SkeletonSimpleCardComponent,
|
SkeletonSimpleCardComponent,
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
|
SimpleDataListComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DataModule {}
|
export class DataModule {}
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
<span
|
<span class="ion-hide-sm-up">
|
||||||
>{{ text | slice: 0:size }}<span *ngIf="text.length > size">...</span></span
|
{{ text | slice: 0:size }}
|
||||||
>
|
<span *ngIf="text.length > size" class="ion-hide-sm-up">…</span>
|
||||||
|
</span>
|
||||||
|
<span class="ion-hide-sm-down ion-hide-md-up">
|
||||||
|
{{ text | slice: 0:size * 2 }}
|
||||||
|
<span *ngIf="text.length > size * 2" class="ion-hide-sm-down ion-hide-md-up">
|
||||||
|
…
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="ion-hide-md-down">
|
||||||
|
{{ text | slice: 0:size * 3 }}
|
||||||
|
<span *ngIf="text.length > size * 3" class="ion-hide-md-down">…</span>
|
||||||
|
</span>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A placeholder to show when a list item is being loaded
|
* A placeholder to show when a list item is being loaded
|
||||||
@@ -22,4 +22,6 @@ import {Component} from '@angular/core';
|
|||||||
templateUrl: 'skeleton-list-item.html',
|
templateUrl: 'skeleton-list-item.html',
|
||||||
styleUrls: ['skeleton-list-item.scss'],
|
styleUrls: ['skeleton-list-item.scss'],
|
||||||
})
|
})
|
||||||
export class SkeletonListItemComponent {}
|
export class SkeletonListItemComponent {
|
||||||
|
@Input() hideThumbnail = false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-thumbnail slot="start" class="ion-margin-end">
|
<ion-thumbnail *ngIf="!hideThumbnail" slot="start" class="ion-margin-end">
|
||||||
<ion-skeleton-text animated></ion-skeleton-text>
|
<ion-skeleton-text animated></ion-skeleton-text>
|
||||||
</ion-thumbnail>
|
</ion-thumbnail>
|
||||||
<ion-grid>
|
<ion-grid>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {SCThings} from '@openstapps/core';
|
import {SCThings} from '@openstapps/core';
|
||||||
import {ceil} from 'lodash-es';
|
|
||||||
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +71,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Items that display the skeleton list
|
* Items that display the skeleton list
|
||||||
*/
|
*/
|
||||||
skeletonItems: number[];
|
skeletonItems: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of all subscriptions to Observables
|
* Array of all subscriptions to Observables
|
||||||
@@ -84,12 +83,10 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Calculate how many items would fill the screen
|
* Calculate how many items would fill the screen
|
||||||
*/
|
*/
|
||||||
@HostListener('window.resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
calcSkeletonItems() {
|
calcSkeletonItems() {
|
||||||
const itemHeight = 122;
|
const itemHeight = 40;
|
||||||
this.skeletonItems = Array.from({
|
this.skeletonItems = Math.ceil(window.innerHeight / itemHeight);
|
||||||
length: ceil(window.innerHeight / itemHeight),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ion-list [style.display]="items ? 'none' : 'block'">
|
<ion-list [style.display]="items ? 'none' : 'block'">
|
||||||
<stapps-skeleton-list-item
|
<stapps-skeleton-list-item
|
||||||
*ngFor="let skeleton of skeletonItems"
|
[hideThumbnail]="singleType"
|
||||||
|
*ngFor="let skeleton of [].constructor(skeletonItems)"
|
||||||
></stapps-skeleton-list-item>
|
></stapps-skeleton-list-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
cdk-virtual-scroll-viewport {
|
cdk-virtual-scroll-viewport {
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,3 +9,7 @@ cdk-virtual-scroll-viewport {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtual-scroll-expander {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input, OnInit, OnDestroy} from '@angular/core';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {AlertController} from '@ionic/angular';
|
import {AlertController} from '@ionic/angular';
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,7 @@ import {PositionService} from '../../map/position.service';
|
|||||||
templateUrl: 'search-page.html',
|
templateUrl: 'search-page.html',
|
||||||
providers: [ContextMenuService],
|
providers: [ContextMenuService],
|
||||||
})
|
})
|
||||||
export class SearchPageComponent {
|
export class SearchPageComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Api query filter
|
* Api query filter
|
||||||
*/
|
*/
|
||||||
@@ -126,9 +126,7 @@ export class SearchPageComponent {
|
|||||||
protected dataRoutingService: DataRoutingService,
|
protected dataRoutingService: DataRoutingService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected positionService: PositionService,
|
protected positionService: PositionService,
|
||||||
) {
|
) {}
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches items with set query configuration
|
* Fetches items with set query configuration
|
||||||
@@ -230,7 +228,8 @@ export class SearchPageComponent {
|
|||||||
this.contextMenuService.updateContextFilter(facets);
|
this.contextMenuService.updateContextFilter(facets);
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewWillEnter() {
|
ngOnInit() {
|
||||||
|
this.initialize();
|
||||||
this.contextMenuService.setContextSort({
|
this.contextMenuService.setContextSort({
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
reversed: false,
|
reversed: false,
|
||||||
@@ -290,7 +289,7 @@ export class SearchPageComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewWillLeave() {
|
ngOnDestroy() {
|
||||||
for (const subscription of this.subscriptions) {
|
for (const subscription of this.subscriptions) {
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/app/modules/data/list/simple-data-list.component.ts
Normal file
76
src/app/modules/data/list/simple-data-list.component.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {SCThings} from '@openstapps/core';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {DataRoutingService} from '../data-routing.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the list of items
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'stapps-simple-data-list',
|
||||||
|
templateUrl: 'simple-data-list.html',
|
||||||
|
styleUrls: ['simple-data-list.scss'],
|
||||||
|
})
|
||||||
|
export class SimpleDataListComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* All SCThings to display
|
||||||
|
*/
|
||||||
|
@Input() items?: SCThings[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether or not the list is to display SCThings of a single type
|
||||||
|
*/
|
||||||
|
@Input() singleType = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List header
|
||||||
|
*/
|
||||||
|
@Input() listHeader?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Items that display the skeleton list
|
||||||
|
*/
|
||||||
|
skeletonItems = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of all subscriptions to Observables
|
||||||
|
*/
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected router: Router,
|
||||||
|
private readonly dataRoutingService: DataRoutingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.dataRoutingService.itemSelectListener().subscribe(item => {
|
||||||
|
void this.router.navigate(['data-detail', item.uid]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove subscriptions when the component is removed
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
for (const sub of this.subscriptions) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/modules/data/list/simple-data-list.html
Normal file
28
src/app/modules/data/list/simple-data-list.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<ng-container *ngIf="items | async as items; else loading">
|
||||||
|
<ion-list>
|
||||||
|
<ng-container *ngIf="!listHeader; else header"></ng-container>
|
||||||
|
<stapps-data-list-item
|
||||||
|
*ngFor="let item of items"
|
||||||
|
[item]="item"
|
||||||
|
[hideThumbnail]="singleType"
|
||||||
|
></stapps-data-list-item>
|
||||||
|
</ion-list>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loading>
|
||||||
|
<ion-list>
|
||||||
|
<stapps-skeleton-list-item
|
||||||
|
[hideThumbnail]="singleType"
|
||||||
|
*ngFor="let skeleton of [].constructor(skeletonItems)"
|
||||||
|
>
|
||||||
|
</stapps-skeleton-list-item>
|
||||||
|
</ion-list>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #header>
|
||||||
|
<ion-list-header lines="inset">
|
||||||
|
<ion-text color="dark">
|
||||||
|
<h3>
|
||||||
|
{{ listHeader }}
|
||||||
|
</h3>
|
||||||
|
</ion-text>
|
||||||
|
</ion-list-header>
|
||||||
|
</ng-template>
|
||||||
0
src/app/modules/data/list/simple-data-list.scss
Normal file
0
src/app/modules/data/list/simple-data-list.scss
Normal file
@@ -12,19 +12,159 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {
|
||||||
import {SCCatalog} from '@openstapps/core';
|
Component,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ElementRef,
|
||||||
|
HostListener,
|
||||||
|
OnDestroy,
|
||||||
|
OnChanges,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {SCCatalog, SCSearchBooleanFilter} from '@openstapps/core';
|
||||||
|
import {SearchPageComponent} from '../../list/search-page.component';
|
||||||
|
|
||||||
|
enum AccordionButtonState {
|
||||||
|
collapsed = 'chevron-down',
|
||||||
|
expanded = 'chevron-up',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-catalog-detail-content',
|
selector: 'stapps-catalog-detail-content',
|
||||||
templateUrl: 'catalog-detail-content.html',
|
templateUrl: 'catalog-detail-content.html',
|
||||||
|
styleUrls: ['catalog-detail-content.scss'],
|
||||||
})
|
})
|
||||||
export class CatalogDetailContentComponent {
|
export class CatalogDetailContentComponent
|
||||||
|
extends SearchPageComponent
|
||||||
|
implements OnInit, OnDestroy, OnChanges
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* TODO
|
* SCCatalog to display
|
||||||
*/
|
*/
|
||||||
@Input() item: SCCatalog;
|
@Input() item: SCCatalog;
|
||||||
|
|
||||||
|
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
|
||||||
|
|
||||||
|
buttonState = AccordionButtonState.collapsed;
|
||||||
|
|
||||||
|
buttonShown = true;
|
||||||
|
|
||||||
|
descriptionLinesShown: number;
|
||||||
|
|
||||||
|
descriptionLinesTotal: number;
|
||||||
|
|
||||||
|
descriptionPreviewLines = 3;
|
||||||
|
|
||||||
|
descriptionLinesToDisplay = 0;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
if (this.item.description) {
|
||||||
|
this.descriptionLinesToDisplay = this.descriptionPreviewLines;
|
||||||
|
setTimeout(() => this.checkTextElipsis(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.checkTextElipsis();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.pageSize = 100;
|
||||||
|
this.sortQuery = {
|
||||||
|
arguments: {field: 'name'},
|
||||||
|
order: 'asc',
|
||||||
|
type: 'ducet',
|
||||||
|
};
|
||||||
|
|
||||||
|
const subCatalogFilter: SCSearchBooleanFilter = {
|
||||||
|
arguments: {
|
||||||
|
operation: 'and',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'type',
|
||||||
|
value: 'catalog',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'superCatalog.uid',
|
||||||
|
value: this.item.uid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
|
};
|
||||||
|
|
||||||
|
const subEventsFilter: SCSearchBooleanFilter = {
|
||||||
|
arguments: {
|
||||||
|
operation: 'and',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'type',
|
||||||
|
value: 'academic event',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
arguments: {
|
||||||
|
field: 'catalogs.uid',
|
||||||
|
value: this.item.uid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.forcedFilter = {
|
||||||
|
arguments: {
|
||||||
|
filters: [subCatalogFilter, subEventsFilter],
|
||||||
|
operation: 'or',
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
checkTextElipsis() {
|
||||||
|
if (typeof this.accordionTextArea === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const element = this.accordionTextArea.nativeElement as HTMLElement;
|
||||||
|
|
||||||
|
const lineHeight = Number.parseInt(
|
||||||
|
getComputedStyle(element).getPropertyValue('line-height'),
|
||||||
|
);
|
||||||
|
this.descriptionLinesTotal = element?.scrollHeight / lineHeight;
|
||||||
|
this.descriptionLinesShown = element?.offsetHeight / lineHeight;
|
||||||
|
if (this.buttonState === AccordionButtonState.expanded) {
|
||||||
|
this.descriptionLinesToDisplay = this.descriptionLinesTotal;
|
||||||
|
}
|
||||||
|
const isElipsed = element?.offsetHeight < element?.scrollHeight;
|
||||||
|
this.buttonShown =
|
||||||
|
(isElipsed && this.buttonState === AccordionButtonState.collapsed) ||
|
||||||
|
(!isElipsed && this.buttonState === AccordionButtonState.expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDescriptionAccordion() {
|
||||||
|
if (this.descriptionLinesToDisplay > 0) {
|
||||||
|
this.descriptionLinesToDisplay =
|
||||||
|
this.descriptionLinesToDisplay === this.descriptionPreviewLines
|
||||||
|
? this.descriptionLinesTotal
|
||||||
|
: this.descriptionPreviewLines;
|
||||||
|
}
|
||||||
|
this.buttonState =
|
||||||
|
this.buttonState === AccordionButtonState.collapsed
|
||||||
|
? AccordionButtonState.expanded
|
||||||
|
: AccordionButtonState.collapsed;
|
||||||
|
setTimeout(() => this.checkTextElipsis(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
<stapps-simple-card
|
<ion-card>
|
||||||
[title]="'categories' | propertyNameTranslate: item | titlecase"
|
<ion-card-content>
|
||||||
[content]="'categories' | thingTranslate: item"
|
<ion-text color="dark">
|
||||||
|
<h1>
|
||||||
|
{{ 'name' | thingTranslate: item }}
|
||||||
|
</h1>
|
||||||
|
</ion-text>
|
||||||
|
<div *ngIf="item.description">
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
class="text-accordion"
|
||||||
|
[style.-webkit-line-clamp]="descriptionLinesToDisplay"
|
||||||
|
#accordionTextArea
|
||||||
|
>
|
||||||
|
{{ 'description' | thingTranslate: item }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
|
<ion-button
|
||||||
|
expand="full"
|
||||||
|
fill="clear"
|
||||||
|
*ngIf="item.description && buttonShown"
|
||||||
|
(click)="toggleDescriptionAccordion()"
|
||||||
>
|
>
|
||||||
</stapps-simple-card>
|
<ion-icon [name]="buttonState" size="large"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<stapps-simple-data-list
|
||||||
|
id="simple-data-list"
|
||||||
|
[items]="items"
|
||||||
|
[singleType]="true"
|
||||||
|
[listHeader]="'type' | thingTranslate: item | titlecase"
|
||||||
|
></stapps-simple-data-list>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.text-accordion {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col>
|
<ion-col>
|
||||||
<div class="ion-text-wrap">
|
<div class="ion-text-wrap">
|
||||||
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
|
<ion-label>
|
||||||
<p *ngIf="item.description">
|
<h2>
|
||||||
<stapps-long-inline-text
|
{{ 'name' | thingTranslate: item }}
|
||||||
[text]="'description' | thingTranslate: item"
|
</h2>
|
||||||
[size]="80"
|
</ion-label>
|
||||||
></stapps-long-inline-text>
|
<ion-note *ngIf="item.academicTerm">{{
|
||||||
</p>
|
item.academicTerm.name
|
||||||
<p *ngIf="item.academicTerm">{{ item.academicTerm.name }}</p>
|
}}</ion-note>
|
||||||
</div>
|
</div>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export class FavoritesPageComponent extends SearchPageComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ionViewWillEnter() {
|
ionViewWillEnter() {
|
||||||
super.ionViewWillEnter();
|
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.favoritesService.favoritesChanged$.subscribe(_favoritesMap => {
|
this.favoritesService.favoritesChanged$.subscribe(_favoritesMap => {
|
||||||
this.fetchAndUpdateItems();
|
this.fetchAndUpdateItems();
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"page": {
|
"page": {
|
||||||
"TITLE": "Karte",
|
"TITLE": "Karte",
|
||||||
"search": {
|
"search": {
|
||||||
"PLACEHOLDER": "Finde Gebäude, Points of Interest, Mensen und Cafés ..."
|
"PLACEHOLDER": "Gebäude, Points of Interest, Mensen und mehr"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"SHOW_LIST": "Liste ansehen",
|
"SHOW_LIST": "Liste ansehen",
|
||||||
@@ -110,6 +110,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"catalog": {
|
||||||
|
"title": "Vorlesungsverzeichnis",
|
||||||
|
"detail": {
|
||||||
|
"EMPTY_SEMESTER": "Keine Verzeichnisdaten für das ausgewählte Semester vorhanden"
|
||||||
|
}
|
||||||
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"context": {
|
"context": {
|
||||||
"title": "Kontext Menü",
|
"title": "Kontext Menü",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"page": {
|
"page": {
|
||||||
"TITLE": "Map",
|
"TITLE": "Map",
|
||||||
"search": {
|
"search": {
|
||||||
"PLACEHOLDER": "Find buildings, points of interests, canteens and cafes ..."
|
"PLACEHOLDER": "Buildings, points of interests, canteens and more"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"SHOW_LIST": "Show list",
|
"SHOW_LIST": "Show list",
|
||||||
@@ -110,6 +110,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"catalog": {
|
||||||
|
"title": "course catalog",
|
||||||
|
"detail": {
|
||||||
|
"EMPTY_SEMESTER": "No catalog data available for selected semester"
|
||||||
|
}
|
||||||
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"context": {
|
"context": {
|
||||||
"title": "context menu",
|
"title": "context menu",
|
||||||
|
|||||||
Reference in New Issue
Block a user