From aac7ec83551fbb35a2b6bbdc0bdbb0b9bea6c26e Mon Sep 17 00:00:00 2001 From: Milad Raeisi Date: Tue, 17 Sep 2024 18:16:19 +0400 Subject: [PATCH] Refactor explore and profile component code --- .../components/explore/explore.component.ts | 31 ++-- .../components/profile/profile.component.ts | 3 - src/app/interface/project.interface.ts | 8 + src/app/layout/common/user/user.component.ts | 39 ++--- src/app/services/indexed-db.service.ts | 150 ++++++++++++++++-- src/app/services/projects.service.ts | 133 ++++++++++------ src/app/services/state.service.ts | 35 ++-- 7 files changed, 273 insertions(+), 126 deletions(-) create mode 100644 src/app/interface/project.interface.ts diff --git a/src/app/components/explore/explore.component.ts b/src/app/components/explore/explore.component.ts index 045db3b..e9c2342 100644 --- a/src/app/components/explore/explore.component.ts +++ b/src/app/components/explore/explore.component.ts @@ -19,15 +19,7 @@ import { NgClass, PercentPipe, I18nPluralPipe, CommonModule } from '@angular/com import { MetadataService } from 'app/services/metadata.service'; import { Subject, takeUntil } from 'rxjs'; import { IndexedDBService } from 'app/services/indexed-db.service'; - -interface Project { - projectIdentifier: string; - nostrPubKey: string; - displayName?: string; - about?: string; - picture?: string; - banner?:string -} +import { Project } from 'app/interface/project.interface'; @Component({ selector: 'explore', @@ -54,22 +46,26 @@ export class ExploreComponent implements OnInit, OnDestroy { private router: Router, private stateService: StateService, private metadataService: MetadataService, - private _indexedDBService: IndexedDBService, - private _changeDetectorRef: ChangeDetectorRef + private indexedDBService: IndexedDBService, + private changeDetectorRef: ChangeDetectorRef ) {} ngOnInit(): void { this.projects = this.stateService.getProjects(); this.filteredProjects = [...this.projects]; + if (this.projects.length === 0) { this.loadProjects(); } else { this.loading = false; - - this.projects.forEach(project => this.subscribeToProjectMetadata(project)); + this.projects.forEach(project => { + if (!project.displayName || !project.about) { + this.loadMetadataForProject(project); + } + }); } - this._indexedDBService.getMetadataStream() + this.indexedDBService.getMetadataStream() .pipe(takeUntil(this._unsubscribeAll)) .subscribe((updatedMetadata) => { if (updatedMetadata) { @@ -81,6 +77,7 @@ export class ExploreComponent implements OnInit, OnDestroy { }); } + loadProjects(): void { if (this.loading || this.errorMessage === 'No more projects found') return; @@ -103,12 +100,12 @@ export class ExploreComponent implements OnInit, OnDestroy { this.projects.forEach(project => this.subscribeToProjectMetadata(project)); } this.loading = false; - this._changeDetectorRef.detectChanges(); + this.changeDetectorRef.detectChanges(); }).catch((error: any) => { console.error('Error fetching projects:', error); this.errorMessage = 'Error fetching projects. Please try again later.'; this.loading = false; - this._changeDetectorRef.detectChanges(); + this.changeDetectorRef.detectChanges(); }); } @@ -142,7 +139,7 @@ export class ExploreComponent implements OnInit, OnDestroy { } this.filteredProjects = [...this.projects]; - this._changeDetectorRef.detectChanges(); + this.changeDetectorRef.detectChanges(); } subscribeToProjectMetadata(project: Project): void { diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts index 26190bf..fbc683c 100644 --- a/src/app/components/profile/profile.component.ts +++ b/src/app/components/profile/profile.component.ts @@ -17,7 +17,6 @@ import { MatMenuModule } from '@angular/material/menu'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Router, RouterLink } from '@angular/router'; import { AngorCardComponent } from '@angor/components/card'; -import { AngorConfigService } from '@angor/services/config'; import { SignerService } from 'app/services/signer.service'; import { MetadataService } from 'app/services/metadata.service'; import { Subject, takeUntil } from 'rxjs'; @@ -53,8 +52,6 @@ export class ProfileComponent implements OnInit, OnDestroy { constructor( private _changeDetectorRef: ChangeDetectorRef, - private _router: Router, - private _angorConfigService: AngorConfigService, private _metadataService: MetadataService, private _signerService: SignerService, private _indexedDBService: IndexedDBService diff --git a/src/app/interface/project.interface.ts b/src/app/interface/project.interface.ts new file mode 100644 index 0000000..8f2688a --- /dev/null +++ b/src/app/interface/project.interface.ts @@ -0,0 +1,8 @@ +export interface Project { + projectIdentifier: string; + nostrPubKey: string; + displayName?: string; + about?: string; + picture?: string; + banner?:string + } diff --git a/src/app/layout/common/user/user.component.ts b/src/app/layout/common/user/user.component.ts index d68bdcc..0780e03 100644 --- a/src/app/layout/common/user/user.component.ts +++ b/src/app/layout/common/user/user.component.ts @@ -46,6 +46,7 @@ export class UserComponent implements OnInit, OnDestroy { theme: string; themes: Themes; + constructor( private _changeDetectorRef: ChangeDetectorRef, private _router: Router, @@ -57,18 +58,6 @@ export class UserComponent implements OnInit, OnDestroy { ngOnInit(): void { this.loadUserProfile(); - - - this._indexedDBService.getMetadataStream() - .pipe(takeUntil(this._unsubscribeAll)) - .subscribe((updatedMetadata) => { - if (updatedMetadata && updatedMetadata.pubkey === this.user?.pubkey) { - this.metadata = updatedMetadata.metadata; - this._changeDetectorRef.detectChanges(); - } - }); - - this._angorConfigService.config$ .pipe(takeUntil(this._unsubscribeAll)) .subscribe((config: AngorConfig) => { @@ -76,6 +65,23 @@ export class UserComponent implements OnInit, OnDestroy { this.config = config; this._changeDetectorRef.detectChanges(); }); + this.loadUserProfile(); + + this._indexedDBService.getMetadataStream() + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((updatedMetadata) => { + if (updatedMetadata && updatedMetadata.pubkey === this.user?.pubkey) { + this.metadata = updatedMetadata.metadata; + this._changeDetectorRef.detectChanges(); + } + }); + } + + + + ngOnDestroy(): void { + this._unsubscribeAll.next(null); + this._unsubscribeAll.complete(); } private async loadUserProfile(): Promise { @@ -86,10 +92,11 @@ export class UserComponent implements OnInit, OnDestroy { if (!publicKey) { this.errorMessage = 'No public key found. Please log in again.'; this.isLoading = false; - this._changeDetectorRef.markForCheck(); + this._changeDetectorRef.detectChanges(); return; } + this.user = { pubkey: publicKey }; try { @@ -112,17 +119,13 @@ export class UserComponent implements OnInit, OnDestroy { } catch (error) { console.error('Failed to load profile data:', error); this.errorMessage = 'Failed to load profile data. Please try again later.'; + this._changeDetectorRef.detectChanges(); } finally { this.isLoading = false; this._changeDetectorRef.detectChanges(); } } - ngOnDestroy(): void { - this._unsubscribeAll.next(null); - this._unsubscribeAll.complete(); - } - logout(): void { this._router.navigate(['/logout']); } diff --git a/src/app/services/indexed-db.service.ts b/src/app/services/indexed-db.service.ts index 63f525e..4d8f8ac 100644 --- a/src/app/services/indexed-db.service.ts +++ b/src/app/services/indexed-db.service.ts @@ -1,23 +1,112 @@ import { Injectable } from '@angular/core'; import localForage from 'localforage'; import { BehaviorSubject, Observable } from 'rxjs'; +import { Project, ProjectStats } from './projects.service'; @Injectable({ providedIn: 'root', }) export class IndexedDBService { private metadataSubject = new BehaviorSubject(null); + private projectsSubject = new BehaviorSubject([]); + private projectStatsSubject = new BehaviorSubject<{ [key: string]: ProjectStats }>({}); + + private userStore: LocalForage; + private projectsStore: LocalForage; + private projectStatsStore: LocalForage; constructor() { - localForage.config({ + + this.userStore = localForage.createInstance({ driver: localForage.INDEXEDDB, - name: 'user-database', + name: 'angor-hub', version: 1.0, storeName: 'users', description: 'Store for user metadata', }); + + this.projectsStore = localForage.createInstance({ + driver: localForage.INDEXEDDB, + name: 'angor-hub', + version: 1.0, + storeName: 'projects', + description: 'Store for projects', + }); + + this.projectStatsStore = localForage.createInstance({ + driver: localForage.INDEXEDDB, + name: 'angor-hub', + version: 1.0, + storeName: 'projectStats', + description: 'Store for project statistics', + }); + + this.loadAllProjectsFromDB(); + this.loadAllProjectStatsFromDB(); } + // Returns projects as observable + getProjectsObservable(): Observable { + return this.projectsSubject.asObservable(); + } + + // Save new project and notify subscribers + async saveProject(project: Project): Promise { + try { + await this.projectsStore.setItem(project.projectIdentifier, project); + const updatedProjects = await this.getAllProjects(); + this.projectsSubject.next(updatedProjects); + } catch (error) { + console.error(`Error saving project ${project.projectIdentifier} to IndexedDB:`, error); + } + } + + async getProject(projectIdentifier: string): Promise { + try { + const project = await this.projectsStore.getItem(projectIdentifier); + return project || null; + } catch (error) { + console.error(`Error getting project ${projectIdentifier} from IndexedDB:`, error); + return null; + } + } + + async getAllProjects(): Promise { + try { + const projects: Project[] = []; + await this.projectsStore.iterate((value) => { + projects.push(value); + }); + return projects; + } catch (error) { + console.error('Error getting all projects from IndexedDB:', error); + return []; + } + } + + getProjectStatsObservable(): Observable<{ [key: string]: ProjectStats }> { + return this.projectStatsSubject.asObservable(); + } + + async saveProjectStats(projectIdentifier: string, stats: ProjectStats): Promise { + try { + await this.projectStatsStore.setItem(projectIdentifier, stats); + const updatedStats = await this.getAllProjectStats(); + this.projectStatsSubject.next(updatedStats); + } catch (error) { + console.error(`Error saving project stats for ${projectIdentifier} to IndexedDB:`, error); + } + } + + async getProjectStats(projectIdentifier: string): Promise { + try { + const stats = await this.projectStatsStore.getItem(projectIdentifier); + return stats || null; + } catch (error) { + console.error(`Error getting project stats for ${projectIdentifier} from IndexedDB:`, error); + return null; + } + } getMetadataStream(): Observable { return this.metadataSubject.asObservable(); @@ -25,7 +114,7 @@ export class IndexedDBService { async getUserMetadata(pubkey: string): Promise { try { - const metadata = await localForage.getItem(pubkey); + const metadata = await this.userStore.getItem(pubkey); return metadata; } catch (error) { console.error('Error getting metadata from IndexedDB:', error); @@ -35,7 +124,7 @@ export class IndexedDBService { async saveUserMetadata(pubkey: string, metadata: any): Promise { try { - await localForage.setItem(pubkey, metadata); + await this.userStore.setItem(pubkey, metadata); this.metadataSubject.next({ pubkey, metadata }); } catch (error) { console.error('Error saving metadata to IndexedDB:', error); @@ -44,36 +133,63 @@ export class IndexedDBService { async removeUserMetadata(pubkey: string): Promise { try { - await localForage.removeItem(pubkey); + await this.userStore.removeItem(pubkey); this.metadataSubject.next({ pubkey, metadata: null }); } catch (error) { console.error('Error removing metadata from IndexedDB:', error); } } - async clearAllMetadata(): Promise { + private async loadAllProjectsFromDB(): Promise { try { - await localForage.clear(); - this.metadataSubject.next(null); + const projects = await this.getAllProjects(); + this.projectsSubject.next(projects); } catch (error) { - console.error('Error clearing all metadata:', error); + console.error('Error loading projects from IndexedDB:', error); + } + } + + async getAllProjectStats(): Promise<{ [key: string]: ProjectStats }> { + try { + const statsMap: { [key: string]: ProjectStats } = {}; + await this.projectStatsStore.iterate((value, key) => { + statsMap[key] = value; + }); + return statsMap; + } catch (error) { + console.error('Error getting all project stats from IndexedDB:', error); + return {}; + } + } + + private async loadAllProjectStatsFromDB(): Promise { + try { + const stats = await this.getAllProjectStats(); + this.projectStatsSubject.next(stats); + } catch (error) { + console.error('Error loading project stats from IndexedDB:', error); } } async getAllUsers(): Promise { try { - const allKeys = await localForage.keys(); - const users = []; - for (const key of allKeys) { - const user = await localForage.getItem(key); - if (user) { - users.push(user); - } - } + const users: any[] = []; + await this.userStore.iterate((value) => { + users.push(value); + }); return users; } catch (error) { console.error('Error getting all users from IndexedDB:', error); return []; } } + + async clearAllMetadata(): Promise { + try { + await this.userStore.clear(); + this.metadataSubject.next(null); + } catch (error) { + console.error('Error clearing all metadata:', error); + } + } } diff --git a/src/app/services/projects.service.ts b/src/app/services/projects.service.ts index 4d99897..3c0e927 100644 --- a/src/app/services/projects.service.ts +++ b/src/app/services/projects.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { catchError } from 'rxjs/operators'; import { of, Observable } from 'rxjs'; import { IndexerService } from './indexer.service'; +import { IndexedDBService } from './indexed-db.service'; export interface Project { founderKey: string; @@ -26,7 +27,7 @@ export interface ProjectStats { }) export class ProjectsService { private offset = 0; - private limit = 45; + private limit = 21; private totalProjects = 0; private loading = false; private projects: Project[] = []; @@ -36,8 +37,8 @@ export class ProjectsService { constructor( private http: HttpClient, - private indexerService: IndexerService - + private indexerService: IndexerService, + private indexedDBService: IndexedDBService ) { this.loadNetwork(); } @@ -45,60 +46,74 @@ export class ProjectsService { loadNetwork() { this.selectedNetwork = this.indexerService.getNetwork(); } + async fetchProjects(): Promise { if (this.loading || this.noMoreProjects) { - return []; + return []; } this.loading = true; const indexerUrl = this.indexerService.getPrimaryIndexer(this.selectedNetwork); const url = this.totalProjectsFetched - ? `${indexerUrl}api/query/Angor/projects?offset=${this.offset}&limit=${this.limit}` - : `${indexerUrl}api/query/Angor/projects?limit=${this.limit}`; + ? `${indexerUrl}api/query/Angor/projects?offset=${this.offset}&limit=${this.limit}` + : `${indexerUrl}api/query/Angor/projects?limit=${this.limit}`; try { - const response = await this.http - .get(url, { observe: 'response' }) - .toPromise(); + const response = await this.http.get(url, { observe: 'response' }).toPromise(); - if (!this.totalProjectsFetched && response && response.headers) { - const paginationTotal = response.headers.get('pagination-total'); - this.totalProjects = paginationTotal ? +paginationTotal : 0; - this.totalProjectsFetched = true; + if (!this.totalProjectsFetched && response && response.headers) { + const paginationTotal = response.headers.get('pagination-total'); + this.totalProjects = paginationTotal ? +paginationTotal : 0; + this.totalProjectsFetched = true; + this.offset = Math.max(this.totalProjects - this.limit, 0); + } - this.offset = Math.max(this.totalProjects - this.limit, 0); - } + const newProjects = response?.body || []; - const newProjects = response?.body || []; + if (!newProjects.length) { + this.noMoreProjects = true; + return []; + } - if (!newProjects || newProjects.length === 0) { - this.noMoreProjects = true; - return []; - } else { const uniqueNewProjects = newProjects.filter( - (newProject) => - !this.projects.some( - (existingProject) => - existingProject.projectIdentifier === newProject.projectIdentifier + newProject => !this.projects.some( + existingProject => existingProject.projectIdentifier === newProject.projectIdentifier ) ); - if (uniqueNewProjects.length > 0) { - this.projects = [...this.projects, ...uniqueNewProjects]; - this.offset = Math.max(this.offset - this.limit, 0); - return uniqueNewProjects; - } else { - this.noMoreProjects = true; - return []; + if (!uniqueNewProjects.length) { + this.noMoreProjects = true; + return []; } - } + + const saveProjectsPromises = uniqueNewProjects.map(async project => { + await this.indexedDBService.saveProject(project); + }); + + const projectDetailsPromises = uniqueNewProjects.map(async project => { + try { + const projectDetails = await this.fetchProjectDetails(project.projectIdentifier).toPromise(); + project.totalInvestmentsCount = projectDetails.totalInvestmentsCount; + return project; + } catch (error) { + console.error(`Error fetching details for project ${project.projectIdentifier}:`, error); + return project; + } + }); + + await Promise.all([...saveProjectsPromises, ...projectDetailsPromises]); + + this.projects = [...this.projects, ...uniqueNewProjects]; + this.offset = Math.max(this.offset - this.limit, 0); + + return uniqueNewProjects; } catch (error) { - console.error('Error fetching projects:', error); - return []; + console.error('Error fetching projects:', error); + return []; } finally { - this.loading = false; + this.loading = false; } - } +} fetchProjectStats(projectIdentifier: string): Observable { @@ -106,29 +121,57 @@ export class ProjectsService { const url = `${indexerUrl}api/query/Angor/projects/${projectIdentifier}/stats`; return this.http.get(url).pipe( catchError((error) => { - console.error( - `Error fetching stats for project ${projectIdentifier}:`, - error - ); + console.error(`Error fetching stats for project ${projectIdentifier}:`, error); return of({} as ProjectStats); }) ); } + async fetchAndSaveProjectStats(projectIdentifier: string): Promise { + try { + const stats = await this.fetchProjectStats(projectIdentifier).toPromise(); + if (stats) { + await this.indexedDBService.saveProjectStats(projectIdentifier, stats); + } + return stats; + } catch (error) { + console.error(`Error fetching and saving stats for project ${projectIdentifier}:`, error); + return null; + } + } + fetchProjectDetails(projectIdentifier: string): Observable { const indexerUrl = this.indexerService.getPrimaryIndexer(this.selectedNetwork); const url = `${indexerUrl}api/query/Angor/projects/${projectIdentifier}`; return this.http.get(url).pipe( catchError((error) => { - console.error( - `Error fetching details for project ${projectIdentifier}:`, - error - ); + console.error(`Error fetching details for project ${projectIdentifier}:`, error); return of({} as Project); }) ); } + async fetchAndSaveProjectDetails(projectIdentifier: string): Promise { + try { + const project = await this.fetchProjectDetails(projectIdentifier).toPromise(); + if (project) { + await this.indexedDBService.saveProject(project); + } + return project; + } catch (error) { + console.error(`Error fetching and saving details for project ${projectIdentifier}:`, error); + return null; + } + } + + async getAllProjectsFromDB(): Promise { + return this.indexedDBService.getAllProjects(); + } + + async getProjectStatsFromDB(projectIdentifier: string): Promise { + return this.indexedDBService.getProjectStats(projectIdentifier); + } + getProjects(): Project[] { return this.projects; } diff --git a/src/app/services/state.service.ts b/src/app/services/state.service.ts index 124d0a8..72276d5 100644 --- a/src/app/services/state.service.ts +++ b/src/app/services/state.service.ts @@ -1,46 +1,32 @@ import { Injectable } from '@angular/core'; +import { Project } from 'app/interface/project.interface'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class StateService { - private projects: any[] = []; - private projectsSubject = new BehaviorSubject([]); // Stream for projects updates + private projects: Project[] = []; + private projectsSubject = new BehaviorSubject([]); - /** - * Returns an observable for project updates. - */ getProjectsObservable() { return this.projectsSubject.asObservable(); } - /** - * Sets the projects and emits the updated list. - */ - setProjects(projects: any[]): void { + setProjects(projects: Project[]): void { this.projects = projects; - this.projectsSubject.next(this.projects); // Emit updated projects + this.projectsSubject.next(this.projects); } - /** - * Returns the current list of projects. - */ - getProjects(): any[] { + getProjects(): Project[] { return this.projects; } - /** - * Returns a boolean indicating whether there are projects. - */ hasProjects(): boolean { return this.projects.length > 0; } - /** - * Updates or adds a project based on its nostrPubKey. - */ - updateProject(project: any): void { + updateProject(project: Project): void { const index = this.projects.findIndex(p => p.nostrPubKey === project.nostrPubKey); if (index > -1) { @@ -49,13 +35,10 @@ export class StateService { this.projects.push(project); } - this.projectsSubject.next(this.projects); // Emit updated projects + this.projectsSubject.next(this.projects); } - /** - * Returns a specific project by its nostrPubKey. - */ - getProjectByPubKey(nostrPubKey: string): any | undefined { + getProjectByPubKey(nostrPubKey: string): Project | undefined { return this.projects.find(p => p.nostrPubKey === nostrPubKey); } }