mirror of
https://github.com/block-core/angor-hub-old.git
synced 2026-02-23 12:12:23 +01:00
Load all contact
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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'],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { SettingsComponent } from 'app/components/settings/settings.component';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
component: SettingsComponent,
|
||||
},
|
||||
] as Routes;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user