Refactor explore and profile component code

This commit is contained in:
Milad Raeisi
2024-09-17 18:16:19 +04:00
parent b9b3a0e4ea
commit aac7ec8355
7 changed files with 273 additions and 126 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -0,0 +1,8 @@
export interface Project {
projectIdentifier: string;
nostrPubKey: string;
displayName?: string;
about?: string;
picture?: string;
banner?:string
}

View File

@@ -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<void> {
@@ -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']);
}

View File

@@ -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<any>(null);
private projectsSubject = new BehaviorSubject<Project[]>([]);
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<Project[]> {
return this.projectsSubject.asObservable();
}
// Save new project and notify subscribers
async saveProject(project: Project): Promise<void> {
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<Project | null> {
try {
const project = await this.projectsStore.getItem<Project>(projectIdentifier);
return project || null;
} catch (error) {
console.error(`Error getting project ${projectIdentifier} from IndexedDB:`, error);
return null;
}
}
async getAllProjects(): Promise<Project[]> {
try {
const projects: Project[] = [];
await this.projectsStore.iterate<Project, void>((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<void> {
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<ProjectStats | null> {
try {
const stats = await this.projectStatsStore.getItem<ProjectStats>(projectIdentifier);
return stats || null;
} catch (error) {
console.error(`Error getting project stats for ${projectIdentifier} from IndexedDB:`, error);
return null;
}
}
getMetadataStream(): Observable<any> {
return this.metadataSubject.asObservable();
@@ -25,7 +114,7 @@ export class IndexedDBService {
async getUserMetadata(pubkey: string): Promise<any | null> {
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<void> {
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<void> {
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<void> {
private async loadAllProjectsFromDB(): Promise<void> {
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<ProjectStats, void>((value, key) => {
statsMap[key] = value;
});
return statsMap;
} catch (error) {
console.error('Error getting all project stats from IndexedDB:', error);
return {};
}
}
private async loadAllProjectStatsFromDB(): Promise<void> {
try {
const stats = await this.getAllProjectStats();
this.projectStatsSubject.next(stats);
} catch (error) {
console.error('Error loading project stats from IndexedDB:', error);
}
}
async getAllUsers(): Promise<any[]> {
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<void> {
try {
await this.userStore.clear();
this.metadataSubject.next(null);
} catch (error) {
console.error('Error clearing all metadata:', error);
}
}
}

View File

@@ -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<Project[]> {
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<Project[]>(url, { observe: 'response' })
.toPromise();
const response = await this.http.get<Project[]>(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<ProjectStats> {
@@ -106,29 +121,57 @@ export class ProjectsService {
const url = `${indexerUrl}api/query/Angor/projects/${projectIdentifier}/stats`;
return this.http.get<ProjectStats>(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<ProjectStats | null> {
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<Project> {
const indexerUrl = this.indexerService.getPrimaryIndexer(this.selectedNetwork);
const url = `${indexerUrl}api/query/Angor/projects/${projectIdentifier}`;
return this.http.get<Project>(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<Project | null> {
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<Project[]> {
return this.indexedDBService.getAllProjects();
}
async getProjectStatsFromDB(projectIdentifier: string): Promise<ProjectStats | null> {
return this.indexedDBService.getProjectStats(projectIdentifier);
}
getProjects(): Project[] {
return this.projects;
}

View File

@@ -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<any[]>([]); // Stream for projects updates
private projects: Project[] = [];
private projectsSubject = new BehaviorSubject<Project[]>([]);
/**
* 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);
}
}