Skip to content

Commit

Permalink
feat: Added organizations front-end #78 #106
Browse files Browse the repository at this point in the history
  • Loading branch information
georgfrodo committed Mar 5, 2024
1 parent 7762a04 commit 0729e53
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 3 deletions.
36 changes: 36 additions & 0 deletions front-end/src/app/tabs/organizations/organization.page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ 'ORGANIZATIONS.DETAILS' | translate }}</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-grid class="contentGrid">
<ion-row class="ion-justify-content-center">
<ion-col size="12" size-md="6">
<app-organization-card [organization]="organization"></app-organization-card>
</ion-col>
<ion-col size="12" size-md="6">
<ion-list>
<ion-list-header>
<ion-label class="ion-text-center">
<h2>{{ 'ORGANIZATIONS.SPEAKERS' | translate }}</h2>
</ion-label>
</ion-list-header>
<ion-searchbar #searchbar color="light" (ionInput)="filterSpeakers($event.target.value)"></ion-searchbar>
<ion-col *ngIf="!speakers">
<app-speaker-card [preview]="true"></app-speaker-card>
</ion-col>
<ion-col *ngIf="speakers && speakers.length === 0">
<ion-item lines="none" color="white">
<ion-label class="ion-text-center">{{ 'COMMON.NO_ELEMENT_FOUND' | translate }}</ion-label>
</ion-item>
</ion-col>
<ion-col *ngFor="let speaker of speakers">
<app-speaker-card [speaker]="speaker" [preview]="true" (click)="app.goToInTabs(['speakers', speaker.speakerId])"></app-speaker-card>
</ion-col>
</ion-list>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
Empty file.
51 changes: 51 additions & 0 deletions front-end/src/app/tabs/organizations/organization.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { IDEALoadingService, IDEAMessageService } from '@idea-ionic/common';

import { AppService } from 'src/app/app.service';
import { OrganizationsService } from './organizations.service';
import { SpeakersService } from '../speakers/speakers.service';

import { Speaker } from '@models/speaker.model';
import { Organization } from '@models/organization.model';

@Component({
selector: 'app-organization',
templateUrl: './organization.page.html',
styleUrls: ['./organization.page.scss']
})
export class OrganizationPage implements OnInit {
organization: Organization;
speakers: Speaker[];

constructor(
private route: ActivatedRoute,
private loading: IDEALoadingService,
private message: IDEAMessageService,
private _organizations: OrganizationsService,
private _speakers: SpeakersService,
public app: AppService
) {}

async ngOnInit() {
await this.loadData();
}

async loadData() {
try {
await this.loading.show();
const organizationId = this.route.snapshot.paramMap.get('organizationId');
this.organization = await this._organizations.getById(organizationId);
this.speakers = await this._speakers.getList({ organization: this.organization.organizationId, force: true });
} catch (err) {
this.message.error('COMMON.NOT_FOUND');
} finally {
await this.loading.hide();
}
}

async filterSpeakers(search: string = ''): Promise<void> {
this.speakers = await this._speakers.getList({ search, organization: this.organization.organizationId });
}
}
59 changes: 59 additions & 0 deletions front-end/src/app/tabs/organizations/organizationCard.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';

import { IDEATranslationsModule } from '@idea-ionic/common';

import { AppService } from 'src/app/app.service';

import { Organization } from '@models/organization.model';

