Load all contact

This commit is contained in:
Milad Raeisi
2024-09-20 09:51:49 +04:00
parent 0aece4331b
commit 6a44634ece
20 changed files with 91 additions and 1546 deletions

View File

@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, Subject, throwError, of } from 'rxjs';
import { BehaviorSubject, Observable, Subject, throwError, of, Subscriber } from 'rxjs';
import { filter, map, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { DomSanitizer } from '@angular/platform-browser';
import { Chat, Contact, Profile } from 'app/components/chat/chat.types';
@@ -78,13 +78,62 @@ export class ChatService implements OnDestroy {
* Fetch all contacts from the server
*/
getContacts(): Observable<Contact[]> {
return this._httpClient.get<Contact[]>('api/apps/chat/contacts').pipe(
tap((contacts: Contact[]) => {
this._contacts.next(contacts);
})
);
return new Observable<Contact[]>((observer) => {
this._indexedDBService.getAllUsers()
.then((cachedContacts: Contact[]) => {
if (cachedContacts.length > 0) {
this._contacts.next(cachedContacts);
observer.next(cachedContacts);
}
const pubkeys = cachedContacts.map(contact => contact.pubKey);
if (pubkeys.length > 0) {
this.subscribeToRealTimeContacts(pubkeys, observer);
}
})
.catch((error) => {
console.error('Error loading cached contacts:', error);
observer.error(error);
});
return () => {
// Cleanup logic if needed (for example, closing subscriptions)
};
});
}
private subscribeToRealTimeContacts(pubkeys: string[], observer: Subscriber<Contact[]>): void {
this._metadataService.fetchMetadataForMultipleKeys(pubkeys)
.then((metadataList: any[]) => {
metadataList.forEach((metadata) => {
const contact = this._contacts.value?.find(c => c.pubKey === metadata.pubkey);
if (contact) {
contact.displayName = metadata.name;
contact.picture = metadata.picture;
contact.about = metadata.about;
} else {
const updatedContacts = [...(this._contacts.value || []), {
pubKey: metadata.pubkey,
displayName: metadata.name,
picture: metadata.picture,
about: metadata.about
}];
this._contacts.next(updatedContacts);
observer.next(updatedContacts);
}
});
observer.next(this._contacts.value || []);
})
.catch((error) => {
console.error('Error fetching metadata for contacts:', error);
observer.error(error);
});
}
/**
* Fetch the profile metadata using the public key and subscribe to real-time updates
*/

View File

@@ -13,30 +13,17 @@ export interface Profile {
}
export interface Contact {
id?: string;
avatar?: string;
pubKey?: string;
name?: string;
username?: string;
picture?: string;
about?: string;
details?: {
emails?: {
email?: string;
label?: string;
}[];
phoneNumbers?: {
country?: string;
phoneNumber?: string;
label?: string;
}[];
title?: string;
company?: string;
birthday?: string;
address?: string;
};
attachments?: {
media?: any[];
docs?: any[];
links?: any[];
};
displayName?: string;
website?: string;
banner?: string;
lud06?: string;
lud16?: string;
nip05?: string;
}
export interface Chat {

View File

@@ -38,7 +38,8 @@
@if (profile.picture) {
<img
class="h-full w-full rounded-full object-cover"
[src]="profile.picture"
[src]="profile.picture || '/images/avatars/avatar-placeholder.png'"
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
alt="Profile picture"
/>
}
@@ -181,14 +182,15 @@
"
></div>
}
@if (chat.contact.avatar) {
@if (chat.contact.picture) {
<img
class="h-full w-full rounded-full object-cover"
[src]="chat.contact.avatar"
alt="Contact avatar"
[src]="chat.contact || '/images/avatars/avatar-placeholder.png'"
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
alt="Contact picture"
/>
}
@if (!chat.contact.avatar) {
@if (!chat.contact.picture) {
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
>

View File

@@ -10,17 +10,18 @@
</div>
<div class="overflow-y-auto">
<!-- Contact avatar & info -->
<!-- Contact picture & info -->
<div class="mt-8 flex flex-col items-center">
<div class="h-40 w-40 rounded-full">
@if (chat.contact.avatar) {
@if (chat.contact.picture) {
<img
class="h-full w-full rounded-full object-cover"
[src]="chat.contact.avatar"
[alt]="'Contact avatar'"
[src]="chat.contact.picture || '/images/avatars/avatar-placeholder.png'"
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
[alt]="'Contact picture'"
/>
}
@if (!chat.contact.avatar) {
@if (!chat.contact.picture) {
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-8xl font-semibold uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
>
@@ -34,62 +35,6 @@
</div>
</div>
<div class="px-7 py-10">
<!-- Media -->
<div class="text-lg font-medium">Media</div>
<div class="mt-4 grid grid-cols-4 gap-1">
@for (media of chat.contact.attachments.media; track media) {
<img class="h-20 rounded object-cover" [src]="media" />
}
</div>
<!-- Details -->
<div class="mt-10 space-y-4">
<div class="mb-3 text-lg font-medium">Details</div>
@if (chat.contact.details.emails.length) {
<div>
<div class="text-secondary font-medium">Email</div>
<div class="">
{{ chat.contact.details.emails[0].email }}
</div>
</div>
}
@if (chat.contact.details.phoneNumbers.length) {
<div>
<div class="text-secondary font-medium">
Phone number
</div>
<div class="">
{{
chat.contact.details.phoneNumbers[0].phoneNumber
}}
</div>
</div>
}
@if (chat.contact.details.title) {
<div>
<div class="text-secondary font-medium">Title</div>
<div class="">{{ chat.contact.details.title }}</div>
</div>
}
@if (chat.contact.details.company) {
<div>
<div class="text-secondary font-medium">Company</div>
<div class="">{{ chat.contact.details.company }}</div>
</div>
}
@if (chat.contact.details.birthday) {
<div>
<div class="text-secondary font-medium">Birthday</div>
<div class="">{{ chat.contact.details.birthday }}</div>
</div>
}
@if (chat.contact.details.address) {
<div>
<div class="text-secondary font-medium">Address</div>
<div class="">{{ chat.contact.details.address }}</div>
</div>
}
</div>
</div>
</div>
</div>

View File

@@ -45,14 +45,15 @@
<div
class="relative flex h-10 w-10 flex-0 items-center justify-center"
>
@if (chat.contact.avatar) {
@if (chat.contact.picture) {
<img
class="h-full w-full rounded-full object-cover"
[src]="chat.contact.avatar"
alt="Contact avatar"
[src]="chat.contact.picture || '/images/avatars/avatar-placeholder.png'"
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
alt="Contact picture"
/>
}
@if (!chat.contact.avatar) {
@if (!chat.contact.picture) {
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
>

View File

@@ -38,14 +38,15 @@
<div
class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full"
>
@if (contact.avatar) {
@if (contact.picture) {
<img
class="h-full w-full object-cover"
[src]="contact.avatar"
alt="Contact avatar"
[src]="contact.picture || '/images/avatars/avatar-placeholder.png'"
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
alt="Contact picture"
/>
}
@if (!contact.avatar) {
@if (!contact.picture) {
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
>

View File

@@ -30,8 +30,9 @@
@if (profile.picture) {
<img
class="h-full w-full rounded-full object-cover"
[src]="profile.picture"
[alt]="'Profile avatar'"
[src]="profile.picture || '/images/avatars/avatar-placeholder.png'"
onerror="this.onerror=null; this.src='/images/avatars/avatar-placeholder.png';"
[alt]="'Profile picture'"
/>
}
@if (!profile.picture) {

View File

@@ -1,168 +0,0 @@
<div class="w-full max-w-3xl">
<!-- Form -->
<form [formGroup]="accountForm">
<!-- Section -->
<div class="w-full">
<div class="text-xl">Profile</div>
<div class="text-secondary">
Following information is publicly displayed, be careful!
</div>
</div>
<div class="mt-8 grid w-full gap-6 sm:grid-cols-4">
<!-- Name -->
<div class="sm:col-span-4">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Name</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:user'"
matPrefix
></mat-icon>
<input [formControlName]="'name'" matInput />
</mat-form-field>
</div>
<!-- Username -->
<div class="sm:col-span-4">
<mat-form-field
class="angor-mat-emphasized-affix w-full"
[subscriptSizing]="'dynamic'"
>
<mat-label>Username</mat-label>
<div class="text-secondary" matPrefix>angortheme.com/</div>
<input [formControlName]="'username'" matInput />
</mat-form-field>
</div>
<!-- Title -->
<div class="sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Title</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:briefcase'"
matPrefix
></mat-icon>
<input [formControlName]="'title'" matInput />
</mat-form-field>
</div>
<!-- Company -->
<div class="sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Company</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:building-office-2'"
matPrefix
></mat-icon>
<input [formControlName]="'company'" matInput />
</mat-form-field>
</div>
<!-- About -->
<div class="sm:col-span-4">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>About</mat-label>
<textarea
matInput
[formControlName]="'about'"
cdkTextareaAutosize
[cdkAutosizeMinRows]="5"
></textarea>
</mat-form-field>
<div class="text-hint mt-1 text-md">
Brief description for your profile. Basic HTML and Emoji are
allowed.
</div>
</div>
</div>
<!-- Divider -->
<div class="my-10 border-t"></div>
<!-- Section -->
<div class="w-full">
<div class="text-xl">Personal Information</div>
<div class="text-secondary">
Communication details in case we want to connect with you. These
will be kept private.
</div>
</div>
<div class="mt-8 grid w-full gap-6 sm:grid-cols-4">
<!-- Email -->
<div class="sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Email</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:envelope'"
matPrefix
></mat-icon>
<input [formControlName]="'email'" matInput />
</mat-form-field>
</div>
<!-- Phone -->
<div class="sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Phone</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:phone'"
matPrefix
></mat-icon>
<input [formControlName]="'phone'" matInput />
</mat-form-field>
</div>
<!-- Country -->
<div class="sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Country</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:map-pin'"
matPrefix
></mat-icon>
<mat-select [formControlName]="'country'">
<mat-option [value]="'usa'">United States</mat-option>
<mat-option [value]="'canada'">Canada</mat-option>
<mat-option [value]="'mexico'">Mexico</mat-option>
<mat-option [value]="'france'">France</mat-option>
<mat-option [value]="'germany'">Germany</mat-option>
<mat-option [value]="'italy'">Italy</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Language -->
<div class="sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Language</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:globe-alt'"
matPrefix
></mat-icon>
<mat-select [formControlName]="'language'">
<mat-option [value]="'english'">English</mat-option>
<mat-option [value]="'french'">French</mat-option>
<mat-option [value]="'spanish'">Spanish</mat-option>
<mat-option [value]="'german'">German</mat-option>
<mat-option [value]="'italian'">Italian</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- Divider -->
<div class="mb-10 mt-11 border-t"></div>
<!-- Actions -->
<div class="flex items-center justify-end">
<button mat-stroked-button type="button">Cancel</button>
<button
class="ml-4"
mat-flat-button
type="button"
[color]="'primary'"
>
Save
</button>
</div>
</form>
</div>

View File

@@ -1,71 +0,0 @@
import { TextFieldModule } from '@angular/cdk/text-field';
import {
ChangeDetectionStrategy,
Component,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormBuilder,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
@Component({
selector: 'settings-account',
templateUrl: './account.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
FormsModule,
ReactiveFormsModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
TextFieldModule,
MatSelectModule,
MatOptionModule,
MatButtonModule,
],
})
export class SettingsAccountComponent implements OnInit {
accountForm: UntypedFormGroup;
/**
* Constructor
*/
constructor(private _formBuilder: UntypedFormBuilder) {}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void {
// Create the form
this.accountForm = this._formBuilder.group({
name: ['Display Name'],
username: ['brianh'],
title: ['Senior Frontend Developer'],
company: ['YXZ Software'],
about: [
"Hey! This is Brian; husband, father and gamer. I'm mostly passionate about bleeding edge tech and chocolate! 🍫",
],
email: ['hughes.brian@mail.com', Validators.email],
phone: ['121-490-33-12'],
country: ['usa'],
language: ['english'],
});
}
}

View File

@@ -1,156 +0,0 @@
<div class="w-full max-w-3xl">
<!-- Form -->
<form [formGroup]="notificationsForm">
<!-- Section -->
<div class="w-full text-xl">Alerts</div>
<div class="mt-8 grid w-full grid-cols-1 gap-6">
<!-- Communication -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer"
(click)="communication.toggle()"
>
<div class="font-medium leading-6">Communication</div>
<div class="text-secondary text-md">
Get news, announcements, and product updates.
</div>
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'communication'"
#communication
>
</mat-slide-toggle>
</div>
<!-- Security -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer"
(click)="securityToggle.toggle()"
>
<div class="font-medium leading-6">Security</div>
<div class="text-secondary text-md">
Get important notifications about your account security.
</div>
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'security'"
#securityToggle
>
</mat-slide-toggle>
</div>
<!-- Meetups -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer"
(click)="meetupsToggle.toggle()"
>
<div class="font-medium leading-6">Meetups</div>
<div class="text-secondary text-md">
Get an email when a Meetup is posted close to my
location.
</div>
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'meetups'"
#meetupsToggle
>
</mat-slide-toggle>
</div>
</div>
<!-- Divider -->
<div class="my-10 border-t"></div>
<!-- Section -->
<div class="w-full text-xl">Account Activity</div>
<div class="mt-8 w-full font-medium">Email me when:</div>
<div class="mt-4 grid w-full grid-cols-1 gap-4">
<!-- Comments -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer leading-6"
(click)="comments.toggle()"
>
someone comments on one of my items
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'comments'"
#comments
>
</mat-slide-toggle>
</div>
<!-- Mention -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer leading-6"
(click)="mention.toggle()"
>
someone mentions me
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'mention'"
#mention
>
</mat-slide-toggle>
</div>
<!-- Follow -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer leading-6"
(click)="follow.toggle()"
>
someone follows me
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'follow'"
#follow
>
</mat-slide-toggle>
</div>
<!-- Inquiry -->
<div class="flex items-center justify-between">
<div
class="flex-auto cursor-pointer leading-6"
(click)="inquiry.toggle()"
>
someone replies to my job posting
</div>
<mat-slide-toggle
class="ml-2"
[color]="'primary'"
[formControlName]="'inquiry'"
#inquiry
>
</mat-slide-toggle>
</div>
</div>
<!-- Divider -->
<div class="my-10 border-t"></div>
<!-- Actions -->
<div class="flex items-center justify-end">
<button mat-stroked-button type="button">Cancel</button>
<button
class="ml-4"
mat-flat-button
type="button"
[color]="'primary'"
>
Save
</button>
</div>
</form>
</div>

View File

@@ -1,56 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormBuilder,
UntypedFormGroup,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
@Component({
selector: 'settings-notifications',
templateUrl: './notifications.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
FormsModule,
ReactiveFormsModule,
MatSlideToggleModule,
MatButtonModule,
],
})
export class SettingsNotificationsComponent implements OnInit {
notificationsForm: UntypedFormGroup;
/**
* Constructor
*/
constructor(private _formBuilder: UntypedFormBuilder) {}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void {
// Create the form
this.notificationsForm = this._formBuilder.group({
communication: [true],
security: [true],
meetups: [false],
comments: [false],
mention: [true],
follow: [true],
inquiry: [true],
});
}
}

View File

@@ -1,171 +0,0 @@
<div class="w-full max-w-3xl">
<!-- Form -->
<form [formGroup]="planBillingForm">
<!-- Section -->
<div class="w-full">
<div class="text-xl">Change your plan</div>
<div class="text-secondary">
Upgrade or downgrade your current plan.
</div>
</div>
<div class="mt-8 grid w-full gap-6 sm:grid-cols-3">
<!-- Plan -->
<div class="sm:col-span-3">
<angor-alert [appearance]="'outline'" [type]="'info'">
Changing the plan will take effect immediately. You will be
charged for the rest of the current month.
</angor-alert>
</div>
<mat-radio-group
class="pointer-events-none invisible absolute h-0 w-0"
[formControlName]="'plan'"
#planRadioGroup="matRadioGroup"
>
@for (plan of plans; track trackByFn($index, plan)) {
<mat-radio-button [value]="plan.value"></mat-radio-button>
}
</mat-radio-group>
@for (plan of plans; track trackByFn($index, plan)) {
<div
class="bg-card relative flex cursor-pointer flex-col items-start justify-start rounded-md p-6 shadow"
[ngClass]="{
'ring ring-inset ring-primary':
planRadioGroup.value === plan.value,
}"
(click)="planRadioGroup.value = plan.value"
>
@if (planRadioGroup.value === plan.value) {
<mat-icon
class="absolute right-0 top-0 mr-3 mt-3 text-primary icon-size-7"
[svgIcon]="'heroicons_solid:check-circle'"
></mat-icon>
}
<div class="font-semibold">{{ plan.label }}</div>
<div class="text-secondary mt-1 whitespace-normal">
{{ plan.details }}
</div>
<div class="flex-auto"></div>
<div class="mt-8 text-lg">
<span>{{
plan.price | currency: 'USD' : 'symbol' : '1.0'
}}</span>
<span class="text-secondary"> / month</span>
</div>
</div>
}
</div>
<!-- Divider -->
<div class="mb-10 mt-12 border-t"></div>
<!-- Section -->
<div class="w-full">
<div class="text-xl">Payment Details</div>
<div class="text-secondary">
Update your billing information. Make sure to set your location
correctly as it could affect your tax rates.
</div>
</div>
<div class="mt-8 grid w-full grid-cols-4 gap-6">
<!-- Card holder -->
<div class="col-span-4">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Card holder</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:user'"
matPrefix
></mat-icon>
<input [formControlName]="'cardHolder'" matInput />
</mat-form-field>
</div>
<!-- Card number -->
<div class="col-span-4 sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Card number</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:credit-card'"
matPrefix
></mat-icon>
<input [formControlName]="'cardNumber'" matInput />
</mat-form-field>
</div>
<!-- Card expiration -->
<div class="col-span-2 sm:col-span-1">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Expiration date</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:calendar'"
matPrefix
></mat-icon>
<input
[formControlName]="'cardExpiration'"
[placeholder]="'MM / YY'"
matInput
/>
</mat-form-field>
</div>
<!-- Card CVC -->
<div class="col-span-2 sm:col-span-1">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>CVC / CVC2</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:lock-closed'"
matPrefix
></mat-icon>
<input [formControlName]="'cardCVC'" matInput />
</mat-form-field>
</div>
<!-- Country -->
<div class="col-span-4 sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Country</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:map-pin'"
matPrefix
></mat-icon>
<mat-select [formControlName]="'country'">
<mat-option [value]="'usa'">United States</mat-option>
<mat-option [value]="'canada'">Canada</mat-option>
<mat-option [value]="'mexico'">Mexico</mat-option>
<mat-option [value]="'france'">France</mat-option>
<mat-option [value]="'germany'">Germany</mat-option>
<mat-option [value]="'italy'">Italy</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- ZIP -->
<div class="col-span-4 sm:col-span-2">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>ZIP / Postal code</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:hashtag'"
matPrefix
></mat-icon>
<input matInput />
</mat-form-field>
</div>
</div>
<!-- Divider -->
<div class="mb-10 mt-11 border-t"></div>
<!-- Actions -->
<div class="flex items-center justify-end">
<button mat-stroked-button type="button">Cancel</button>
<button
class="ml-4"
mat-flat-button
type="button"
[color]="'primary'"
>
Save
</button>
</div>
</form>
</div>

View File

@@ -1,108 +0,0 @@
import { CurrencyPipe, NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormBuilder,
UntypedFormGroup,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { AngorAlertComponent } from '@angor/components/alert';
@Component({
selector: 'settings-plan-billing',
templateUrl: './plan-billing.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
FormsModule,
ReactiveFormsModule,
AngorAlertComponent,
MatRadioModule,
NgClass,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatOptionModule,
MatButtonModule,
CurrencyPipe,
],
})
export class SettingsPlanBillingComponent implements OnInit {
planBillingForm: UntypedFormGroup;
plans: any[];
/**
* Constructor
*/
constructor(private _formBuilder: UntypedFormBuilder) {}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void {
// Create the form
this.planBillingForm = this._formBuilder.group({
plan: ['team'],
cardHolder: ['Display Name'],
cardNumber: [''],
cardExpiration: [''],
cardCVC: [''],
country: ['usa'],
zip: [''],
});
// Setup the plans
this.plans = [
{
value: 'basic',
label: 'BASIC',
details: 'Starter plan for individuals.',
price: '10',
},
{
value: 'team',
label: 'TEAM',
details: 'Collaborate up to 10 people.',
price: '20',
},
{
value: 'enterprise',
label: 'ENTERPRISE',
details: 'For bigger businesses.',
price: '40',
},
];
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any {
return item.id || index;
}
}

View File

@@ -1,123 +0,0 @@
<div class="w-full max-w-3xl">
<!-- Form -->
<form [formGroup]="securityForm">
<!-- Section -->
<div class="w-full">
<div class="text-xl">Change your password</div>
<div class="text-secondary">
You can only change your password twice within 24 hours!
</div>
</div>
<div class="mt-8 grid w-full gap-6 sm:grid-cols-4">
<!-- Current password -->
<div class="sm:col-span-4">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Current password</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:key'"
matPrefix
></mat-icon>
<input
[formControlName]="'currentPassword'"
type="password"
matInput
/>
</mat-form-field>
</div>
<!-- New password -->
<div class="sm:col-span-4">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>New password</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:key'"
matPrefix
></mat-icon>
<input
[formControlName]="'newPassword'"
type="password"
matInput
/>
</mat-form-field>
<div class="text-hint mt-1 text-md">
Minimum 8 characters. Must include numbers, letters and
special characters.
</div>
</div>
</div>
<!-- Divider -->
<div class="my-10 border-t"></div>
<!-- Section -->
<div class="w-full">
<div class="text-xl">Security preferences</div>
<div class="text-secondary">
Keep your account more secure with following preferences.
</div>
</div>
<div class="mt-8 grid w-full gap-6 sm:grid-cols-4">
<!-- 2-step auth -->
<div class="flex items-center justify-between sm:col-span-4">
<div
class="flex-auto cursor-pointer"
(click)="twoStepToggle.toggle()"
>
<div class="font-medium leading-6">
Enable 2-step authentication
</div>
<div class="text-secondary text-md">
Protects you against password theft by requesting an
authentication code via SMS on every login.
</div>
</div>
<mat-slide-toggle
class="ml-4"
[color]="'primary'"
[formControlName]="'twoStep'"
#twoStepToggle
>
</mat-slide-toggle>
</div>
<!-- Ask to change password -->
<div class="flex items-center justify-between sm:col-span-4">
<div
class="flex-auto cursor-pointer"
(click)="askPasswordChangeToggle.toggle()"
>
<div class="font-medium leading-6">
Ask to change password on every 6 months
</div>
<div class="text-secondary text-md">
A simple but an effective way to be protected against
data leaks and password theft.
</div>
</div>
<mat-slide-toggle
class="ml-4"
[color]="'primary'"
[formControlName]="'askPasswordChange'"
#askPasswordChangeToggle
>
</mat-slide-toggle>
</div>
</div>
<!-- Divider -->
<div class="my-10 border-t"></div>
<!-- Actions -->
<div class="flex items-center justify-end">
<button mat-stroked-button type="button">Cancel</button>
<button
class="ml-4"
mat-flat-button
type="button"
[color]="'primary'"
>
Save
</button>
</div>
</form>
</div>

View File

@@ -1,59 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
UntypedFormBuilder,
UntypedFormGroup,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
@Component({
selector: 'settings-security',
templateUrl: './security.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
FormsModule,
ReactiveFormsModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSlideToggleModule,
MatButtonModule,
],
})
export class SettingsSecurityComponent implements OnInit {
securityForm: UntypedFormGroup;
/**
* Constructor
*/
constructor(private _formBuilder: UntypedFormBuilder) {}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void {
// Create the form
this.securityForm = this._formBuilder.group({
currentPassword: [''],
newPassword: [''],
twoStep: [true],
askPasswordChange: [false],
});
}
}

View File

@@ -1,126 +0,0 @@
<div
class="flex w-full min-w-0 flex-col sm:absolute sm:inset-0 sm:overflow-hidden"
>
<mat-drawer-container class="flex-auto sm:h-full">
<!-- Drawer -->
<mat-drawer
class="dark:bg-gray-900 sm:w-96"
[autoFocus]="false"
[mode]="drawerMode"
[opened]="drawerOpened"
#drawer
>
<!-- Header -->
<div class="m-8 mr-6 flex items-center justify-between sm:my-10">
<!-- Title -->
<div
class="text-4xl font-extrabold leading-none tracking-tight"
>
Settings
</div>
<!-- Close button -->
<div class="lg:hidden">
<button mat-icon-button (click)="drawer.close()">
<mat-icon
[svgIcon]="'heroicons_outline:x-mark'"
></mat-icon>
</button>
</div>
</div>
<!-- Panel links -->
<div class="flex flex-col divide-y border-b border-t">
@for (panel of panels; track trackByFn($index, panel)) {
<div
class="flex cursor-pointer px-8 py-5"
[ngClass]="{
'dark:hover:bg-hover hover:bg-gray-100':
!selectedPanel || selectedPanel !== panel.id,
'bg-primary-50 dark:bg-hover':
selectedPanel && selectedPanel === panel.id,
}"
(click)="goToPanel(panel.id)"
>
<mat-icon
[ngClass]="{
'text-hint':
!selectedPanel ||
selectedPanel !== panel.id,
'text-primary dark:text-primary-500':
selectedPanel && selectedPanel === panel.id,
}"
[svgIcon]="panel.icon"
></mat-icon>
<div class="ml-3">
<div
class="font-medium leading-6"
[ngClass]="{
'text-primary dark:text-primary-500':
selectedPanel &&
selectedPanel === panel.id,
}"
>
{{ panel.title }}
</div>
<div class="text-secondary mt-0.5">
{{ panel.description }}
</div>
</div>
</div>
}
</div>
</mat-drawer>
<!-- Drawer content -->
<mat-drawer-content class="flex flex-col">
<!-- Main -->
<div class="flex-auto px-6 pb-12 pt-9 md:p-8 md:pb-12 lg:p-12">
<!-- Panel header -->
<div class="flex items-center">
<!-- Drawer toggle -->
<button
class="-ml-2 lg:hidden"
mat-icon-button
(click)="drawer.toggle()"
>
<mat-icon
[svgIcon]="'heroicons_outline:bars-3'"
></mat-icon>
</button>
<!-- Panel title -->
<div
class="ml-2 text-3xl font-bold leading-none tracking-tight lg:ml-0"
>
{{ getPanelInfo(selectedPanel).title }}
</div>
</div>
<!-- Load settings panel -->
<div class="mt-8">
@switch (selectedPanel) {
<!-- Account -->
@case ('account') {
<settings-account></settings-account>
}
<!-- Security -->
@case ('security') {
<settings-security></settings-security>
}
<!-- Plan & Billing -->
@case ('plan-billing') {
<settings-plan-billing></settings-plan-billing>
}
<!-- Notifications -->
@case ('notifications') {
<settings-notifications></settings-notifications>
}
<!-- Team -->
@case ('team') {
<settings-team></settings-team>
}
}
</div>
</div>
</mat-drawer-content>
</mat-drawer-container>
</div>

View File

@@ -1,165 +0,0 @@
import { NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav';
import { AngorMediaWatcherService } from '@angor/services/media-watcher';
import { Subject, takeUntil } from 'rxjs';
import { SettingsAccountComponent } from './account/account.component';
import { SettingsNotificationsComponent } from './notifications/notifications.component';
import { SettingsPlanBillingComponent } from './plan-billing/plan-billing.component';
import { SettingsSecurityComponent } from './security/security.component';
import { SettingsTeamComponent } from './team/team.component';
@Component({
selector: 'settings',
templateUrl: './settings.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatSidenavModule,
MatButtonModule,
MatIconModule,
NgClass,
SettingsAccountComponent,
SettingsSecurityComponent,
SettingsPlanBillingComponent,
SettingsNotificationsComponent,
SettingsTeamComponent,
],
})
export class SettingsComponent implements OnInit, OnDestroy {
@ViewChild('drawer') drawer: MatDrawer;
drawerMode: 'over' | 'side' = 'side';
drawerOpened: boolean = true;
panels: any[] = [];
selectedPanel: string = 'account';
private _unsubscribeAll: Subject<any> = new Subject<any>();
/**
* Constructor
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _angorMediaWatcherService: AngorMediaWatcherService
) {}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void {
// Setup available panels
this.panels = [
{
id: 'account',
icon: 'heroicons_outline:user-circle',
title: 'Account',
description:
'Manage your public profile and private information',
},
{
id: 'security',
icon: 'heroicons_outline:lock-closed',
title: 'Security',
description:
'Manage your password and 2-step verification preferences',
},
{
id: 'plan-billing',
icon: 'heroicons_outline:credit-card',
title: 'Plan & Billing',
description:
'Manage your subscription plan, payment method and billing information',
},
{
id: 'notifications',
icon: 'heroicons_outline:bell',
title: 'Notifications',
description: "Manage when you'll be notified on which channels",
},
{
id: 'team',
icon: 'heroicons_outline:user-group',
title: 'Team',
description:
'Manage your existing team and change roles/permissions',
},
];
// Subscribe to media changes
this._angorMediaWatcherService.onMediaChange$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe(({ matchingAliases }) => {
// Set the drawerMode and drawerOpened
if (matchingAliases.includes('lg')) {
this.drawerMode = 'side';
this.drawerOpened = true;
} else {
this.drawerMode = 'over';
this.drawerOpened = false;
}
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this._unsubscribeAll.next(null);
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Navigate to the panel
*
* @param panel
*/
goToPanel(panel: string): void {
this.selectedPanel = panel;
// Close the drawer on 'over' mode
if (this.drawerMode === 'over') {
this.drawer.close();
}
}
/**
* Get the details of the panel
*
* @param id
*/
getPanelInfo(id: string): any {
return this.panels.find((panel) => panel.id === id);
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any {
return item.id || index;
}
}

View File

@@ -1,9 +0,0 @@
import { Routes } from '@angular/router';
import { SettingsComponent } from 'app/components/settings/settings.component';
export default [
{
path: '',
component: SettingsComponent,
},
] as Routes;

View File

@@ -1,99 +0,0 @@
<div class="w-full max-w-3xl">
<!-- Add team member -->
<div class="w-full">
<mat-form-field class="w-full" [subscriptSizing]="'dynamic'">
<mat-label>Add team members</mat-label>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:user'"
matPrefix
></mat-icon>
<input matInput [placeholder]="'Email address'" />
<button mat-icon-button matSuffix>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:plus-circle'"
></mat-icon>
</button>
</mat-form-field>
</div>
<!-- Team members -->
<div class="mt-8 flex flex-col divide-y border-b border-t">
@for (member of members; track trackByFn($index, member)) {
<div class="flex flex-col py-6 sm:flex-row sm:items-center">
<div class="flex items-center">
<div
class="flex h-10 w-10 flex-0 items-center justify-center overflow-hidden rounded-full"
>
@if (member.avatar) {
<img
class="h-full w-full object-cover"
[src]="member.avatar"
alt="Contact avatar"
/>
}
@if (!member.avatar) {
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gray-200 text-lg uppercase text-gray-600 dark:bg-gray-700 dark:text-gray-200"
>
{{ member.name.charAt(0) }}
</div>
}
</div>
<div class="ml-4">
<div class="font-medium">{{ member.name }}</div>
<div class="text-secondary">{{ member.email }}</div>
</div>
</div>
<div class="mt-4 flex items-center sm:ml-auto sm:mt-0">
<div class="order-2 ml-4 sm:order-1 sm:ml-0">
<mat-form-field
class="angor-mat-dense w-32"
[subscriptSizing]="'dynamic'"
>
<mat-select
[panelClass]="
'w-72 min-w-72 max-w-72 h-auto max-h-none'
"
[value]="member.role"
disableOptionCentering
#roleSelect="matSelect"
>
<mat-select-trigger class="text-md">
<span>Role:</span>
<span class="ml-1 font-medium">{{
roleSelect.value | titlecase
}}</span>
</mat-select-trigger>
@for (role of roles; track role) {
<mat-option
class="h-auto py-4 leading-none"
[value]="role.value"
>
<div class="font-medium">
{{ role.label }}
</div>
<div
class="text-secondary mt-1.5 whitespace-normal text-sm leading-normal"
>
{{ role.description }}
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="order-1 sm:order-2 sm:ml-3">
<button mat-icon-button>
<mat-icon
class="text-hint"
[svgIcon]="'heroicons_outline:trash'"
></mat-icon>
</button>
</div>
</div>
</div>
}
</div>
</div>

View File

@@ -1,130 +0,0 @@
import { TitleCasePipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
@Component({
selector: 'settings-team',
templateUrl: './team.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
MatOptionModule,
TitleCasePipe,
],
})
export class SettingsTeamComponent implements OnInit {
members: any[];
roles: any[];
/**
* Constructor
*/
constructor() {}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void {
// Setup the team members
this.members = [
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Dejesus Michael',
email: 'dejesusmichael@mail.org',
role: 'admin',
},
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Mclaughlin Steele',
email: 'mclaughlinsteele@mail.me',
role: 'admin',
},
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Laverne Dodson',
email: 'lavernedodson@mail.ca',
role: 'write',
},
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Trudy Berg',
email: 'trudyberg@mail.us',
role: 'read',
},
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Lamb Underwood',
email: 'lambunderwood@mail.me',
role: 'read',
},
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Mcleod Wagner',
email: 'mcleodwagner@mail.biz',
role: 'read',
},
{
avatar: 'images/avatars/avatar-placeholder.png',
name: 'Shannon Kennedy',
email: 'shannonkennedy@mail.ca',
role: 'read',
},
];
// Setup the roles
this.roles = [
{
label: 'Read',
value: 'read',
description:
'Can read and clone this repository. Can also open and comment on issues and pull requests.',
},
{
label: 'Write',
value: 'write',
description:
'Can read, clone, and push to this repository. Can also manage issues and pull requests.',
},
{
label: 'Admin',
value: 'admin',
description:
'Can read, clone, and push to this repository. Can also manage issues, pull requests, and repository settings, including adding collaborators.',
},
];
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any {
return item.id || index;
}
}