@Component({
standalone: true,
imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
selector: 'app-organization-card',
template: `
<ion-card *ngIf="organization" color="white">
<ion-img [src]="app.getImageURLByURI(organization.imageURI)"></ion-img>
<ion-card-header>
<ion-card-title>{{ organization.name }}</ion-card-title>
<ion-card-subtitle>
<a [href]="(organization.website.startsWith('http') ? '' : 'http://') + organization.website" target="_blank">{{ organization.website }}</a>
</ion-card-subtitle>
<ion-card-subtitle>
<a [href]="'mailto:' + organization.contactEmail">{{ organization.contactEmail }}</a>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<div class="divDescription" *ngIf="organization.description">
<ion-textarea readonly [rows]="4" [(ngModel)]="organization.description"></ion-textarea>
</div>
</ion-card-content>
</ion-card>
<ion-card *ngIf="!organization" color="white">
<ion-skeleton-text animated style="height: 200px;"></ion-skeleton-text>
<ion-card-header>
<ion-card-title>
<ion-skeleton-text animated style="width: 60%;"></ion-skeleton-text>
</ion-card-title>
<ion-card-subtitle>
<ion-skeleton-text animated style="width: 50%;"></ion-skeleton-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-skeleton-text animated style="width: 80%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 70%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 60%;"></ion-skeleton-text>
</ion-card-content>
</ion-card>
`
})
export class OrganizationCardStandaloneComponent {
@Input() organization: Organization;
@Input() preview: boolean;

constructor(public app: AppService) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { OrganizationsPage } from './organizations.page';
import { OrganizationPage } from './organization.page';

const routes: Routes = [
{ path: '', component: OrganizationsPage },
{ path: ':organizationId', component: OrganizationPage }
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class OrganizationsRoutingModule {}
25 changes: 25 additions & 0 deletions front-end/src/app/tabs/organizations/organizations.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { IDEATranslationsModule } from '@idea-ionic/common';

import { OrganizationsRoutingModule } from './organizations-routing.module';
import { OrganizationPage } from './organization.page';
import { OrganizationsPage } from './organizations.page';
import { OrganizationCardStandaloneComponent } from './organizationCard.component';
import { SpeakerCardStandaloneComponent } from '../speakers/speakerCard.component';

@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
IDEATranslationsModule,
OrganizationsRoutingModule,
OrganizationCardStandaloneComponent,
SpeakerCardStandaloneComponent
],
declarations: [OrganizationsPage, OrganizationPage]
})
export class OrganizationsModule {}
54 changes: 54 additions & 0 deletions front-end/src/app/tabs/organizations/organizations.page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<ion-header>
<ion-toolbar *ngIf="app.isInMobileMode()" color="ideaToolbar">
<ion-segment-button value="list">
{{ 'ORGANIZATIONS.LIST' | translate }}
</ion-segment-button>
</ion-toolbar>
</ion-header>

<ion-content>
<div *ngIf="organizations">
<div *ngIf="app.isInMobileMode(); else desktopContent">
<ion-list lines="inset" style="padding: 0; max-width: 500px; margin: 0 auto;">
<ion-searchbar #searchbar color="light" (ionInput)="filterOrganizations($event.target.value)"></ion-searchbar>
<ion-item color="white" class="noElements" *ngIf="organizations && !organizations.length">
<ion-label>{{ 'COMMON.NO_ELEMENT_FOUND' | translate }}</ion-label>
</ion-item>
<ion-item color="white" *ngIf="!organizations">
<ion-label><ion-skeleton-text animated></ion-skeleton-text></ion-label>
</ion-item>
<ion-item
color="white"
*ngFor="let organization of organizations"
button
detail
(click)="selectOrganization(organization)"
>
<ion-label class="ion-text-wrap">{{ organization.name }}</ion-label>
</ion-item>
</ion-list>
</div>
<ng-template #desktopContent>
<div style="width: 30%; float: left;">
<ion-list lines="inset" style="padding: 0; max-width: 500px; margin: 0 auto;">
<ion-searchbar #searchbar color="light" (ionInput)="filterOrganizations($event.target.value)"></ion-searchbar>
<ion-item color="white" class="noElements" *ngIf="organizations && !organizations.length">
<ion-label>{{ 'COMMON.NO_ELEMENT_FOUND' | translate }}</ion-label>
</ion-item>
<ion-item color="white" *ngIf="!organizations">
<ion-label><ion-skeleton-text animated></ion-skeleton-text></ion-label>
</ion-item>
<ion-item
color="white"
*ngFor="let organization of organizations"
button
detail
(click)="selectOrganization(organization)"
>
<ion-label class="ion-text-wrap">{{ organization.name }}</ion-label>
</ion-item>
</ion-list>
</div>
</ng-template>
</div>
</ion-content>
Empty file.
49 changes: 49 additions & 0 deletions front-end/src/app/tabs/organizations/organizations.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { IonContent } from '@ionic/angular';

import { Organization } from '@models/organization.model';
import { OrganizationsService } from './organizations.service';
import { AppService } from 'src/app/app.service';
import { IDEALoadingService, IDEAMessageService } from '@idea-ionic/common';

@Component({
selector: 'app-organizations',
templateUrl: './organizations.page.html',
styleUrls: ['./organizations.page.scss']
})
export class OrganizationsPage implements OnInit {
@ViewChild(IonContent) content: IonContent;

organizations: Organization[];
filteredOrganizations: Organization[];

constructor(
private loading: IDEALoadingService,
private message: IDEAMessageService,
private _organizations: OrganizationsService,
public app: AppService
) {}

ngOnInit() {
this.loadData();
}

async loadData() {
try {
await this.loading.show();
this.organizations = await this._organizations.getList({});
} catch (error) {
this.message.error('COMMON.OPERATION_FAILED');
} finally {
this.loading.hide();
}
}

async filterOrganizations(search = ''): Promise<void> {
this.organizations = await this._organizations.getList({ search });
}

selectOrganization(organization: Organization) {
this.app.goToInTabs(['organizations', organization.organizationId]);
}
}
65 changes: 65 additions & 0 deletions front-end/src/app/tabs/speakers/speakerCard.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';

import { IDEATranslationsModule } from '@idea-ionic/common';

import { AppService } from 'src/app/app.service';

import { Speaker } from '@models/speaker.model';

@Component({
standalone: true,
imports: [CommonModule, FormsModule, IonicModule, IDEATranslationsModule],
selector: 'app-speaker-card',
template: `
<ng-container *ngIf="speaker; else skeletonTemplate">
<ion-card *ngIf="preview" [color]="preview ? 'white' : ''">
<ion-card-header>
<ion-card-title>{{ speaker.name }}</ion-card-title>
<ion-card-subtitle>{{ speaker.description }}</ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-card *ngIf="!preview" color="white">
<ion-card-header>
<ion-card-subtitle style="font-weight: 300;" class="ion-text-right">
</ion-card-subtitle>
<ion-card-title>{{ speaker.name }}</ion-card-title>
<ion-card-subtitle>{{ speaker.organization }}</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<div class="divDescription" *ngIf="speaker.description">
<ion-textarea readonly [rows]="4" [(ngModel)]="speaker.description"></ion-textarea>
</div>
</ion-card-content>
</ion-card>
</ng-container>
<ng-template #skeletonTemplate>
<ion-card color="white">
<ion-skeleton-text animated style="height: 200px;"></ion-skeleton-text>
<ion-card-header>
<ion-card-title>
<ion-skeleton-text animated style="width: 60%;"></ion-skeleton-text>
</ion-card-title>
<ion-card-subtitle>
<ion-skeleton-text animated style="width: 50%;"></ion-skeleton-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-skeleton-text animated style="width: 80%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 70%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 60%;"></ion-skeleton-text>
</ion-card-content>
</ion-card>
</ng-template>
`
})
export class SpeakerCardStandaloneComponent {
@Input() speaker: Speaker;
@Input() preview: boolean;

constructor(public app: AppService) {}
}
8 changes: 5 additions & 3 deletions front-end/src/app/tabs/speakers/speakers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ export class SpeakersService {

constructor(private api: IDEAApiService) {}

private async loadList(): Promise<void> {
this.speakers = (await this.api.getResource(['speakers'])).map(s => new Speaker(s));
private async loadList(organization?: string): Promise<void> {
this.speakers = (await this.api.getResource(['speakers'], { params: { organization } })).map(s => new Speaker(s));
}

/**
* Get (and optionally filter) the list of speakers.
* Note: it can be paginated.
* Note: it's a slice of the array.
* Note: if organization id is passed, it will filter speakers for that organization.
*/
async getList(options: {
force?: boolean;
withPagination?: boolean;
startPaginationAfterId?: string;
search?: string;
organization?: string;
}): Promise<Speaker[]> {
if (!this.speakers || options.force) await this.loadList();
if (!this.speakers || options.force) await this.loadList(options.organization);
if (!this.speakers) return null;

options.search = options.search ? String(options.search).toLowerCase() : '';
Expand Down
Loading

0 comments on commit 0729e53

Please sign in to comment.