mirror of
https://github.com/richard-loafle/fuse-angular.git
synced 2025-12-25 21:17:15 +00:00
Compare commits
33 Commits
v12.0.0
...
v12.2.0-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcfba4c9e4 | ||
|
|
40894e0aa3 | ||
|
|
5dd60c816c | ||
|
|
0ac967a945 | ||
|
|
e3821da077 | ||
|
|
ee48e11548 | ||
|
|
215546cc31 | ||
|
|
072dbce6d4 | ||
|
|
5ffe0d0efa | ||
|
|
e90fb9e618 | ||
|
|
88e98d002d | ||
|
|
deeef323f9 | ||
|
|
8dcf21cb1a | ||
|
|
284e282761 | ||
|
|
d917f03883 | ||
|
|
52e234325f | ||
|
|
0f2ddbda83 | ||
|
|
fa0d74504b | ||
|
|
42e0864538 | ||
|
|
ad2b19a07a | ||
|
|
6b6442b37f | ||
|
|
bb0efade72 | ||
|
|
63edc8d1f2 | ||
|
|
9dde624bb5 | ||
|
|
a5a27d0a51 | ||
|
|
85ea34a6ce | ||
|
|
9b059f8d0d | ||
|
|
df48ad1c56 | ||
|
|
6a113a5317 | ||
|
|
4b268e5d1b | ||
|
|
4bf11591a2 | ||
|
|
f45a605b4e | ||
|
|
c150a8902c |
2797
package-lock.json
generated
2797
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fuse/demo",
|
||||
"version": "12.0.0",
|
||||
"version": "12.2.0",
|
||||
"license": "https://themeforest.net/licenses/standard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -12,17 +12,17 @@
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "11.2.10",
|
||||
"@angular/cdk": "11.2.9",
|
||||
"@angular/common": "11.2.10",
|
||||
"@angular/compiler": "11.2.10",
|
||||
"@angular/core": "11.2.10",
|
||||
"@angular/forms": "11.2.10",
|
||||
"@angular/material": "11.2.9",
|
||||
"@angular/material-moment-adapter": "11.2.9",
|
||||
"@angular/platform-browser": "11.2.10",
|
||||
"@angular/platform-browser-dynamic": "11.2.10",
|
||||
"@angular/router": "11.2.10",
|
||||
"@angular/animations": "11.2.12",
|
||||
"@angular/cdk": "11.2.11",
|
||||
"@angular/common": "11.2.12",
|
||||
"@angular/compiler": "11.2.12",
|
||||
"@angular/core": "11.2.12",
|
||||
"@angular/forms": "11.2.12",
|
||||
"@angular/material": "11.2.11",
|
||||
"@angular/material-moment-adapter": "11.2.11",
|
||||
"@angular/platform-browser": "11.2.12",
|
||||
"@angular/platform-browser-dynamic": "11.2.12",
|
||||
"@angular/router": "11.2.12",
|
||||
"@fullcalendar/angular": "4.4.5-beta",
|
||||
"@fullcalendar/core": "4.4.2",
|
||||
"@fullcalendar/daygrid": "4.4.2",
|
||||
@@ -31,37 +31,37 @@
|
||||
"@fullcalendar/moment": "4.4.2",
|
||||
"@fullcalendar/rrule": "4.4.2",
|
||||
"@fullcalendar/timegrid": "4.4.2",
|
||||
"apexcharts": "3.26.0",
|
||||
"apexcharts": "3.26.1",
|
||||
"crypto-js": "3.3.0",
|
||||
"highlight.js": "10.7.2",
|
||||
"lodash-es": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
"ng-apexcharts": "1.5.9",
|
||||
"ngx-markdown": "11.1.2",
|
||||
"ngx-quill": "13.2.0",
|
||||
"ngx-markdown": "11.1.3",
|
||||
"ngx-quill": "13.3.1",
|
||||
"perfect-scrollbar": "1.5.0",
|
||||
"quill": "1.3.7",
|
||||
"rrule": "2.6.8",
|
||||
"rxjs": "6.6.7",
|
||||
"tslib": "2.1.0",
|
||||
"tslib": "2.2.0",
|
||||
"web-animations-js": "2.3.2",
|
||||
"zone.js": "0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "0.1102.9",
|
||||
"@angular/cli": "11.2.9",
|
||||
"@angular/compiler-cli": "11.2.10",
|
||||
"@angular/language-service": "11.2.10",
|
||||
"@angular-devkit/build-angular": "0.1102.11",
|
||||
"@angular/cli": "11.2.11",
|
||||
"@angular/compiler-cli": "11.2.12",
|
||||
"@angular/language-service": "11.2.12",
|
||||
"@tailwindcss/aspect-ratio": "0.2.0",
|
||||
"@tailwindcss/line-clamp": "0.2.0",
|
||||
"@tailwindcss/typography": "0.4.0",
|
||||
"@types/chroma-js": "2.1.3",
|
||||
"@types/crypto-js": "3.1.47",
|
||||
"@types/highlight.js": "10.1.0",
|
||||
"@types/jasmine": "3.6.8",
|
||||
"@types/jasmine": "3.6.9",
|
||||
"@types/lodash": "4.14.168",
|
||||
"@types/lodash-es": "4.17.4",
|
||||
"@types/node": "12.20.6",
|
||||
"@types/node": "12.20.10",
|
||||
"autoprefixer": "10.2.5",
|
||||
"chroma-js": "2.1.1",
|
||||
"codelyzer": "6.0.1",
|
||||
@@ -73,9 +73,9 @@
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-jasmine-html-reporter": "1.5.4",
|
||||
"lodash": "4.17.21",
|
||||
"postcss": "8.2.10",
|
||||
"postcss": "8.2.13",
|
||||
"protractor": "7.0.0",
|
||||
"tailwindcss": "2.1.1",
|
||||
"tailwindcss": "2.1.2",
|
||||
"ts-node": "8.3.0",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "4.1.5"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<!-- Button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="'Toggle Fullscreen'"
|
||||
(click)="toggleFullscreen()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrows-expand'"></mat-icon>
|
||||
</button>
|
||||
164
src/@fuse/components/fullscreen/fullscreen.component.ts
Normal file
164
src/@fuse/components/fullscreen/fullscreen.component.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { FSDocument, FSDocumentElement } from '@fuse/components/fullscreen/fullscreen.types';
|
||||
|
||||
@Component({
|
||||
selector : 'fuse-fullscreen',
|
||||
templateUrl : './fullscreen.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs : 'fuseFullscreen'
|
||||
})
|
||||
export class FuseFullscreenComponent implements OnInit
|
||||
{
|
||||
private _fsDoc: FSDocument;
|
||||
private _fsDocEl: FSDocumentElement;
|
||||
private _isFullscreen: boolean = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(@Inject(DOCUMENT) private _document: Document)
|
||||
{
|
||||
this._fsDoc = _document as FSDocument;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
this._fsDocEl = document.documentElement as FSDocumentElement;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle the fullscreen mode
|
||||
*/
|
||||
toggleFullscreen(): void
|
||||
{
|
||||
// Check if the fullscreen is open
|
||||
this._isFullscreen = this._getBrowserFullscreenElement() !== null;
|
||||
|
||||
// Toggle the fullscreen
|
||||
if ( this._isFullscreen )
|
||||
{
|
||||
this._closeFullscreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
this._openFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get browser's fullscreen element
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _getBrowserFullscreenElement(): Element
|
||||
{
|
||||
if ( typeof this._fsDoc.fullscreenElement !== 'undefined' )
|
||||
{
|
||||
return this._fsDoc.fullscreenElement;
|
||||
}
|
||||
|
||||
if ( typeof this._fsDoc.mozFullScreenElement !== 'undefined' )
|
||||
{
|
||||
return this._fsDoc.mozFullScreenElement;
|
||||
}
|
||||
|
||||
if ( typeof this._fsDoc.msFullscreenElement !== 'undefined' )
|
||||
{
|
||||
return this._fsDoc.msFullscreenElement;
|
||||
}
|
||||
|
||||
if ( typeof this._fsDoc.webkitFullscreenElement !== 'undefined' )
|
||||
{
|
||||
return this._fsDoc.webkitFullscreenElement;
|
||||
}
|
||||
|
||||
throw new Error('Fullscreen mode is not supported by this browser');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the fullscreen
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _openFullscreen(): void
|
||||
{
|
||||
if ( this._fsDocEl.requestFullscreen )
|
||||
{
|
||||
this._fsDocEl.requestFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Firefox
|
||||
if ( this._fsDocEl.mozRequestFullScreen )
|
||||
{
|
||||
this._fsDocEl.mozRequestFullScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome, Safari and Opera
|
||||
if ( this._fsDocEl.webkitRequestFullscreen )
|
||||
{
|
||||
this._fsDocEl.webkitRequestFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// IE/Edge
|
||||
if ( this._fsDocEl.msRequestFullscreen )
|
||||
{
|
||||
this._fsDocEl.msRequestFullscreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the fullscreen
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _closeFullscreen(): void
|
||||
{
|
||||
if ( this._fsDoc.exitFullscreen )
|
||||
{
|
||||
this._fsDoc.exitFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Firefox
|
||||
if ( this._fsDoc.mozCancelFullScreen )
|
||||
{
|
||||
this._fsDoc.mozCancelFullScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome, Safari and Opera
|
||||
if ( this._fsDoc.webkitExitFullscreen )
|
||||
{
|
||||
this._fsDoc.webkitExitFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// IE/Edge
|
||||
else if ( this._fsDoc.msExitFullscreen )
|
||||
{
|
||||
this._fsDoc.msExitFullscreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/@fuse/components/fullscreen/fullscreen.module.ts
Normal file
22
src/@fuse/components/fullscreen/fullscreen.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FuseFullscreenComponent } from '@fuse/components/fullscreen/fullscreen.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FuseFullscreenComponent
|
||||
],
|
||||
imports : [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
exports : [
|
||||
FuseFullscreenComponent
|
||||
]
|
||||
})
|
||||
export class FuseFullscreenModule
|
||||
{
|
||||
}
|
||||
16
src/@fuse/components/fullscreen/fullscreen.types.ts
Normal file
16
src/@fuse/components/fullscreen/fullscreen.types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface FSDocument extends HTMLDocument
|
||||
{
|
||||
mozFullScreenElement?: Element;
|
||||
mozCancelFullScreen?: () => void;
|
||||
msFullscreenElement?: Element;
|
||||
msExitFullscreen?: () => void;
|
||||
webkitFullscreenElement?: Element;
|
||||
webkitExitFullscreen?: () => void;
|
||||
}
|
||||
|
||||
export interface FSDocumentElement extends HTMLElement
|
||||
{
|
||||
mozRequestFullScreen?: () => void;
|
||||
msRequestFullscreen?: () => void;
|
||||
webkitRequestFullscreen?: () => void;
|
||||
}
|
||||
1
src/@fuse/components/fullscreen/index.ts
Normal file
1
src/@fuse/components/fullscreen/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@fuse/components/fullscreen/public-api';
|
||||
3
src/@fuse/components/fullscreen/public-api.ts
Normal file
3
src/@fuse/components/fullscreen/public-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from '@fuse/components/fullscreen/fullscreen.component';
|
||||
export * from '@fuse/components/fullscreen/fullscreen.module';
|
||||
export * from '@fuse/components/fullscreen/fullscreen.types';
|
||||
@@ -72,13 +72,17 @@ function generateThemesObject(themes)
|
||||
return _.map(_.cloneDeep(themes), (value, key) =>
|
||||
{
|
||||
const theme = normalizeTheme(value);
|
||||
const primary = (theme && theme.primary && theme.primary.DEFAULT) ? theme.primary.DEFAULT : normalizedDefaultTheme.primary.DEFAULT;
|
||||
const accent = (theme && theme.accent && theme.accent.DEFAULT) ? theme.accent.DEFAULT : normalizedDefaultTheme.accent.DEFAULT;
|
||||
const warn = (theme && theme.warn && theme.warn.DEFAULT) ? theme.warn.DEFAULT : normalizedDefaultTheme.warn.DEFAULT;
|
||||
|
||||
return _.fromPairs([
|
||||
[
|
||||
key,
|
||||
{
|
||||
primary: theme?.primary?.DEFAULT ?? normalizedDefaultTheme.primary.DEFAULT,
|
||||
accent : theme?.accent?.DEFAULT ?? normalizedDefaultTheme.accent.DEFAULT,
|
||||
warn : theme?.warn?.DEFAULT ?? normalizedDefaultTheme.warn.DEFAULT
|
||||
primary,
|
||||
accent,
|
||||
warn
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
@@ -56,7 +56,7 @@ const utilities = plugin(({
|
||||
}
|
||||
},
|
||||
{
|
||||
variants: ['dark', 'responsive']
|
||||
variants: ['dark', 'responsive', 'group-hover', 'hover']
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Version } from '@fuse/version/version';
|
||||
|
||||
const __FUSE_VERSION__ = '12.0.0';
|
||||
const __FUSE_VERSION__ = '12.2.0';
|
||||
export const FUSE_VERSION = new Version(__FUSE_VERSION__).full;
|
||||
|
||||
@@ -8,15 +8,15 @@ import { InitialDataResolver } from 'app/app.resolvers';
|
||||
// tslint:disable:max-line-length
|
||||
export const appRoutes: Route[] = [
|
||||
|
||||
// Redirect empty path to '/dashboards/project'
|
||||
{path: '', pathMatch : 'full', redirectTo: 'dashboards/project'},
|
||||
// Redirect empty path to '/example'
|
||||
{path: '', pathMatch : 'full', redirectTo: 'example'},
|
||||
|
||||
// Redirect signed in user to the '/dashboards/project'
|
||||
// Redirect signed in user to the '/example'
|
||||
//
|
||||
// After the user signs in, the sign in page will redirect the user to the 'signed-in-redirect'
|
||||
// path. Below is another redirection for that path to redirect the user to the desired
|
||||
// location. This is a small convenience to keep all main routes together here on this file.
|
||||
{path: 'signed-in-redirect', pathMatch : 'full', redirectTo: 'dashboards/project'},
|
||||
{path: 'signed-in-redirect', pathMatch : 'full', redirectTo: 'example'},
|
||||
|
||||
// Auth routes for guests
|
||||
{
|
||||
@@ -73,119 +73,7 @@ export const appRoutes: Route[] = [
|
||||
initialData: InitialDataResolver,
|
||||
},
|
||||
children : [
|
||||
|
||||
// Dashboards
|
||||
{path: 'dashboards', children: [
|
||||
{path: 'project', loadChildren: () => import('app/modules/admin/dashboards/project/project.module').then(m => m.ProjectModule)},
|
||||
{path: 'analytics', loadChildren: () => import('app/modules/admin/dashboards/analytics/analytics.module').then(m => m.AnalyticsModule)},
|
||||
]},
|
||||
|
||||
// Apps
|
||||
{path: 'apps', children: [
|
||||
{path: 'calendar', loadChildren: () => import('app/modules/admin/apps/calendar/calendar.module').then(m => m.CalendarModule)},
|
||||
{path: 'contacts', loadChildren: () => import('app/modules/admin/apps/contacts/contacts.module').then(m => m.ContactsModule)},
|
||||
{path: 'ecommerce', loadChildren: () => import('app/modules/admin/apps/ecommerce/ecommerce.module').then(m => m.ECommerceModule)},
|
||||
{path: 'file-manager', loadChildren: () => import('app/modules/admin/apps/file-manager/file-manager.module').then(m => m.FileManagerModule)},
|
||||
{path: 'help-center', loadChildren: () => import('app/modules/admin/apps/help-center/help-center.module').then(m => m.HelpCenterModule)},
|
||||
{path: 'mailbox', loadChildren: () => import('app/modules/admin/apps/mailbox/mailbox.module').then(m => m.MailboxModule)},
|
||||
{path: 'tasks', loadChildren: () => import('app/modules/admin/apps/tasks/tasks.module').then(m => m.TasksModule)},
|
||||
]},
|
||||
|
||||
// Pages
|
||||
{path: 'pages', children: [
|
||||
|
||||
// Authentication
|
||||
{path: 'authentication', loadChildren: () => import('app/modules/admin/pages/authentication/authentication.module').then(m => m.AuthenticationModule)},
|
||||
|
||||
// Coming soon
|
||||
{path: 'coming-soon', loadChildren: () => import('app/modules/admin/pages/coming-soon/coming-soon.module').then(m => m.ComingSoonModule)},
|
||||
|
||||
// Error
|
||||
{path: 'error', children: [
|
||||
{path: '404', loadChildren: () => import('app/modules/admin/pages/error/error-404/error-404.module').then(m => m.Error404Module)},
|
||||
{path: '500', loadChildren: () => import('app/modules/admin/pages/error/error-500/error-500.module').then(m => m.Error500Module)}
|
||||
]},
|
||||
|
||||
// Invoice
|
||||
{path: 'invoice', children: [
|
||||
{path: 'printable', children: [
|
||||
{path: 'compact', loadChildren: () => import('app/modules/admin/pages/invoice/printable/compact/compact.module').then(m => m.CompactModule)},
|
||||
{path: 'modern', loadChildren: () => import('app/modules/admin/pages/invoice/printable/modern/modern.module').then(m => m.ModernModule)}
|
||||
]}
|
||||
]},
|
||||
|
||||
// Maintenance
|
||||
{path: 'maintenance', loadChildren: () => import('app/modules/admin/pages/maintenance/maintenance.module').then(m => m.MaintenanceModule)},
|
||||
|
||||
// Pricing
|
||||
{path: 'pricing', children: [
|
||||
{path: 'modern', loadChildren: () => import('app/modules/admin/pages/pricing/modern/modern.module').then(m => m.PricingModernModule)},
|
||||
{path: 'simple', loadChildren: () => import('app/modules/admin/pages/pricing/simple/simple.module').then(m => m.PricingSimpleModule)},
|
||||
{path: 'single', loadChildren: () => import('app/modules/admin/pages/pricing/single/single.module').then(m => m.PricingSingleModule)},
|
||||
{path: 'table', loadChildren: () => import('app/modules/admin/pages/pricing/table/table.module').then(m => m.PricingTableModule)}
|
||||
]},
|
||||
|
||||
// Profile
|
||||
{path: 'profile', loadChildren: () => import('app/modules/admin/pages/profile/profile.module').then(m => m.ProfileModule)},
|
||||
]},
|
||||
|
||||
// User interface
|
||||
{path: 'ui', children: [
|
||||
|
||||
// Angular Material
|
||||
{path: 'angular-material', loadChildren: () => import('app/modules/admin/ui/angular-material/angular-material.module').then(m => m.AngularMaterialModule)},
|
||||
|
||||
// TailwindCSS
|
||||
{path: 'tailwindcss', loadChildren: () => import('app/modules/admin/ui/tailwindcss/tailwindcss.module').then(m => m.TailwindCSSModule)},
|
||||
|
||||
// Animations
|
||||
{path: 'animations', loadChildren: () => import('app/modules/admin/ui/animations/animations.module').then(m => m.AnimationsModule)},
|
||||
|
||||
// Cards
|
||||
{path: 'cards', loadChildren: () => import('app/modules/admin/ui/cards/cards.module').then(m => m.CardsModule)},
|
||||
|
||||
// Colors
|
||||
{path: 'colors', loadChildren: () => import('app/modules/admin/ui/colors/colors.module').then(m => m.ColorsModule)},
|
||||
|
||||
// Datatable
|
||||
{path: 'datatable', loadChildren: () => import('app/modules/admin/ui/datatable/datatable.module').then(m => m.DatatableModule)},
|
||||
|
||||
// Forms
|
||||
{path: 'forms', children: [
|
||||
{path: 'fields', loadChildren: () => import('app/modules/admin/ui/forms/fields/fields.module').then(m => m.FormsFieldsModule)},
|
||||
{path: 'layouts', loadChildren: () => import('app/modules/admin/ui/forms/layouts/layouts.module').then(m => m.FormsLayoutsModule)},
|
||||
{path: 'wizards', loadChildren: () => import('app/modules/admin/ui/forms/wizards/wizards.module').then(m => m.FormsWizardsModule)}
|
||||
]},
|
||||
|
||||
// Icons
|
||||
{path: 'icons', loadChildren: () => import('app/modules/admin/ui/icons/icons.module').then(m => m.IconsModule)},
|
||||
|
||||
// Page layouts
|
||||
{path: 'page-layouts', loadChildren: () => import('app/modules/admin/ui/page-layouts/page-layouts.module').then(m => m.PageLayoutsModule)},
|
||||
|
||||
// Typography
|
||||
{path: 'typography', loadChildren: () => import('app/modules/admin/ui/typography/typography.module').then(m => m.TypographyModule)}
|
||||
]},
|
||||
|
||||
// Documentation
|
||||
{path: 'docs', children: [
|
||||
|
||||
// Changelog
|
||||
{path: 'changelog', loadChildren: () => import('app/modules/admin/docs/changelog/changelog.module').then(m => m.ChangelogModule)},
|
||||
|
||||
// Guides
|
||||
{path: 'guides', loadChildren: () => import('app/modules/admin/docs/guides/guides.module').then(m => m.GuidesModule)},
|
||||
|
||||
// Core features
|
||||
{path: 'core-features', loadChildren: () => import('app/modules/admin/docs/core-features/core-features.module').then(m => m.CoreFeaturesModule)},
|
||||
|
||||
// Other components
|
||||
{path: 'other-components', loadChildren: () => import('app/modules/admin/docs/other-components/other-components.module').then(m => m.OtherComponentsModule)},
|
||||
]},
|
||||
|
||||
// 404 & Catch all
|
||||
{path: '404-not-found', pathMatch: 'full', loadChildren: () => import('app/modules/admin/pages/error/error-404/error-404.module').then(m => m.Error404Module)},
|
||||
{path: '**', redirectTo: '404-not-found'}
|
||||
{path: 'example', loadChildren: () => import('app/modules/admin/example/example.module').then(m => m.ExampleModule)},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -38,6 +38,7 @@ export class CoreModule
|
||||
// Register icon sets
|
||||
this._matIconRegistry.addSvgIconSet(this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-twotone.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('mat_outline', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-outline.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('mat_solid', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/material-solid.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('iconsmind', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/iconsmind.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('feather', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/feather.svg'));
|
||||
this._matIconRegistry.addSvgIconSetInNamespace('heroicons_outline', this._domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/heroicons-outline.svg'));
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
</ng-container>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { CenteredLayoutComponent } from 'app/layout/layouts/horizontal/centered/
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
</ng-container>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { EnterpriseLayoutComponent } from 'app/layout/layouts/horizontal/enterpr
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
</ng-container>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { MaterialLayoutComponent } from 'app/layout/layouts/horizontal/material/
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
</ng-container>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { ModernLayoutComponent } from 'app/layout/layouts/horizontal/modern/mode
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
</button>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { ClassicLayoutComponent } from 'app/layout/layouts/vertical/classic/clas
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
</button>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
@@ -80,8 +81,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="relative flex flex-0 items-center justify-start w-full h-14 px-4 md:px-6 z-49 border-t bg-card dark:bg-transparent print:hidden">
|
||||
<!--<div class="relative flex flex-0 items-center justify-start w-full h-14 px-4 md:px-6 z-49 border-t bg-card dark:bg-transparent print:hidden">
|
||||
<span class="font-medium text-secondary">Fuse © {{currentYear}}</span>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen/fullscreen.module';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
import { SearchModule } from 'app/layout/common/search/search.module';
|
||||
@@ -25,6 +26,7 @@ import { ClassyLayoutComponent } from 'app/layout/layouts/vertical/classy/classy
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
</button>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { CompactLayoutComponent } from 'app/layout/layouts/vertical/compact/comp
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
</div>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { DenseLayoutComponent } from 'app/layout/layouts/vertical/dense/dense.co
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
</button>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { FuturisticLayoutComponent } from 'app/layout/layouts/vertical/futuristi
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
</button>
|
||||
<!-- Components -->
|
||||
<div class="flex items-center pl-2 ml-auto space-x-2">
|
||||
<fuse-fullscreen></fuse-fullscreen>
|
||||
<search [appearance]="'bar'"></search>
|
||||
<shortcuts [shortcuts]="data.shortcuts"></shortcuts>
|
||||
<messages [messages]="data.messages"></messages>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { FuseFullscreenModule } from '@fuse/components/fullscreen';
|
||||
import { FuseNavigationModule } from '@fuse/components/navigation';
|
||||
import { MessagesModule } from 'app/layout/common/messages/messages.module';
|
||||
import { NotificationsModule } from 'app/layout/common/notifications/notifications.module';
|
||||
@@ -25,6 +26,7 @@ import { ThinLayoutComponent } from 'app/layout/layouts/vertical/thin/thin.compo
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
FuseFullscreenModule,
|
||||
FuseNavigationModule,
|
||||
MessagesModule,
|
||||
NotificationsModule,
|
||||
|
||||
89
src/app/mock-api/apps/academy/api.ts
Normal file
89
src/app/mock-api/apps/academy/api.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { FuseMockApiService } from '@fuse/lib/mock-api/mock-api.service';
|
||||
import { categories as categoriesData, courses as coursesData, demoCourseSteps as demoCourseStepsData } from 'app/mock-api/apps/academy/data';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AcademyMockApi
|
||||
{
|
||||
private _categories: any[] = categoriesData;
|
||||
private _courses: any[] = coursesData;
|
||||
private _demoCourseSteps: any[] = demoCourseStepsData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _fuseMockApiService: FuseMockApiService)
|
||||
{
|
||||
// Register Mock API handlers
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register Mock API handlers
|
||||
*/
|
||||
registerHandlers(): void
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Categories - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/academy/categories')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the categories
|
||||
const categories = cloneDeep(this._categories);
|
||||
|
||||
// Sort the categories alphabetically by title
|
||||
categories.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return [200, categories];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Courses - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/academy/courses')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the courses
|
||||
const courses = cloneDeep(this._courses);
|
||||
|
||||
return [200, courses];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Course - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/academy/courses/course')
|
||||
.reply(({request}) => {
|
||||
|
||||
// Get the id from the params
|
||||
const id = request.params.get('id');
|
||||
|
||||
// Clone the courses and steps
|
||||
const courses = cloneDeep(this._courses);
|
||||
const steps = cloneDeep(this._demoCourseSteps);
|
||||
|
||||
// Find the course and attach steps to it
|
||||
const course = courses.find((item) => item.id === id);
|
||||
if ( course )
|
||||
{
|
||||
course.steps = steps;
|
||||
}
|
||||
|
||||
return [
|
||||
200,
|
||||
course
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
719
src/app/mock-api/apps/academy/data.ts
Normal file
719
src/app/mock-api/apps/academy/data.ts
Normal file
@@ -0,0 +1,719 @@
|
||||
/* tslint:disable:max-line-length */
|
||||
export const categories = [
|
||||
{
|
||||
id : '9a67dff7-3c38-4052-a335-0cef93438ff6',
|
||||
title: 'Web',
|
||||
slug : 'web'
|
||||
},
|
||||
{
|
||||
id : 'a89672f5-e00d-4be4-9194-cb9d29f82165',
|
||||
title: 'Firebase',
|
||||
slug : 'firebase'
|
||||
},
|
||||
{
|
||||
id : '02f42092-bb23-4552-9ddb-cfdcc235d48f',
|
||||
title: 'Cloud',
|
||||
slug : 'cloud'
|
||||
},
|
||||
{
|
||||
id : '5648a630-979f-4403-8c41-fc9790dea8cd',
|
||||
title: 'Android',
|
||||
slug : 'android'
|
||||
}
|
||||
];
|
||||
export const courses = [
|
||||
{
|
||||
id : '694e4e5f-f25f-470b-bd0e-26b1d4f64028',
|
||||
title : 'Basics of Angular',
|
||||
slug : 'basics-of-angular',
|
||||
description: 'Introductory course for Angular and framework basics',
|
||||
category : 'web',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jun 28, 2021',
|
||||
featured : true,
|
||||
progress : {
|
||||
currentStep: 3,
|
||||
completed : 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'f924007a-2ee9-470b-a316-8d21ed78277f',
|
||||
title : 'Basics of TypeScript',
|
||||
slug : 'basics-of-typeScript',
|
||||
description: 'Beginner course for Typescript and its basics',
|
||||
category : 'web',
|
||||
duration : 60,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Nov 01, 2021',
|
||||
featured : true,
|
||||
progress : {
|
||||
currentStep: 5,
|
||||
completed : 3
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '0c06e980-abb5-4ba7-ab65-99a228cab36b',
|
||||
title : 'Android N: Quick Settings',
|
||||
slug : 'android-n-quick-settings',
|
||||
description: 'Step by step guide for Android N: Quick Settings',
|
||||
category : 'android',
|
||||
duration : 120,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'May 08, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 10,
|
||||
completed : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '1b9a9acc-9a36-403e-a1e7-b11780179e38',
|
||||
title : 'Build an App for the Google Assistant with Firebase',
|
||||
slug : 'build-an-app-for-the-google-assistant-with-firebase',
|
||||
description: 'Dive deep into Google Assistant apps using Firebase',
|
||||
category : 'firebase',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jan 09, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 4,
|
||||
completed : 3
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '55eb415f-3f4e-4853-a22b-f0ae91331169',
|
||||
title : 'Keep Sensitive Data Safe and Private',
|
||||
slug : 'keep-sensitive-data-safe-and-private',
|
||||
description: 'Learn how to keep your important data safe and private',
|
||||
category : 'android',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jan 14, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 6,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'fad2ab23-1011-4028-9a54-e52179ac4a50',
|
||||
title : 'Manage Your Pivotal Cloud Foundry App\'s Using Apigee Edge',
|
||||
slug : 'manage-your-pivotal-cloud-foundry-apps-using-apigee-Edge',
|
||||
description: 'Introductory course for Pivotal Cloud Foundry App',
|
||||
category : 'cloud',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jun 24, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 6,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'c4bc107b-edc4-47a7-a7a8-4fb09732e794',
|
||||
title : 'Build a PWA Using Workbox',
|
||||
slug : 'build-a-pwa-using-workbox',
|
||||
description: 'Step by step guide for building a PWA using Workbox',
|
||||
category : 'web',
|
||||
duration : 120,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Nov 19, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '1449f945-d032-460d-98e3-406565a22293',
|
||||
title : 'Cloud Functions for Firebase',
|
||||
slug : 'cloud-functions-for-firebase',
|
||||
description: 'Beginners guide of Firebase Cloud Functions',
|
||||
category : 'firebase',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jul 11, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 3,
|
||||
completed : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'f05e08ab-f3e3-4597-a032-6a4b69816f24',
|
||||
title : 'Building a gRPC Service with Java',
|
||||
slug : 'building-a-grpc-service-with-java',
|
||||
description: 'Learn more about building a gRPC Service with Java',
|
||||
category : 'cloud',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Mar 13, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '181728f4-87c8-45c5-b9cc-92265bcd2f4d',
|
||||
title : 'Looking at Campaign Finance with BigQuery',
|
||||
slug : 'looking-at-campaign-finance-with-bigquery',
|
||||
description: 'Dive deep into BigQuery: Campaign Finance',
|
||||
category : 'cloud',
|
||||
duration : 60,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Nov 01, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'fcbfedbf-6187-4b3b-89d3-1a7cb4e11616',
|
||||
title : 'Personalize Your iOS App with Firebase User Management',
|
||||
slug : 'personalize-your-ios-app-with-firebase-user-management',
|
||||
description: 'Dive deep into User Management on iOS apps using Firebase',
|
||||
category : 'firebase',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Aug 08, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '5213f6a1-1dd7-4b1d-b6e9-ffb7af534f28',
|
||||
title : 'Customize Network Topology with Subnetworks',
|
||||
slug : 'customize-network-topology-with-subnetworks',
|
||||
description: 'Dive deep into Network Topology with Subnetworks',
|
||||
category : 'web',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'May 12, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '02992ac9-d1a3-4167-b70e-8a1d5b5ba253',
|
||||
title : 'Building Beautiful UIs with Flutter',
|
||||
slug : 'building-beautiful-uis-with-flutter',
|
||||
description: 'Dive deep into Flutter\'s hidden secrets for creating beautiful UIs',
|
||||
category : 'web',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Sep 18, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 8,
|
||||
completed : 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '2139512f-41fb-4a4a-841a-0b4ac034f9b4',
|
||||
title : 'Firebase Android',
|
||||
slug : 'firebase-android',
|
||||
description: 'Beginners guide of Firebase for Android',
|
||||
category : 'android',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Apr 24, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '65e0a0e0-d8c0-4117-a3cb-eb74f8e28809',
|
||||
title : 'Simulating a Thread Network Using OpenThread',
|
||||
slug : 'simulating-a-thread-network-using-openthread',
|
||||
description: 'Introductory course for OpenThread and Simulating a Thread Network',
|
||||
category : 'web',
|
||||
duration : 45,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Jun 05, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'c202ebc9-9be3-433a-9d38-7003b3ed7b7a',
|
||||
title : 'Your First Progressive Web App',
|
||||
slug : 'your-first-progressive-web-app',
|
||||
description: 'Step by step guide for creating a PWA from scratch',
|
||||
category : 'web',
|
||||
duration : 30,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Oct 14, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : '980ae7da-9f77-4e30-aa98-1b1ea594e775',
|
||||
title : 'Launch Cloud Datalab',
|
||||
slug : 'launch-cloud-datalab',
|
||||
description: 'From start to finish: Launch Cloud Datalab',
|
||||
category : 'cloud',
|
||||
duration : 60,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Dec 16, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 0,
|
||||
completed : 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id : 'c9748ea9-4117-492c-bdb2-55085b515978',
|
||||
title : 'Cloud Firestore',
|
||||
slug : 'cloud-firestore',
|
||||
description: 'Step by step guide for setting up Cloud Firestore',
|
||||
category : 'firebase',
|
||||
duration : 90,
|
||||
totalSteps : 11,
|
||||
updatedAt : 'Apr 04, 2021',
|
||||
featured : false,
|
||||
progress : {
|
||||
currentStep: 2,
|
||||
completed : 0
|
||||
}
|
||||
}
|
||||
];
|
||||
export const demoCourseContent = `
|
||||
<p class="lead">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus aperiam lab et fugiat id magnam minus nemo quam
|
||||
voluptatem. Culpa deleniti explica nisi quod soluta.
|
||||
</p>
|
||||
<p>
|
||||
Alias animi labque, deserunt distinctio eum excepturi fuga iure labore magni molestias mollitia natus, officia pofro
|
||||
quis sunt temporibus veritatis voluptatem, voluptatum. Aut blanditiis esse et illum maxim, obcaecati possimus
|
||||
voluptate! Accusamus <em>adipisci</em> amet aperiam, assumenda consequuntur fugiat inventore iusto magnam molestias
|
||||
natus necessitatibus, nulla pariatur.
|
||||
</p>
|
||||
<p>
|
||||
Amet distinctio enim itaque minima minus nesciunt recusandae soluta voluptatibus:
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
Ad aliquid amet asperiores lab distinctio doloremque <code>eaque</code>, exercitationem explicabo, minus mollitia
|
||||
natus necessitatibus odio omnis pofro rem.
|
||||
</p>
|
||||
</blockquote>
|
||||
<p>
|
||||
Alias architecto asperiores, dignissimos illum ipsam ipsum itaque, natus necessitatibus officiis, perferendis quae
|
||||
sed ullam veniam vitae voluptas! Magni, nisi, quis! A <code>accusamus</code> animi commodi, consectetur distinctio
|
||||
eaque, eos excepturi illum laboriosam maiores nam natus nulla officiis perspiciatis rem <em>reprehenderit</em> sed
|
||||
tenetur veritatis.
|
||||
</p>
|
||||
<p>
|
||||
Consectetur <code>dicta enim</code> error eveniet expedita, facere in itaque labore <em>natus</em> quasi? Ad consectetur
|
||||
eligendi facilis magni quae quis, quo temporibus voluptas voluptate voluptatem!
|
||||
</p>
|
||||
<p>
|
||||
Adipisci alias animi <code>debitis</code> eos et impedit maiores, modi nam nobis officia optio perspiciatis, rerum.
|
||||
Accusantium esse nostrum odit quis quo:
|
||||
</p>
|
||||
<pre><code>h1 a {{'{'}}
|
||||
display: block;
|
||||
width: 300px;
|
||||
height: 80px;
|
||||
{{'}'}}</code></pre>
|
||||
<p>
|
||||
Accusantium aut autem, lab deleniti eaque fugiat fugit id ipsa iste molestiae,
|
||||
<a>necessitatibus nemo quasi</a>
|
||||
.
|
||||
</p>
|
||||
<hr>
|
||||
<h2>
|
||||
Accusantium aspernatur autem enim
|
||||
</h2>
|
||||
<p>
|
||||
Blanditiis, fugit voluptate! Assumenda blanditiis consectetur, labque cupiditate ducimus eaque earum, fugiat illum
|
||||
ipsa, necessitatibus omnis quaerat reiciendis totam. Architecto, <strong>facere</strong> illum molestiae nihil nulla
|
||||
quibusdam quidem vel! Atque <em>blanditiis deserunt</em>.
|
||||
</p>
|
||||
<p>
|
||||
Debitis deserunt doloremque labore laboriosam magni minus odit:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Asperiores dicta esse maiores nobis officiis.</li>
|
||||
<li>Accusamus aliquid debitis dolore illo ipsam molettiae possimus.</li>
|
||||
<li>Magnam mollitia pariatur perspiciatis quasi quidem tenetur voluptatem! Adipisci aspernatur assumenda dicta.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Animi fugit incidunt iure magni maiores molestias.
|
||||
</p>
|
||||
<h3>
|
||||
Consequatur iusto soluta
|
||||
</h3>
|
||||
<p>
|
||||
Aliquid asperiores corporis — deserunt dolorum ducimus eius eligendi explicabo quaerat suscipit voluptas.
|
||||
</p>
|
||||
<p>
|
||||
Deserunt dolor eos et illum laborum magni molestiae mollitia:
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>Autem beatae consectetur consequatur, facere, facilis fugiat id illo, impedit numquam optio quis sunt ducimus illo.</p>
|
||||
</blockquote>
|
||||
<p>
|
||||
Adipisci consequuntur doloribus facere in ipsam maxime molestias pofro quam:
|
||||
</p>
|
||||
<figure>
|
||||
<img
|
||||
src="assets/images/pages/help-center/image-1.jpg"
|
||||
alt="">
|
||||
<figcaption>
|
||||
Accusamus blanditiis labque delectus esse et eum excepturi, impedit ipsam iste maiores minima mollitia, nihil obcaecati
|
||||
placeat quaerat qui quidem sint unde!
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p>
|
||||
A beatae lab deleniti explicabo id inventore magni nisi omnis placeat praesentium quibusdam:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Dolorem eaque laboriosam omnis praesentium.</li>
|
||||
<li>Atque debitis delectus distinctio doloremque.</li>
|
||||
<li>Fuga illo impedit minima mollitia neque obcaecati.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Consequ eius eum excepturi explicabo.
|
||||
</p>
|
||||
<h2>
|
||||
Adipisicing elit atque impedit?
|
||||
</h2>
|
||||
<h3>
|
||||
Atque distinctio doloremque ea qui quo, repellendus.
|
||||
</h3>
|
||||
<p>
|
||||
Delectus deserunt explicabo facilis numquam quasi! Laboriosam, magni, quisquam. Aut, blanditiis commodi distinctio, facere fuga
|
||||
hic itaque iure labore laborum maxime nemo neque provident quos recusandae sequi veritatis illum inventore iure qui rerum sapiente.
|
||||
</p>
|
||||
<h3>
|
||||
Accusamus iusto sint aperiam consectetur …
|
||||
</h3>
|
||||
<p>
|
||||
Aliquid assumenda ipsa nam odit pofro quaerat, quasi recusandae sint! Aut, esse explicabo facilis fugit illum iure magni
|
||||
necessitatibus odio quas.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>Dolore natus placeat rem atque dignissimos laboriosam.</strong></p>
|
||||
<p>
|
||||
Amet repudiandae voluptates architecto dignissimos repellendus voluptas dignissimos eveniet itaque maiores natus.
|
||||
</p>
|
||||
<p>
|
||||
Accusamus aliquam debitis delectus dolorem ducimus enim eos, exercitationem fugiat id iusto nostrum quae quos
|
||||
recusandae reiciendis rerum sequi temporibus veniam vero? Accusantium culpa, cupiditate ducimus eveniet id maiores modi
|
||||
mollitia nisi aliquid dolorum ducimus et illo in.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Ab amet deleniti dolor, et hic optio placeat.</strong></p>
|
||||
<p>
|
||||
Accusantium ad alias beatae, consequatur consequuntur eos error eveniet expedita fuga laborum libero maxime nulla pofro
|
||||
praesentium rem rerum saepe soluta ullam vero, voluptas? Architecto at debitis, doloribus harum iure libero natus odio
|
||||
optio soluta veritatis voluptate.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>At aut consectetur nam necessitatibus neque nesciunt.</strong></p>
|
||||
<p>
|
||||
Aut dignissimos labore nobis nostrum optio! Dolor id minima velit voluptatibus. Aut consequuntur eum exercitationem
|
||||
fuga, harum id impedit molestiae natus neque numquam perspiciatis quam rem voluptatum.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Animi aperiam autem labque dolore enim ex expedita harum hic id impedit ipsa laborum modi mollitia non perspiciatis quae ratione.
|
||||
</p>
|
||||
<h2>
|
||||
Alias eos excepturi facilis fugit.
|
||||
</h2>
|
||||
<p>
|
||||
Alias asperiores, aspernatur corporis
|
||||
<a>delectus</a>
|
||||
est
|
||||
<a>facilis</a>
|
||||
inventore dolore
|
||||
ipsa nobis nostrum officia quia, veritatis vero! At dolore est nesciunt numquam quam. Ab animi <em>architecto</em> aut, dignissimos
|
||||
eos est eum explicabo.
|
||||
</p>
|
||||
<p>
|
||||
Adipisci autem consequuntur <code>labque cupiditate</code> dolor ducimus fuga neque nesciunt:
|
||||
</p>
|
||||
<pre><code>module.exports = {{'{'}}
|
||||
purge: [],
|
||||
theme: {{'{'}}
|
||||
extend: {{'{}'}},
|
||||
},
|
||||
variants: {{'{}'}},
|
||||
plugins: [],
|
||||
{{'}'}}</code></pre>
|
||||
<p>
|
||||
Aliquid aspernatur eius fugit hic iusto.
|
||||
</p>
|
||||
<h3>
|
||||
Dolorum ducimus expedita?
|
||||
</h3>
|
||||
<p>
|
||||
Culpa debitis explicabo maxime minus quaerat reprehenderit temporibus! Asperiores, cupiditate ducimus esse est expedita fuga hic
|
||||
ipsam necessitatibus placeat possimus? Amet animi aut consequuntur earum eveniet.
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Aspernatur at beatae corporis debitis.</strong>
|
||||
<ul>
|
||||
<li>
|
||||
Aperiam assumenda commodi lab dicta eius, “fugit ipsam“ itaque iure molestiae nihil numquam, officia omnis quia
|
||||
repellendus sapiente sed.
|
||||
</li>
|
||||
<li>
|
||||
Nulla odio quod saepe accusantium, adipisci autem blanditiis lab doloribus.
|
||||
</li>
|
||||
<li>
|
||||
Explicabo facilis iusto molestiae nisi nostrum obcaecati officia.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Nobis odio officiis optio quae quis quisquam quos rem.</strong>
|
||||
<ul>
|
||||
<li>Modi pariatur quod totam. Deserunt doloribus eveniet, expedita.</li>
|
||||
<li>Ad beatae dicta et fugit libero optio quaerat rem repellendus./</li>
|
||||
<li>Architecto atque consequuntur corporis id iste magni.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Deserunt non placeat unde veniam veritatis? Odio quod.</strong>
|
||||
<ul>
|
||||
<li>Inventore iure magni quod repellendus tempora. Magnam neque, quia. Adipisci amet.</li>
|
||||
<li>Consectetur adipisicing elit.</li>
|
||||
<li>labque eum expedita illo inventore iusto laboriosam nesciunt non, odio provident.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
A aliquam architecto consequatur labque dicta doloremque <code><li></code> doloribus, ducimus earum, est <code><p></code>
|
||||
eveniet explicabo fuga fugit ipsum minima minus molestias nihil nisi non qui sunt vel voluptatibus? A dolorum illum nihil quidem.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Laboriosam nesciunt obcaecati optio qui.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Doloremque magni molestias reprehenderit.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Accusamus aperiam blanditiis <code><p></code> commodi</li>
|
||||
<li>Dolorum ea explicabo fugiat in ipsum</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Commodi dolor dolorem dolores eum expedita libero.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Accusamus alias consectetur dolores et, excepturi fuga iusto possimus.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
Accusantium ad alias atque aut autem consequuntur deserunt dignissimos eaque iure <code><p></code> maxime.
|
||||
</p>
|
||||
<p>
|
||||
Dolorum in nisi numquam omnis quam sapiente sit vero.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Adipisci lab in nisi odit soluta sunt vitae commodi excepturi.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Assumenda deserunt distinctio dolor iste mollitia nihil non?
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Consectetur adipisicing elit dicta dolor iste.
|
||||
</p>
|
||||
<h2>
|
||||
Consectetur ea natus officia omnis reprehenderit.
|
||||
</h2>
|
||||
<p>
|
||||
Distinctio impedit quaerat sed! Accusamus
|
||||
<a>aliquam aspernatur enim expedita explicabo</a>
|
||||
. Libero molestiae
|
||||
odio quasi unde ut? Ab exercitationem id numquam odio quisquam!
|
||||
</p>
|
||||
<p>
|
||||
Explicabo facilis nemo quidem natus tempore:
|
||||
</p>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Wrestler</th>
|
||||
<th>Origin</th>
|
||||
<th>Finisher</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Bret “The Hitman” Hart</td>
|
||||
<td>Calgary, AB</td>
|
||||
<td>Sharpshooter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stone Cold Steve Austin</td>
|
||||
<td>Austin, TX</td>
|
||||
<td>Stone Cold Stunner</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Randy Savage</td>
|
||||
<td>Sarasota, FL</td>
|
||||
<td>Elbow Drop</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vader</td>
|
||||
<td>Boulder, CO</td>
|
||||
<td>Vader Bomb</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Razor Ramon</td>
|
||||
<td>Chuluota, FL</td>
|
||||
<td>Razor’s Edge</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
A aliquid autem lab doloremque, ea earum eum fuga fugit illo ipsa minus natus nisi <code><span></code> obcaecati pariatur
|
||||
perferendis pofro <code>suscipit tempore</code>.
|
||||
</p>
|
||||
<h3>
|
||||
Ad alias atque culpa <code>illum</code> earum optio
|
||||
</h3>
|
||||
<p>
|
||||
Architecto consequatur eveniet illo in iure laborum minus omnis quibusdam sequi temporibus? Ab aliquid <em>“atque dolores molestiae
|
||||
nemo perferendis”</em> reprehenderit saepe.
|
||||
</p>
|
||||
<p>
|
||||
Accusantium aliquid eligendi est fuga natus, <code>quos</code> vel? Adipisci aperiam asperiores aspernatur consectetur cupiditate
|
||||
<a><code>@distinctio/doloribus</code></a>
|
||||
et exercitationem expedita, facere facilis illum, impedit inventore
|
||||
ipsa iure iusto magnam, magni minus nesciunt non officia possimus quod reiciendis.
|
||||
</p>
|
||||
<h4>
|
||||
Cupiditate explicabo <code>hic</code> maiores
|
||||
</h4>
|
||||
<p>
|
||||
Aliquam amet consequuntur distinctio <code>ea</code> est <code>excepturi</code> facere illum maiores nisi nobis non odit officiis
|
||||
quisquam, similique tempora temporibus, tenetur ullam <code>voluptates</code> adipisci aperiam deleniti <code>doloremque</code>
|
||||
ducimus <code>eos</code>.
|
||||
</p>
|
||||
<p>
|
||||
Ducimus qui quo tempora. lab enim explicabo <code>hic</code> inventore qui soluta voluptates voluptatum? Asperiores consectetur
|
||||
delectus dolorem fugiat ipsa pariatur, quas <code>quos</code> repellendus <em>repudiandae</em> sunt aut blanditiis.
|
||||
</p>
|
||||
<h3>
|
||||
Asperiores aspernatur autem error praesentium quidem.
|
||||
</h3>
|
||||
<h4>
|
||||
Ad blanditiis commodi, doloribus id iste <code>repudiandae</code> vero vitae.
|
||||
</h4>
|
||||
<p>
|
||||
Atque consectetur lab debitis enim est et, facere fugit impedit, possimus quaerat quibusdam.
|
||||
</p>
|
||||
<p>
|
||||
Dolorem nihil placeat quibusdam veniam? Amet architecto at consequatur eligendi eveniet excepturi hic illo impedit in iste magni maxime
|
||||
modi nisi nulla odio placeat quidem, quos rem repellat similique suscipit voluptate voluptates nobis omnis quo repellendus.
|
||||
</p>
|
||||
<p>
|
||||
Assumenda, eum, minima! Autem consectetur fugiat iste sit! Nobis omnis quo repellendus.
|
||||
</p>
|
||||
`;
|
||||
export const demoCourseSteps = [
|
||||
{
|
||||
order : 0,
|
||||
title : 'Introduction',
|
||||
subtitle: 'Introducing the library and how it works',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Introduction</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 1,
|
||||
title : 'Get the sample code',
|
||||
subtitle: 'Where to find the sample code and how to access it',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Get the sample code</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 2,
|
||||
title : 'Create a Firebase project and Set up your app',
|
||||
subtitle: 'How to create a basic Firebase project and how to run it locally',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Create a Firebase project and Set up your app</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 3,
|
||||
title : 'Install the Firebase Command Line Interface',
|
||||
subtitle: 'Setting up the Firebase CLI to access command line tools',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Install the Firebase Command Line Interface</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 4,
|
||||
title : 'Deploy and run the web app',
|
||||
subtitle: 'How to build, push and run the project remotely',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Deploy and run the web app</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 5,
|
||||
title : 'The Functions Directory',
|
||||
subtitle: 'Introducing the Functions and Functions Directory',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">The Functions Directory</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 6,
|
||||
title : 'Import the Cloud Functions and Firebase Admin modules',
|
||||
subtitle: 'Create your first Function and run it to administer your app',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Import the Cloud Functions and Firebase Admin modules</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 7,
|
||||
title : 'Welcome New Users',
|
||||
subtitle: 'How to create a welcome message for the new users',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Welcome New Users</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 8,
|
||||
title : 'Images moderation',
|
||||
subtitle: 'How to moderate images; crop, resize, optimize',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Images moderation</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 9,
|
||||
title : 'New Message Notifications',
|
||||
subtitle: 'How to create and push a notification to a user',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">New Message Notifications</h1> ${demoCourseContent}`
|
||||
},
|
||||
{
|
||||
order : 10,
|
||||
title : 'Congratulations!',
|
||||
subtitle: 'Nice work, you have created your first application',
|
||||
content : `<h2 class="text-2xl sm:text-3xl">Congratulations!</h1> ${demoCourseContent}`
|
||||
}
|
||||
];
|
||||
167
src/app/mock-api/apps/chat/api.ts
Normal file
167
src/app/mock-api/apps/chat/api.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { assign, cloneDeep, omit } from 'lodash-es';
|
||||
import { FuseMockApiService } from '@fuse/lib/mock-api';
|
||||
import { chats as chatsData, contacts as contactsData, messages as messagesData, profile as profileData } from 'app/mock-api/apps/chat/data';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ChatMockApi
|
||||
{
|
||||
private _chats: any[] = chatsData;
|
||||
private _contacts: any[] = contactsData;
|
||||
private _messages: any[] = messagesData;
|
||||
private _profile: any = profileData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _fuseMockApiService: FuseMockApiService)
|
||||
{
|
||||
// Register Mock API handlers
|
||||
this.registerHandlers();
|
||||
|
||||
// Modify the chats array to attach certain data to it
|
||||
this._chats = this._chats.map((chat) => ({
|
||||
...chat,
|
||||
// Get the actual contact object from the id and attach it to the chat
|
||||
contact: this._contacts.find((contact) => contact.id === chat.contactId),
|
||||
// Since we use same set of messages on all chats, we assign them here.
|
||||
messages: this._messages.map((message) => ({
|
||||
...message,
|
||||
chatId : chat.id,
|
||||
contactId: message.contactId === 'me' ? this._profile.id : chat.contactId,
|
||||
isMine : message.contactId === 'me'
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register Mock API handlers
|
||||
*/
|
||||
registerHandlers(): void
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Chats - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/chat/chats')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the chats
|
||||
const chats = cloneDeep(this._chats);
|
||||
|
||||
// Return the response
|
||||
return [200, chats];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Chat - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/chat/chat')
|
||||
.reply(({request}) => {
|
||||
|
||||
// Get the chat id
|
||||
const id = request.params.get('id');
|
||||
|
||||
// Clone the chats
|
||||
const chats = cloneDeep(this._chats);
|
||||
|
||||
// Find the chat we need
|
||||
const chat = chats.find((item) => item.id === id);
|
||||
|
||||
// Return the response
|
||||
return [200, chat];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Chat - PATCH
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onPatch('api/apps/chat/chat')
|
||||
.reply(({request}) => {
|
||||
|
||||
// Get the id and chat
|
||||
const id = request.body.id;
|
||||
const chat = cloneDeep(request.body.chat);
|
||||
|
||||
// Prepare the updated chat
|
||||
let updatedChat = null;
|
||||
|
||||
// Find the chat and update it
|
||||
this._chats.forEach((item, index, chats) => {
|
||||
|
||||
if ( item.id === id )
|
||||
{
|
||||
// Update the chat
|
||||
chats[index] = assign({}, chats[index], chat);
|
||||
|
||||
// Store the updated chat
|
||||
updatedChat = chats[index];
|
||||
}
|
||||
});
|
||||
|
||||
// Return the response
|
||||
return [200, updatedChat];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Contacts - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/chat/contacts')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the contacts
|
||||
let contacts = cloneDeep(this._contacts);
|
||||
|
||||
// Sort the contacts by the name field by default
|
||||
contacts.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Omit details and attachments from contacts
|
||||
contacts = contacts.map((contact) => omit(contact, ['details', 'attachments']));
|
||||
|
||||
// Return the response
|
||||
return [200, contacts];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Contact Details - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/chat/contact')
|
||||
.reply(({request}) => {
|
||||
|
||||
// Get the contact id
|
||||
const id = request.params.get('id');
|
||||
|
||||
// Clone the contacts
|
||||
const contacts = cloneDeep(this._contacts);
|
||||
|
||||
// Find the contact
|
||||
const contact = contacts.find((item) => item.id === id);
|
||||
|
||||
// Return the response
|
||||
return [200, contact];
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Profile - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/apps/chat/profile')
|
||||
.reply(() => {
|
||||
|
||||
// Clone the profile
|
||||
const profile = cloneDeep(this._profile);
|
||||
|
||||
// Return the response
|
||||
return [200, profile];
|
||||
});
|
||||
}
|
||||
}
|
||||
3007
src/app/mock-api/apps/chat/data.ts
Normal file
3007
src/app/mock-api/apps/chat/data.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
import { AcademyMockApi } from 'app/mock-api/apps/academy/api';
|
||||
import { AnalyticsMockApi } from 'app/mock-api/dashboards/analytics/api';
|
||||
import { AuthMockApi } from 'app/mock-api/common/auth/api';
|
||||
import { CalendarMockApi } from 'app/mock-api/apps/calendar/api';
|
||||
import { ChatMockApi } from 'app/mock-api/apps/chat/api';
|
||||
import { ContactsMockApi } from 'app/mock-api/apps/contacts/api';
|
||||
import { ECommerceInventoryMockApi } from 'app/mock-api/apps/ecommerce/inventory/api';
|
||||
import { FileManagerMockApi } from 'app/mock-api/apps/file-manager/api';
|
||||
@@ -17,9 +19,11 @@ import { TasksMockApi } from 'app/mock-api/apps/tasks/api';
|
||||
import { UserMockApi } from 'app/mock-api/common/user/api';
|
||||
|
||||
export const mockApiServices = [
|
||||
AcademyMockApi,
|
||||
AnalyticsMockApi,
|
||||
AuthMockApi,
|
||||
CalendarMockApi,
|
||||
ChatMockApi,
|
||||
ContactsMockApi,
|
||||
ECommerceInventoryMockApi,
|
||||
FileManagerMockApi,
|
||||
|
||||
@@ -91,6 +91,21 @@ export class IconsMockApi
|
||||
}
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Material solid icons - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
this._fuseMockApiService
|
||||
.onGet('api/ui/icons/material-solid')
|
||||
.reply(() => [
|
||||
200,
|
||||
{
|
||||
namespace: 'mat_solid',
|
||||
name : 'Material Solid',
|
||||
grid : 6,
|
||||
list : cloneDeep(this._material)
|
||||
}
|
||||
]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Material outline icons - GET
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* tslint:disable:max-line-length */
|
||||
|
||||
// 20210326 - 1777 icons
|
||||
// Updated at: 20210425 - 1792 icons
|
||||
export const material = [
|
||||
'10k',
|
||||
'10mp',
|
||||
@@ -100,6 +100,7 @@ export const material = [
|
||||
'addchart',
|
||||
'adjust',
|
||||
'admin_panel_settings',
|
||||
'ads_click',
|
||||
'agriculture',
|
||||
'air',
|
||||
'airline_seat_flat',
|
||||
@@ -146,6 +147,7 @@ export const material = [
|
||||
'apps',
|
||||
'architecture',
|
||||
'archive',
|
||||
'area_chart',
|
||||
'arrow_back',
|
||||
'arrow_back_ios',
|
||||
'arrow_back_ios_new',
|
||||
@@ -195,6 +197,7 @@ export const material = [
|
||||
'autorenew',
|
||||
'av_timer',
|
||||
'baby_changing_station',
|
||||
'back_hand',
|
||||
'backpack',
|
||||
'backspace',
|
||||
'backup',
|
||||
@@ -386,6 +389,7 @@ export const material = [
|
||||
'compare',
|
||||
'compare_arrows',
|
||||
'compass_calibration',
|
||||
'compost',
|
||||
'compress',
|
||||
'computer',
|
||||
'confirmation_number',
|
||||
@@ -429,10 +433,12 @@ export const material = [
|
||||
'crop_portrait',
|
||||
'crop_rotate',
|
||||
'crop_square',
|
||||
'cruelty_free',
|
||||
'dangerous',
|
||||
'dark_mode',
|
||||
'dashboard',
|
||||
'dashboard_customize',
|
||||
'data_exploration',
|
||||
'data_saver_off',
|
||||
'data_saver_on',
|
||||
'data_usage',
|
||||
@@ -482,6 +488,7 @@ export const material = [
|
||||
'directions_walk',
|
||||
'dirty_lens',
|
||||
'disabled_by_default',
|
||||
'disabled_visible',
|
||||
'disc_full',
|
||||
'dns',
|
||||
'do_disturb',
|
||||
@@ -521,6 +528,7 @@ export const material = [
|
||||
'draw',
|
||||
'drive_eta',
|
||||
'drive_file_move',
|
||||
'drive_file_move_rtl',
|
||||
'drive_file_rename_outline',
|
||||
'drive_folder_upload',
|
||||
'dry',
|
||||
@@ -533,7 +541,6 @@ export const material = [
|
||||
'earbuds',
|
||||
'earbuds_battery',
|
||||
'east',
|
||||
'eco',
|
||||
'edgesensor_high',
|
||||
'edgesensor_low',
|
||||
'edit',
|
||||
@@ -715,6 +722,7 @@ export const material = [
|
||||
'foundation',
|
||||
'free_breakfast',
|
||||
'free_cancellation',
|
||||
'front_hand',
|
||||
'fullscreen',
|
||||
'fullscreen_exit',
|
||||
'functions',
|
||||
@@ -829,6 +837,7 @@ export const material = [
|
||||
'import_export',
|
||||
'important_devices',
|
||||
'inbox',
|
||||
'incomplete_circle',
|
||||
'indeterminate_check_box',
|
||||
'info',
|
||||
'input',
|
||||
@@ -1215,6 +1224,8 @@ export const material = [
|
||||
'pie_chart_outline',
|
||||
'pin',
|
||||
'pin_drop',
|
||||
'pin_end',
|
||||
'pin_invoke',
|
||||
'pivot_table_chart',
|
||||
'place',
|
||||
'plagiarism',
|
||||
@@ -1289,6 +1300,7 @@ export const material = [
|
||||
'recent_actors',
|
||||
'recommend',
|
||||
'record_voice_over',
|
||||
'recycling',
|
||||
'redeem',
|
||||
'redo',
|
||||
'reduce_capacity',
|
||||
@@ -1631,6 +1643,7 @@ export const material = [
|
||||
'timer_3',
|
||||
'timer_3_select',
|
||||
'timer_off',
|
||||
'tips_and_updates',
|
||||
'title',
|
||||
'toc',
|
||||
'today',
|
||||
@@ -1737,8 +1750,10 @@ export const material = [
|
||||
'watch_later',
|
||||
'water',
|
||||
'water_damage',
|
||||
'water_drop',
|
||||
'waterfall_chart',
|
||||
'waves',
|
||||
'waving_hand',
|
||||
'wb_auto',
|
||||
'wb_cloudy',
|
||||
'wb_incandescent',
|
||||
@@ -4152,87 +4167,91 @@ export const feather = [
|
||||
'zoom-in',
|
||||
'zoom-out'
|
||||
];
|
||||
// heroicons v0.4.2
|
||||
// heroicons v1.0.1 - 230 icons
|
||||
export const heroicons = [
|
||||
'academic-cap',
|
||||
'annotation',
|
||||
'arrow-circle-down',
|
||||
'adjustments',
|
||||
'archive',
|
||||
'arrow-circle-down',
|
||||
'arrow-circle-left',
|
||||
'arrow-circle-right',
|
||||
'archive',
|
||||
'arrow-circle-up',
|
||||
'arrow-down',
|
||||
'arrow-narrow-down',
|
||||
'arrow-left',
|
||||
'arrow-narrow-down',
|
||||
'arrow-narrow-left',
|
||||
'arrow-narrow-right',
|
||||
'arrow-right',
|
||||
'arrow-narrow-up',
|
||||
'arrow-right',
|
||||
'arrow-sm-left',
|
||||
'arrow-sm-down',
|
||||
'arrow-sm-right',
|
||||
'arrow-sm-up',
|
||||
'arrow-up',
|
||||
'at-symbol',
|
||||
'arrows-expand',
|
||||
'backspace',
|
||||
'at-symbol',
|
||||
'badge-check',
|
||||
'backspace',
|
||||
'ban',
|
||||
'beaker',
|
||||
'bell',
|
||||
'book-open',
|
||||
'bookmark-alt',
|
||||
'bookmark',
|
||||
'calendar',
|
||||
'briefcase',
|
||||
'calculator',
|
||||
'cake',
|
||||
'calculator',
|
||||
'calendar',
|
||||
'camera',
|
||||
'cash',
|
||||
'chart-bar',
|
||||
'chart-pie',
|
||||
'chart-square-bar',
|
||||
'chat-alt-2',
|
||||
'chat-alt',
|
||||
'chat',
|
||||
'check-circle',
|
||||
'chat-alt',
|
||||
'check',
|
||||
'chevron-double-down',
|
||||
'chevron-double-left',
|
||||
'chevron-down',
|
||||
'chevron-double-up',
|
||||
'chevron-left',
|
||||
'chevron-double-right',
|
||||
'chevron-down',
|
||||
'chevron-left',
|
||||
'chevron-right',
|
||||
'chevron-up',
|
||||
'chip',
|
||||
'clipboard-check',
|
||||
'clipboard-copy',
|
||||
'clipboard-list',
|
||||
'clock',
|
||||
'cloud-upload',
|
||||
'clipboard',
|
||||
'clock',
|
||||
'cloud-download',
|
||||
'code',
|
||||
'cloud-upload',
|
||||
'cloud',
|
||||
'code',
|
||||
'cog',
|
||||
'collection',
|
||||
'color-swatch',
|
||||
'credit-card',
|
||||
'color-swatch',
|
||||
'cube-transparent',
|
||||
'cube',
|
||||
'currency-bangladeshi',
|
||||
'currency-dollar',
|
||||
'currency-pound',
|
||||
'currency-euro',
|
||||
'currency-pound',
|
||||
'currency-rupee',
|
||||
'cursor-click',
|
||||
'currency-yen',
|
||||
'cursor-click',
|
||||
'database',
|
||||
'desktop-computer',
|
||||
'device-mobile',
|
||||
'device-tablet',
|
||||
'document-download',
|
||||
'document-add',
|
||||
'document-remove',
|
||||
'document-download',
|
||||
'document-duplicate',
|
||||
'document-remove',
|
||||
'document-report',
|
||||
'document-search',
|
||||
'document-text',
|
||||
@@ -4241,81 +4260,81 @@ export const heroicons = [
|
||||
'dots-horizontal',
|
||||
'dots-vertical',
|
||||
'download',
|
||||
'duplicate',
|
||||
'emoji-happy',
|
||||
'emoji-sad',
|
||||
'duplicate',
|
||||
'exclamation-circle',
|
||||
'exclamation',
|
||||
'external-link',
|
||||
'eye-off',
|
||||
'film',
|
||||
'fast-forward',
|
||||
'finger-print',
|
||||
'eye',
|
||||
'fast-forward',
|
||||
'film',
|
||||
'filter',
|
||||
'finger-print',
|
||||
'fire',
|
||||
'flag',
|
||||
'folder-add',
|
||||
'folder-remove',
|
||||
'folder-open',
|
||||
'folder',
|
||||
'folder-download',
|
||||
'folder-open',
|
||||
'folder-remove',
|
||||
'folder',
|
||||
'gift',
|
||||
'globe-alt',
|
||||
'hand',
|
||||
'globe',
|
||||
'hand',
|
||||
'hashtag',
|
||||
'identification',
|
||||
'heart',
|
||||
'home',
|
||||
'identification',
|
||||
'inbox-in',
|
||||
'inbox',
|
||||
'information-circle',
|
||||
'key',
|
||||
'library',
|
||||
'light-bulb',
|
||||
'lightning-bolt',
|
||||
'library',
|
||||
'link',
|
||||
'location-marker',
|
||||
'lock-closed',
|
||||
'lock-open',
|
||||
'logout',
|
||||
'mail',
|
||||
'login',
|
||||
'map',
|
||||
'logout',
|
||||
'mail-open',
|
||||
'mail',
|
||||
'map',
|
||||
'menu-alt-1',
|
||||
'menu-alt-2',
|
||||
'menu-alt-3',
|
||||
'menu-alt-4',
|
||||
'menu-alt-2',
|
||||
'menu',
|
||||
'microphone',
|
||||
'minus-circle',
|
||||
'minus-sm',
|
||||
'minus',
|
||||
'music-note',
|
||||
'moon',
|
||||
'music-note',
|
||||
'newspaper',
|
||||
'office-building',
|
||||
'paper-clip',
|
||||
'paper-airplane',
|
||||
'paper-clip',
|
||||
'pause',
|
||||
'pencil-alt',
|
||||
'pencil',
|
||||
'phone-incoming',
|
||||
'phone-missed-call',
|
||||
'phone-outgoing',
|
||||
'plus-circle',
|
||||
'play',
|
||||
'photograph',
|
||||
'phone',
|
||||
'play',
|
||||
'plus-circle',
|
||||
'plus-sm',
|
||||
'plus',
|
||||
'presentation-chart-bar',
|
||||
'puzzle',
|
||||
'presentation-chart-line',
|
||||
'qrcode',
|
||||
'printer',
|
||||
'qrcode',
|
||||
'puzzle',
|
||||
'question-mark-circle',
|
||||
'receipt-refund',
|
||||
'receipt-tax',
|
||||
@@ -4327,8 +4346,8 @@ export const heroicons = [
|
||||
'save',
|
||||
'scale',
|
||||
'scissors',
|
||||
'search',
|
||||
'search-circle',
|
||||
'search',
|
||||
'selector',
|
||||
'server',
|
||||
'share',
|
||||
@@ -4338,22 +4357,22 @@ export const heroicons = [
|
||||
'shopping-cart',
|
||||
'sort-ascending',
|
||||
'sort-descending',
|
||||
'sparkles',
|
||||
'speakerphone',
|
||||
'sparkles',
|
||||
'star',
|
||||
'status-offline',
|
||||
'status-online',
|
||||
'status-offline',
|
||||
'stop',
|
||||
'sun',
|
||||
'support',
|
||||
'switch-horizontal',
|
||||
'switch-vertical',
|
||||
'switch-horizontal',
|
||||
'table',
|
||||
'tag',
|
||||
'template',
|
||||
'thumb-down',
|
||||
'terminal',
|
||||
'thumb-up',
|
||||
'thumb-down',
|
||||
'ticket',
|
||||
'translate',
|
||||
'trash',
|
||||
@@ -4362,22 +4381,22 @@ export const heroicons = [
|
||||
'truck',
|
||||
'upload',
|
||||
'user-add',
|
||||
'user-circle',
|
||||
'user-group',
|
||||
'user',
|
||||
'user-circle',
|
||||
'user-remove',
|
||||
'user',
|
||||
'users',
|
||||
'variable',
|
||||
'video-camera',
|
||||
'view-boards',
|
||||
'variable',
|
||||
'view-grid-add',
|
||||
'view-grid',
|
||||
'view-list',
|
||||
'volume-off',
|
||||
'volume-up',
|
||||
'wifi',
|
||||
'volume-off',
|
||||
'x-circle',
|
||||
'x',
|
||||
'zoom-in',
|
||||
'zoom-out'
|
||||
'wifi',
|
||||
'zoom-out',
|
||||
'x'
|
||||
];
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden dark:bg-gray-900">
|
||||
|
||||
<mat-drawer-container class="flex-auto h-full bg-transparent">
|
||||
|
||||
<!-- Drawer -->
|
||||
<mat-drawer
|
||||
class="w-60 dark:bg-gray-900"
|
||||
[autoFocus]="false"
|
||||
[mode]="drawerMode"
|
||||
[opened]="drawerOpened"
|
||||
#drawer>
|
||||
<calendar-sidebar (calendarUpdated)="onCalendarUpdated($event)"></calendar-sidebar>
|
||||
</mat-drawer>
|
||||
|
||||
<mat-drawer-content class="flex">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col flex-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-0 flex-wrap items-center p-4 border-b bg-card">
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="toggleDrawer()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:menu'"></mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="ml-4 text-2xl font-semibold tracking-tight whitespace-nowrap">
|
||||
{{viewTitle}}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="ml-5"
|
||||
mat-icon-button
|
||||
(click)="previous()">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:chevron-left'"></mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="next()">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:chevron-right'"></mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="hidden md:inline-flex"
|
||||
mat-icon-button
|
||||
(click)="today()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:calendar'"></mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="hidden md:block ml-auto">
|
||||
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript w-30 ml-2">
|
||||
<mat-select
|
||||
(selectionChange)="changeView(viewChanger.value)"
|
||||
[value]="view"
|
||||
#viewChanger="matSelect">
|
||||
<mat-option [value]="'dayGridMonth'">Month</mat-option>
|
||||
<mat-option [value]="'timeGridWeek'">Week</mat-option>
|
||||
<mat-option [value]="'timeGridDay'">Day</mat-option>
|
||||
<mat-option [value]="'listYear'">Schedule</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div class="md:hidden ml-auto">
|
||||
<button
|
||||
class=""
|
||||
[matMenuTriggerFor]="actionsMenu"
|
||||
mat-icon-button>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:dots-vertical'"></mat-icon>
|
||||
|
||||
<mat-menu #actionsMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="today()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:calendar'"></mat-icon>
|
||||
<span>Go to today</span>
|
||||
</button>
|
||||
<button
|
||||
[matMenuTriggerFor]="actionsViewsMenu"
|
||||
mat-menu-item>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:view-grid'"></mat-icon>
|
||||
<span>View</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #actionsViewsMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="view === 'dayGridMonth'"
|
||||
(click)="changeView('dayGridMonth')">
|
||||
<span>Month</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="view === 'timeGridWeek'"
|
||||
(click)="changeView('timeGridWeek')">
|
||||
<span>Week</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="view === 'timeGridDay'"
|
||||
(click)="changeView('timeGridDay')">
|
||||
<span>Day</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="view === 'listYear'"
|
||||
(click)="changeView('listYear')">
|
||||
<span>Schedule</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FullCalendar -->
|
||||
<div class="flex flex-col flex-auto">
|
||||
<full-calendar
|
||||
[defaultView]="view"
|
||||
[events]="events"
|
||||
[firstDay]="settings.startWeekOn"
|
||||
[handleWindowResize]="false"
|
||||
[header]="false"
|
||||
[height]="'parent'"
|
||||
[plugins]="calendarPlugins"
|
||||
[views]="views"
|
||||
(dateClick)="onDateClick($event)"
|
||||
(eventClick)="onEventClick($event)"
|
||||
(eventRender)="onEventRender($event)"
|
||||
#fullCalendar></full-calendar>
|
||||
</div>
|
||||
|
||||
<!-- Event panel -->
|
||||
<ng-template #eventPanel>
|
||||
|
||||
<!-- Preview mode -->
|
||||
<ng-container *ngIf="panelMode === 'view'">
|
||||
<div class="flex-auto p-8">
|
||||
<!-- Info -->
|
||||
<div class="flex">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:information-circle'"></mat-icon>
|
||||
<div class="flex flex-auto justify-between ml-6">
|
||||
<!-- Info -->
|
||||
<div>
|
||||
<div class="text-3xl font-semibold tracking-tight leading-none">{{event.title || '(No title)'}}</div>
|
||||
<div class="mt-0.5 text-secondary">{{event.start | date:'EEEE, MMMM d'}}</div>
|
||||
<div class="text-secondary">{{recurrenceStatus}}</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex -mt-2 -mr-2 ml-10">
|
||||
|
||||
<!-- Non-recurring event -->
|
||||
<ng-container *ngIf="!event.recurrence">
|
||||
<!-- Edit -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="changeEventPanelMode('edit', 'single')">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:pencil-alt'"></mat-icon>
|
||||
</button>
|
||||
<!-- Delete -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="deleteEvent(event)">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Recurring event -->
|
||||
<ng-container *ngIf="event.recurrence">
|
||||
<!-- Edit -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="editMenu">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:pencil-alt'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #editMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="changeEventPanelMode('edit', 'single')">
|
||||
This event
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="!event.isFirstInstance"
|
||||
(click)="changeEventPanelMode('edit', 'future')">
|
||||
This and following events
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="changeEventPanelMode('edit', 'all')">
|
||||
All events
|
||||
</button>
|
||||
</mat-menu>
|
||||
<!-- Delete -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="deleteMenu">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #deleteMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="deleteEvent(event, 'single')">
|
||||
This event
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="!event.isFirstInstance"
|
||||
(click)="deleteEvent(event, 'future')">
|
||||
This and following events
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="deleteEvent(event, 'all')">
|
||||
All events
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div
|
||||
class="flex mt-6"
|
||||
*ngIf="event.description">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:menu-alt-2'"></mat-icon>
|
||||
<div class="flex-auto ml-6">{{event.description}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar -->
|
||||
<div class="flex mt-6">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:calendar'"></mat-icon>
|
||||
<div class="flex flex-auto items-center ml-6">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full"
|
||||
[ngClass]="getCalendar(event.calendarId).color"></div>
|
||||
<div class="ml-3 leading-none">{{getCalendar(event.calendarId).title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Add / Edit mode -->
|
||||
<ng-container *ngIf="panelMode === 'add' || panelMode === 'edit'">
|
||||
<form
|
||||
class="flex flex-col w-full p-6 pt-8 sm:pt-10 sm:pr-8"
|
||||
[formGroup]="eventForm">
|
||||
|
||||
<!-- Title -->
|
||||
<div class="flex items-center">
|
||||
<mat-icon
|
||||
class="hidden sm:inline-flex mr-6"
|
||||
[svgIcon]="'heroicons_outline:pencil-alt'"></mat-icon>
|
||||
<mat-form-field class="fuse-mat-no-subscript flex-auto">
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'title'"
|
||||
[placeholder]="'Event title'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Dates -->
|
||||
<div class="flex items-start mt-6">
|
||||
<mat-icon
|
||||
class="hidden sm:inline-flex mt-3 mr-6"
|
||||
[svgIcon]="'heroicons_outline:calendar'"></mat-icon>
|
||||
<div class="flex-auto">
|
||||
<fuse-date-range
|
||||
[formControlName]="'range'"
|
||||
[dateFormat]="settings.dateFormat"
|
||||
[timeRange]="!eventForm.get('allDay').value"
|
||||
[timeFormat]="settings.timeFormat"></fuse-date-range>
|
||||
<mat-checkbox
|
||||
class="mt-4"
|
||||
[color]="'primary'"
|
||||
[formControlName]="'allDay'">
|
||||
All day
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recurrence -->
|
||||
<div
|
||||
class="flex items-center mt-6"
|
||||
*ngIf="!event.recurrence || eventEditMode !== 'single'">
|
||||
<mat-icon
|
||||
class="hidden sm:inline-flex mr-6 transform -scale-x-1"
|
||||
[svgIcon]="'heroicons_outline:refresh'"></mat-icon>
|
||||
<div
|
||||
class="flex flex-auto items-center h-12 px-4 rounded-md border cursor-pointer shadow-sm border-gray-300 dark:bg-black dark:bg-opacity-5 dark:border-gray-500"
|
||||
(click)="openRecurrenceDialog()">
|
||||
<div class="flex-auto">
|
||||
{{recurrenceStatus || 'Does not repeat'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar -->
|
||||
<div class="flex items-center mt-6">
|
||||
<mat-icon
|
||||
class="hidden sm:inline-flex mr-6"
|
||||
[svgIcon]="'heroicons_outline:tag'"></mat-icon>
|
||||
<mat-form-field class="fuse-mat-no-subscript flex-auto">
|
||||
<mat-select
|
||||
[formControlName]="'calendarId'"
|
||||
(change)="$event.stopImmediatePropagation()">
|
||||
<mat-select-trigger class="inline-flex items-center leading-none">
|
||||
<span
|
||||
class="w-3 h-3 rounded-full"
|
||||
[ngClass]="getCalendar(eventForm.get('calendarId').value)?.color"></span>
|
||||
<span class="ml-3">{{getCalendar(eventForm.get('calendarId').value)?.title}}</span>
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let calendar of calendars"
|
||||
[value]="calendar.id">
|
||||
<div class="inline-flex items-center">
|
||||
<span
|
||||
class="w-3 h-3 rounded-full"
|
||||
[ngClass]="calendar.color"></span>
|
||||
<span class="ml-3">{{calendar.title}}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="flex items-start mt-6">
|
||||
<mat-icon
|
||||
class="hidden sm:inline-flex mr-6 mt-3"
|
||||
[svgIcon]="'heroicons_outline:menu-alt-2'"></mat-icon>
|
||||
<mat-form-field class="fuse-mat-textarea fuse-mat-no-subscript flex-auto">
|
||||
<textarea
|
||||
matInput
|
||||
cdkTextareaAutosize
|
||||
[cdkAutosizeMinRows]="1"
|
||||
[formControlName]="'description'"
|
||||
[placeholder]="'Event description'">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ml-auto mt-6">
|
||||
<button
|
||||
class="add"
|
||||
*ngIf="panelMode === 'add'"
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[color]="'primary'"
|
||||
(click)="addEvent()">
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
class="save"
|
||||
*ngIf="panelMode === 'edit'"
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[color]="'primary'"
|
||||
(click)="updateEvent()">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
</mat-drawer-content>
|
||||
|
||||
</mat-drawer-container>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
calendar {
|
||||
|
||||
/* Tweak: FullCalendar CSS only height to improve resize performance */
|
||||
/* With this tweak, we can disable "handleWindowResize" option of FullCalendar */
|
||||
/* which disables the height calculations on window resize and increases the */
|
||||
/* overall performance. */
|
||||
/* This tweak only affects the Calendar app's FullCalendar. */
|
||||
full-calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.fc-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.fc-view {
|
||||
|
||||
/* Day grid - Month view */
|
||||
/* Time grid - Week view */
|
||||
/* Time grid - Day view */
|
||||
&.fc-dayGridMonth-view,
|
||||
&.fc-timeGridWeek-view,
|
||||
&.fc-timeGridDay-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
height: 100%;
|
||||
|
||||
> thead {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> tbody {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
> tr {
|
||||
display: flex;
|
||||
|
||||
> td {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.fc-scroller {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden scroll !important;
|
||||
height: auto !important;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Day grid - Month view */
|
||||
&.fc-dayGridMonth-view {
|
||||
|
||||
> table {
|
||||
|
||||
> tbody {
|
||||
|
||||
> tr {
|
||||
|
||||
> td {
|
||||
|
||||
.fc-scroller {
|
||||
display: flex;
|
||||
|
||||
> .fc-day-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 580px;
|
||||
|
||||
> .fc-row {
|
||||
flex: 1 0 0;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* List - Year view */
|
||||
&.fc-listYear-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.fc-scroller {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Event panel */
|
||||
.calendar-event-panel {
|
||||
border-radius: 8px;
|
||||
@apply shadow-2xl bg-card;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { MAT_DATE_FORMATS } from '@angular/material/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatMomentDateModule } from '@angular/material-moment-adapter';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FullCalendarModule } from '@fullcalendar/angular';
|
||||
import { FuseDateRangeModule } from '@fuse/components/date-range';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { CalendarComponent } from 'app/modules/admin/apps/calendar/calendar.component';
|
||||
import { CalendarRecurrenceComponent } from 'app/modules/admin/apps/calendar/recurrence/recurrence.component';
|
||||
import { CalendarSettingsComponent } from 'app/modules/admin/apps/calendar/settings/settings.component';
|
||||
import { CalendarSidebarComponent } from 'app/modules/admin/apps/calendar/sidebar/sidebar.component';
|
||||
import { calendarRoutes } from 'app/modules/admin/apps/calendar/calendar.routing';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CalendarComponent,
|
||||
CalendarRecurrenceComponent,
|
||||
CalendarSettingsComponent,
|
||||
CalendarSidebarComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(calendarRoutes),
|
||||
ScrollingModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCheckboxModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatMomentDateModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatTooltipModule,
|
||||
FullCalendarModule,
|
||||
FuseDateRangeModule,
|
||||
SharedModule
|
||||
],
|
||||
providers : [
|
||||
{
|
||||
provide : MAT_DATE_FORMATS,
|
||||
useValue: {
|
||||
parse : {
|
||||
dateInput: 'DD.MM.YYYY'
|
||||
},
|
||||
display: {
|
||||
dateInput : 'DD.MM.YYYY',
|
||||
monthYearLabel : 'MMM YYYY',
|
||||
dateA11yLabel : 'DD.MM.YYYY',
|
||||
monthYearA11yLabel: 'MMMM YYYY'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
export class CalendarModule
|
||||
{
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CalendarService } from 'app/modules/admin/apps/calendar/calendar.service';
|
||||
import { Calendar, CalendarSettings, CalendarWeekday } from 'app/modules/admin/apps/calendar/calendar.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CalendarCalendarsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _calendarService: CalendarService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Calendar[]>
|
||||
{
|
||||
return this._calendarService.getCalendars();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CalendarSettingsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _calendarService: CalendarService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<CalendarSettings>
|
||||
{
|
||||
return this._calendarService.getSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CalendarWeekdaysResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _calendarService: CalendarService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<CalendarWeekday[]>
|
||||
{
|
||||
return this._calendarService.getWeekdays();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { CalendarComponent } from 'app/modules/admin/apps/calendar/calendar.component';
|
||||
import { CalendarSettingsComponent } from 'app/modules/admin/apps/calendar/settings/settings.component';
|
||||
import { CalendarCalendarsResolver, CalendarSettingsResolver, CalendarWeekdaysResolver } from 'app/modules/admin/apps/calendar/calendar.resolvers';
|
||||
|
||||
export const calendarRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
component: CalendarComponent,
|
||||
resolve : {
|
||||
calendars: CalendarCalendarsResolver,
|
||||
settings : CalendarSettingsResolver,
|
||||
weekdays : CalendarWeekdaysResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path : 'settings',
|
||||
component: CalendarSettingsComponent,
|
||||
resolve : {
|
||||
settings: CalendarSettingsResolver
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1,475 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { Moment } from 'moment';
|
||||
import { Calendar, CalendarEvent, CalendarEventEditMode, CalendarSettings, CalendarWeekday } from 'app/modules/admin/apps/calendar/calendar.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CalendarService
|
||||
{
|
||||
// Private
|
||||
private _calendars: BehaviorSubject<Calendar[] | null> = new BehaviorSubject(null);
|
||||
private _events: BehaviorSubject<CalendarEvent[] | null> = new BehaviorSubject(null);
|
||||
private _loadedEventsRange: { start: Moment | null, end: Moment | null } = {
|
||||
start: null,
|
||||
end : null
|
||||
};
|
||||
private readonly _numberOfDaysToPrefetch = 60;
|
||||
private _settings: BehaviorSubject<CalendarSettings | null> = new BehaviorSubject(null);
|
||||
private _weekdays: BehaviorSubject<CalendarWeekday[] | null> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _httpClient: HttpClient)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for calendars
|
||||
*/
|
||||
get calendars$(): Observable<Calendar[]>
|
||||
{
|
||||
return this._calendars.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for events
|
||||
*/
|
||||
get events$(): Observable<CalendarEvent[]>
|
||||
{
|
||||
return this._events.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for settings
|
||||
*/
|
||||
get settings$(): Observable<CalendarSettings>
|
||||
{
|
||||
return this._settings.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for weekdays
|
||||
*/
|
||||
get weekdays$(): Observable<CalendarWeekday[]>
|
||||
{
|
||||
return this._weekdays.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get calendars
|
||||
*/
|
||||
getCalendars(): Observable<Calendar[]>
|
||||
{
|
||||
return this._httpClient.get<Calendar[]>('api/apps/calendar/calendars').pipe(
|
||||
tap((response) => {
|
||||
this._calendars.next(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add calendar
|
||||
*
|
||||
* @param calendar
|
||||
*/
|
||||
addCalendar(calendar: Calendar): Observable<Calendar>
|
||||
{
|
||||
return this.calendars$.pipe(
|
||||
take(1),
|
||||
switchMap(calendars => this._httpClient.post<Calendar>('api/apps/calendar/calendars', {
|
||||
calendar
|
||||
}).pipe(
|
||||
map((addedCalendar) => {
|
||||
|
||||
// Add the calendar
|
||||
calendars.push(addedCalendar);
|
||||
|
||||
// Update the calendars
|
||||
this._calendars.next(calendars);
|
||||
|
||||
// Return the added calendar
|
||||
return addedCalendar;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update calendar
|
||||
*
|
||||
* @param id
|
||||
* @param calendar
|
||||
*/
|
||||
updateCalendar(id: string, calendar: Calendar): Observable<Calendar>
|
||||
{
|
||||
return this.calendars$.pipe(
|
||||
take(1),
|
||||
switchMap(calendars => this._httpClient.patch<Calendar>('api/apps/calendar/calendars', {
|
||||
id,
|
||||
calendar
|
||||
}).pipe(
|
||||
map((updatedCalendar) => {
|
||||
|
||||
// Find the index of the updated calendar
|
||||
const index = calendars.findIndex(item => item.id === id);
|
||||
|
||||
// Update the calendar
|
||||
calendars[index] = updatedCalendar;
|
||||
|
||||
// Update the calendars
|
||||
this._calendars.next(calendars);
|
||||
|
||||
// Return the updated calendar
|
||||
return updatedCalendar;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete calendar
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
deleteCalendar(id: string): Observable<any>
|
||||
{
|
||||
return this.calendars$.pipe(
|
||||
take(1),
|
||||
switchMap(calendars => this._httpClient.delete<Calendar>('api/apps/calendar/calendars', {
|
||||
params: {id}
|
||||
}).pipe(
|
||||
map((isDeleted) => {
|
||||
|
||||
// Find the index of the deleted calendar
|
||||
const index = calendars.findIndex(item => item.id === id);
|
||||
|
||||
// Delete the calendar
|
||||
calendars.splice(index, 1);
|
||||
|
||||
// Update the calendars
|
||||
this._calendars.next(calendars);
|
||||
|
||||
// Remove the events belong to deleted calendar
|
||||
const events = this._events.value.filter((event) => event.calendarId !== id);
|
||||
|
||||
// Update the events
|
||||
this._events.next(events);
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events
|
||||
*
|
||||
* @param start
|
||||
* @param end
|
||||
* @param replace
|
||||
*/
|
||||
getEvents(start: Moment, end: Moment, replace = false): Observable<CalendarEvent[]>
|
||||
{
|
||||
// Set the new start date for loaded events
|
||||
if ( replace || !this._loadedEventsRange.start || start.isBefore(this._loadedEventsRange.start) )
|
||||
{
|
||||
this._loadedEventsRange.start = start;
|
||||
}
|
||||
|
||||
// Set the new end date for loaded events
|
||||
if ( replace || !this._loadedEventsRange.end || end.isAfter(this._loadedEventsRange.end) )
|
||||
{
|
||||
this._loadedEventsRange.end = end;
|
||||
}
|
||||
|
||||
// Get the events
|
||||
return this._httpClient.get<CalendarEvent[]>('api/apps/calendar/events', {
|
||||
params: {
|
||||
start: start.toISOString(true),
|
||||
end : end.toISOString(true)
|
||||
}
|
||||
}).pipe(
|
||||
switchMap(response => this._events.pipe(
|
||||
take(1),
|
||||
map((events) => {
|
||||
|
||||
// If replace...
|
||||
if ( replace )
|
||||
{
|
||||
// Execute the observable with the response replacing the events object
|
||||
this._events.next(response);
|
||||
}
|
||||
// Otherwise...
|
||||
else
|
||||
{
|
||||
// If events is null, replace it with an empty array
|
||||
events = events || [];
|
||||
|
||||
// Execute the observable by appending the response to the current events
|
||||
this._events.next([...events, ...response]);
|
||||
}
|
||||
|
||||
// Return the response
|
||||
return response;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload events using the loaded events range
|
||||
*/
|
||||
reloadEvents(): Observable<CalendarEvent[]>
|
||||
{
|
||||
// Get the events
|
||||
return this._httpClient.get<CalendarEvent[]>('api/apps/calendar/events', {
|
||||
params: {
|
||||
start: this._loadedEventsRange.start.toISOString(),
|
||||
end : this._loadedEventsRange.end.toISOString()
|
||||
}
|
||||
}).pipe(
|
||||
map((response) => {
|
||||
|
||||
// Execute the observable with the response replacing the events object
|
||||
this._events.next(response);
|
||||
|
||||
// Return the response
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch future events
|
||||
*
|
||||
* @param end
|
||||
*/
|
||||
prefetchFutureEvents(end: Moment): Observable<CalendarEvent[]>
|
||||
{
|
||||
// Calculate the remaining prefetched days
|
||||
const remainingDays = this._loadedEventsRange.end.diff(end, 'days');
|
||||
|
||||
// Return if remaining days is bigger than the number
|
||||
// of days to prefetch. This means we were already been
|
||||
// there and fetched the events mock-api so no need for doing
|
||||
// it again.
|
||||
if ( remainingDays >= this._numberOfDaysToPrefetch )
|
||||
{
|
||||
return of([]);
|
||||
}
|
||||
|
||||
// Figure out the start and end dates
|
||||
const start = this._loadedEventsRange.end.clone().add(1, 'day');
|
||||
end = this._loadedEventsRange.end.clone().add(this._numberOfDaysToPrefetch - remainingDays, 'days');
|
||||
|
||||
// Prefetch the events
|
||||
return this.getEvents(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch past events
|
||||
*
|
||||
* @param start
|
||||
*/
|
||||
prefetchPastEvents(start: Moment): Observable<CalendarEvent[]>
|
||||
{
|
||||
// Calculate the remaining prefetched days
|
||||
const remainingDays = start.diff(this._loadedEventsRange.start, 'days');
|
||||
|
||||
// Return if remaining days is bigger than the number
|
||||
// of days to prefetch. This means we were already been
|
||||
// there and fetched the events mock-api so no need for doing
|
||||
// it again.
|
||||
if ( remainingDays >= this._numberOfDaysToPrefetch )
|
||||
{
|
||||
return of([]);
|
||||
}
|
||||
|
||||
// Figure out the start and end dates
|
||||
start = this._loadedEventsRange.start.clone().subtract(this._numberOfDaysToPrefetch - remainingDays, 'days');
|
||||
const end = this._loadedEventsRange.start.clone().subtract(1, 'day');
|
||||
|
||||
// Prefetch the events
|
||||
return this.getEvents(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
addEvent(event): Observable<CalendarEvent>
|
||||
{
|
||||
return this.events$.pipe(
|
||||
take(1),
|
||||
switchMap(events => this._httpClient.post<CalendarEvent>('api/apps/calendar/event', {
|
||||
event
|
||||
}).pipe(
|
||||
map((addedEvent) => {
|
||||
|
||||
// Update the events
|
||||
this._events.next(events);
|
||||
|
||||
// Return the added event
|
||||
return addedEvent;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update event
|
||||
*
|
||||
* @param id
|
||||
* @param event
|
||||
*/
|
||||
updateEvent(id: string, event): Observable<CalendarEvent>
|
||||
{
|
||||
return this.events$.pipe(
|
||||
take(1),
|
||||
switchMap(events => this._httpClient.patch<CalendarEvent>('api/apps/calendar/event', {
|
||||
id,
|
||||
event
|
||||
}).pipe(
|
||||
map((updatedEvent) => {
|
||||
|
||||
// Find the index of the updated event
|
||||
const index = events.findIndex(item => item.id === id);
|
||||
|
||||
// Update the event
|
||||
events[index] = updatedEvent;
|
||||
|
||||
// Update the events
|
||||
this._events.next(events);
|
||||
|
||||
// Return the updated event
|
||||
return updatedEvent;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update recurring event
|
||||
*
|
||||
* @param event
|
||||
* @param originalEvent
|
||||
* @param mode
|
||||
*/
|
||||
updateRecurringEvent(event, originalEvent, mode: CalendarEventEditMode): Observable<boolean>
|
||||
{
|
||||
return this._httpClient.patch<boolean>('api/apps/calendar/recurring-event', {
|
||||
event,
|
||||
originalEvent,
|
||||
mode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete event
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
deleteEvent(id: string): Observable<CalendarEvent>
|
||||
{
|
||||
return this.events$.pipe(
|
||||
take(1),
|
||||
switchMap(events => this._httpClient.delete<CalendarEvent>('api/apps/calendar/event', {params: {id}}).pipe(
|
||||
map((isDeleted) => {
|
||||
|
||||
// Find the index of the deleted event
|
||||
const index = events.findIndex(item => item.id === id);
|
||||
|
||||
// Delete the event
|
||||
events.splice(index, 1);
|
||||
|
||||
// Update the events
|
||||
this._events.next(events);
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete recurring event
|
||||
*
|
||||
* @param event
|
||||
* @param mode
|
||||
*/
|
||||
deleteRecurringEvent(event, mode: CalendarEventEditMode): Observable<boolean>
|
||||
{
|
||||
return this._httpClient.delete<boolean>('api/apps/calendar/recurring-event', {
|
||||
params: {
|
||||
event: JSON.stringify(event),
|
||||
mode
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings
|
||||
*/
|
||||
getSettings(): Observable<CalendarSettings>
|
||||
{
|
||||
return this._httpClient.get<CalendarSettings>('api/apps/calendar/settings').pipe(
|
||||
tap((response) => {
|
||||
this._settings.next(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings
|
||||
*/
|
||||
updateSettings(settings: CalendarSettings): Observable<CalendarSettings>
|
||||
{
|
||||
return this.events$.pipe(
|
||||
take(1),
|
||||
switchMap(events => this._httpClient.patch<CalendarSettings>('api/apps/calendar/settings', {
|
||||
settings
|
||||
}).pipe(
|
||||
map((updatedSettings) => {
|
||||
|
||||
// Update the settings
|
||||
this._settings.next(settings);
|
||||
|
||||
// Get weekdays again to get them in correct order
|
||||
// in case the startWeekOn setting changes
|
||||
this.getWeekdays().subscribe();
|
||||
|
||||
// Return the updated settings
|
||||
return updatedSettings;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weekdays
|
||||
*/
|
||||
getWeekdays(): Observable<CalendarWeekday[]>
|
||||
{
|
||||
return this._httpClient.get<CalendarWeekday[]>('api/apps/calendar/weekdays').pipe(
|
||||
tap((response) => {
|
||||
this._weekdays.next(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
export interface Calendar
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
color: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export type CalendarDrawerMode = 'over' | 'side';
|
||||
|
||||
export interface CalendarEvent
|
||||
{
|
||||
id: string;
|
||||
calendarId: string;
|
||||
recurringEventId: string | null;
|
||||
isFirstInstance: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
start: string | null;
|
||||
end: string | null;
|
||||
allDay: boolean;
|
||||
recurrence: string;
|
||||
}
|
||||
|
||||
export interface CalendarEventException
|
||||
{
|
||||
id: string;
|
||||
eventId: string;
|
||||
exdate: string;
|
||||
}
|
||||
|
||||
export type CalendarEventPanelMode = 'view' | 'add' | 'edit';
|
||||
export type CalendarEventEditMode = 'single' | 'future' | 'all';
|
||||
|
||||
export interface CalendarSettings
|
||||
{
|
||||
dateFormat: 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY-MM-DD' | 'll';
|
||||
timeFormat: '12' | '24';
|
||||
startWeekOn: 6 | 0 | 1;
|
||||
}
|
||||
|
||||
export interface CalendarWeekday
|
||||
{
|
||||
abbr: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
[formGroup]="recurrenceForm">
|
||||
|
||||
<div class="text-2xl font-semibold tracking-tight">Recurrence rules</div>
|
||||
|
||||
<!-- Interval and frequency -->
|
||||
<div class="flex mt-12">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-24 -mt-6">
|
||||
<mat-label>Repeat every</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[autocomplete]="'off'"
|
||||
[formControlName]="'interval'"
|
||||
[min]="1">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="fuse-mat-no-subscript w-40 ml-4">
|
||||
<mat-select [formControlName]="'freq'">
|
||||
<mat-option [value]="'DAILY'">day(s)</mat-option>
|
||||
<mat-option [value]="'WEEKLY'">week(s)</mat-option>
|
||||
<mat-option [value]="'MONTHLY'">month(s)</mat-option>
|
||||
<mat-option [value]="'YEARLY'">year(s)</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Weekly repeat options -->
|
||||
<div
|
||||
class="flex flex-col mt-6"
|
||||
[formGroupName]="'weekly'"
|
||||
*ngIf="recurrenceForm.get('freq').value === 'WEEKLY'">
|
||||
<div class="font-medium">Repeat on</div>
|
||||
<mat-button-toggle-group
|
||||
class="mt-1.5 border-0 space-x-1"
|
||||
[formControlName]="'byDay'"
|
||||
[multiple]="true">
|
||||
<mat-button-toggle
|
||||
class="w-10 h-10 border-0 rounded-full"
|
||||
*ngFor="let weekday of weekdays"
|
||||
[disableRipple]="true"
|
||||
[value]="weekday.value"
|
||||
[matTooltip]="weekday.label">
|
||||
{{weekday.abbr}}
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<!-- Monthly repeat options -->
|
||||
<div
|
||||
class="flex mt-6"
|
||||
[formGroupName]="'monthly'"
|
||||
*ngIf="recurrenceForm.get('freq').value === 'MONTHLY'">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<mat-label>Repeat on</mat-label>
|
||||
<mat-select [formControlName]="'repeatOn'">
|
||||
<mat-option [value]="'date'">Monthly on day {{recurrenceForm.get('monthly.date').value}}</mat-option>
|
||||
<mat-option [value]="'nthWeekday'">Monthly on the {{nthWeekdayText}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Ends -->
|
||||
<div
|
||||
class="flex flex-col mt-12"
|
||||
[formGroupName]="'end'">
|
||||
<div class="flex items-center">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-24 -mt-6">
|
||||
<mat-label>Ends</mat-label>
|
||||
<mat-select [formControlName]="'type'">
|
||||
<mat-option [value]="'never'">Never</mat-option>
|
||||
<mat-option [value]="'until'">On</mat-option>
|
||||
<mat-option [value]="'count'">After</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="fuse-mat-no-subscript w-40 ml-4"
|
||||
*ngIf="recurrenceForm.get('end.type').value === 'until'">
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="untilDatePicker"
|
||||
[formControlName]="'until'">
|
||||
<mat-datepicker-toggle
|
||||
matSuffix
|
||||
[for]="untilDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #untilDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="fuse-mat-no-subscript w-40 ml-4"
|
||||
*ngIf="recurrenceForm.get('end.type').value === 'count'">
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[autocomplete]="'off'"
|
||||
[formControlName]="'count'"
|
||||
[min]="1">
|
||||
<span matSuffix>occurrence(s)</span>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ml-auto mt-8">
|
||||
<button
|
||||
class="clear"
|
||||
mat-button
|
||||
[color]="'primary'"
|
||||
(click)="clear()">
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
[disabled]="recurrenceForm.invalid"
|
||||
[color]="'primary'"
|
||||
(click)="done()">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@@ -1,341 +0,0 @@
|
||||
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import * as moment from 'moment';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { CalendarService } from 'app/modules/admin/apps/calendar/calendar.service';
|
||||
import { CalendarWeekday } from 'app/modules/admin/apps/calendar/calendar.types';
|
||||
|
||||
@Component({
|
||||
selector : 'calendar-recurrence',
|
||||
templateUrl : './recurrence.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CalendarRecurrenceComponent implements OnInit, OnDestroy
|
||||
{
|
||||
nthWeekdayText: string;
|
||||
recurrenceForm: FormGroup;
|
||||
recurrenceFormValues: any;
|
||||
weekdays: CalendarWeekday[];
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
public matDialogRef: MatDialogRef<CalendarRecurrenceComponent>,
|
||||
private _calendarService: CalendarService,
|
||||
private _formBuilder: FormBuilder
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get weekdays
|
||||
this._calendarService.weekdays$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((weekdays) => {
|
||||
|
||||
// Store the weekdays
|
||||
this.weekdays = weekdays;
|
||||
});
|
||||
|
||||
// Initialize
|
||||
this._init();
|
||||
|
||||
// Create the recurrence form
|
||||
this.recurrenceForm = this._formBuilder.group({
|
||||
freq : [null],
|
||||
interval: [null, Validators.required],
|
||||
weekly : this._formBuilder.group({
|
||||
byDay: [[]]
|
||||
}),
|
||||
monthly : this._formBuilder.group({
|
||||
repeatOn : [null], // date | nthWeekday
|
||||
date : [null],
|
||||
nthWeekday: [null]
|
||||
}),
|
||||
end : this._formBuilder.group({
|
||||
type : [null], // never | until | count
|
||||
until: [null],
|
||||
count: [null]
|
||||
})
|
||||
});
|
||||
|
||||
// Subscribe to 'freq' field value changes
|
||||
this.recurrenceForm.get('freq').valueChanges.subscribe((value) => {
|
||||
|
||||
// Set the end values
|
||||
this._setEndValues(value);
|
||||
});
|
||||
|
||||
// Subscribe to 'weekly.byDay' field value changes
|
||||
this.recurrenceForm.get('weekly.byDay').valueChanges.subscribe((value) => {
|
||||
|
||||
// Get the event's start date
|
||||
const startDate = moment(this.data.event.start);
|
||||
|
||||
// If nothing is selected, select the original value from
|
||||
// the event form to prevent an empty value on the field
|
||||
if ( !value || !value.length )
|
||||
{
|
||||
// Get the day of event start date
|
||||
const eventStartDay = startDate.format('dd').toUpperCase();
|
||||
|
||||
// Set the original value back without emitting a
|
||||
// change event to prevent an infinite loop
|
||||
this.recurrenceForm.get('weekly.byDay').setValue([eventStartDay], {emitEvent: false});
|
||||
}
|
||||
});
|
||||
|
||||
// Patch the form with the values
|
||||
this.recurrenceForm.patchValue(this.recurrenceFormValues);
|
||||
|
||||
// Set end values for the first time
|
||||
this._setEndValues(this.recurrenceForm.get('freq').value);
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Clear
|
||||
*/
|
||||
clear(): void
|
||||
{
|
||||
// Close the dialog
|
||||
this.matDialogRef.close({recurrence: 'cleared'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Done
|
||||
*/
|
||||
done(): void
|
||||
{
|
||||
// Get the recurrence form values
|
||||
const recurrenceForm = this.recurrenceForm.value;
|
||||
|
||||
// Prepare the rule array and add the base rules
|
||||
const ruleArr = ['FREQ=' + recurrenceForm.freq, 'INTERVAL=' + recurrenceForm.interval];
|
||||
|
||||
// If monthly on certain days...
|
||||
if ( recurrenceForm.freq === 'MONTHLY' && recurrenceForm.monthly.repeatOn === 'nthWeekday' )
|
||||
{
|
||||
ruleArr.push('BYDAY=' + recurrenceForm.monthly.nthWeekday);
|
||||
}
|
||||
|
||||
// If weekly...
|
||||
if ( recurrenceForm.freq === 'WEEKLY' )
|
||||
{
|
||||
// If byDay is an array...
|
||||
if ( Array.isArray(recurrenceForm.weekly.byDay) )
|
||||
{
|
||||
ruleArr.push('BYDAY=' + recurrenceForm.weekly.byDay.join(','));
|
||||
}
|
||||
// Otherwise
|
||||
else
|
||||
{
|
||||
ruleArr.push('BYDAY=' + recurrenceForm.weekly.byDay);
|
||||
}
|
||||
}
|
||||
|
||||
// If one of the end options is selected...
|
||||
if ( recurrenceForm.end.type === 'until' )
|
||||
{
|
||||
ruleArr.push('UNTIL=' + moment(recurrenceForm.end.until).endOf('day').utc().format('YYYYMMDD[T]HHmmss[Z]'));
|
||||
}
|
||||
|
||||
if ( recurrenceForm.end.type === 'count' )
|
||||
{
|
||||
ruleArr.push('COUNT=' + recurrenceForm.end.count);
|
||||
}
|
||||
|
||||
// Generate rule text
|
||||
const ruleText = ruleArr.join(';');
|
||||
|
||||
// Close the dialog
|
||||
this.matDialogRef.close({recurrence: ruleText});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _init(): void
|
||||
{
|
||||
// Get the event's start date
|
||||
const startDate = moment(this.data.event.start);
|
||||
|
||||
// Calculate the weekday
|
||||
const weekday = moment(this.data.event.start).format('dd').toUpperCase();
|
||||
|
||||
// Calculate the nthWeekday
|
||||
let nthWeekdayNo = 1;
|
||||
while ( startDate.clone().isSame(startDate.clone().subtract(nthWeekdayNo, 'week'), 'month') )
|
||||
{
|
||||
nthWeekdayNo++;
|
||||
}
|
||||
const nthWeekday = nthWeekdayNo + weekday;
|
||||
|
||||
// Calculate the nthWeekday as text
|
||||
const ordinalNumberSuffixes = {
|
||||
1: 'st',
|
||||
2: 'nd',
|
||||
3: 'rd',
|
||||
4: 'th',
|
||||
5: 'th'
|
||||
};
|
||||
this.nthWeekdayText = nthWeekday.slice(0, 1) + ordinalNumberSuffixes[nthWeekday.slice(0, 1)] + ' ' +
|
||||
this.weekdays.find((item) => item.value === nthWeekday.slice(-2)).label;
|
||||
|
||||
// Set the defaults on recurrence form values
|
||||
this.recurrenceFormValues = {
|
||||
freq : 'DAILY',
|
||||
interval: 1,
|
||||
weekly : {
|
||||
byDay: weekday
|
||||
},
|
||||
monthly : {
|
||||
repeatOn : 'date',
|
||||
date : moment(this.data.event.start).date(),
|
||||
nthWeekday: nthWeekday
|
||||
},
|
||||
end : {
|
||||
type : 'never',
|
||||
until: null,
|
||||
count: null
|
||||
}
|
||||
};
|
||||
|
||||
// If recurrence rule string is available on the
|
||||
// event meaning that the is a recurring one...
|
||||
if ( this.data.event.recurrence )
|
||||
{
|
||||
// Parse the rules
|
||||
const parsedRules: any = {};
|
||||
this.data.event.recurrence.split(';').forEach((rule) => {
|
||||
parsedRules[rule.split('=')[0]] = rule.split('=')[1];
|
||||
});
|
||||
|
||||
// Overwrite the recurrence form values
|
||||
this.recurrenceFormValues.freq = parsedRules.FREQ;
|
||||
this.recurrenceFormValues.interval = parsedRules.INTERVAL;
|
||||
|
||||
if ( parsedRules.FREQ === 'WEEKLY' )
|
||||
{
|
||||
this.recurrenceFormValues.weekly.byDay = parsedRules.BYDAY.split(',');
|
||||
}
|
||||
|
||||
if ( parsedRules.FREQ === 'MONTHLY' )
|
||||
{
|
||||
this.recurrenceFormValues.monthly.repeatOn = parsedRules.BYDAY ? 'nthWeekday' : 'date';
|
||||
}
|
||||
|
||||
this.recurrenceFormValues.end.type = parsedRules.UNTIL ? 'until' : (parsedRules.COUNT ? 'count' : 'never');
|
||||
this.recurrenceFormValues.end.until = parsedRules.UNTIL || null;
|
||||
this.recurrenceFormValues.end.count = parsedRules.COUNT || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the end value based on frequency
|
||||
*
|
||||
* @param freq
|
||||
* @private
|
||||
*/
|
||||
private _setEndValues(freq: string): void
|
||||
{
|
||||
// Return if freq is not available
|
||||
if ( !freq )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the event's start date
|
||||
const startDate = moment(this.data.event.startDate);
|
||||
|
||||
// Get the end type
|
||||
const endType = this.recurrenceForm.get('end.type').value;
|
||||
|
||||
// If until is not selected
|
||||
if ( endType !== 'until' )
|
||||
{
|
||||
let until;
|
||||
|
||||
// Change the until's default value based on the frequency
|
||||
if ( freq === 'DAILY' )
|
||||
{
|
||||
until = startDate.clone().add(1, 'month').toISOString();
|
||||
}
|
||||
|
||||
if ( freq === 'WEEKLY' )
|
||||
{
|
||||
until = startDate.clone().add(12, 'weeks').toISOString();
|
||||
}
|
||||
|
||||
if ( freq === 'MONTHLY' )
|
||||
{
|
||||
until = startDate.clone().add(12, 'months').toISOString();
|
||||
}
|
||||
|
||||
if ( freq === 'YEARLY' )
|
||||
{
|
||||
until = startDate.clone().add(5, 'years').toISOString();
|
||||
}
|
||||
|
||||
// Set the until
|
||||
this.recurrenceForm.get('end.until').setValue(until);
|
||||
}
|
||||
|
||||
// If count is not selected...
|
||||
if ( endType !== 'count' )
|
||||
{
|
||||
let count;
|
||||
|
||||
// Change the count's default value based on the frequency
|
||||
if ( freq === 'DAILY' )
|
||||
{
|
||||
count = 30;
|
||||
}
|
||||
|
||||
if ( freq === 'WEEKLY' || freq === 'MONTHLY' )
|
||||
{
|
||||
count = 12;
|
||||
}
|
||||
|
||||
if ( freq === 'YEARLY' )
|
||||
{
|
||||
count = 5;
|
||||
}
|
||||
|
||||
// Set the count
|
||||
this.recurrenceForm.get('end.count').setValue(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col min-w-0 overflow-y-auto"
|
||||
cdkScrollable>
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col flex-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center h-16 px-4 sm:px-6 py-2 border-b">
|
||||
<a
|
||||
[routerLink]="['..']"
|
||||
mat-icon-button>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
||||
</a>
|
||||
<div class="ml-1 text-lg font-medium">Settings</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-auto p-6 sm:p-8">
|
||||
<form
|
||||
class="flex flex-col w-full max-w-xs"
|
||||
[formGroup]="settingsForm">
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Date format</mat-label>
|
||||
<mat-select [formControlName]="'dateFormat'">
|
||||
<mat-option [value]="'ll'">Aug 20, {{year}}</mat-option>
|
||||
<mat-option [value]="'MM/DD/YYYY'">12/31/{{year}}</mat-option>
|
||||
<mat-option [value]="'DD/MM/YYYY'">31/12/{{year}}</mat-option>
|
||||
<mat-option [value]="'YYYY-MM-DD'">{{year}}-12-31</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Time format</mat-label>
|
||||
<mat-select [formControlName]="'timeFormat'">
|
||||
<mat-option [value]="'12'">1:00pm</mat-option>
|
||||
<mat-option [value]="'24'">13:30</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Start week on</mat-label>
|
||||
<mat-select [formControlName]="'startWeekOn'">
|
||||
<mat-option [value]="6">Saturday</mat-option>
|
||||
<mat-option [value]="0">Sunday</mat-option>
|
||||
<mat-option [value]="1">Monday</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
class="mt-4"
|
||||
mat-flat-button
|
||||
[color]="'primary'"
|
||||
[disabled]="settingsForm.invalid || settingsForm.pristine"
|
||||
(click)="updateSettings()">
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,96 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { CalendarService } from 'app/modules/admin/apps/calendar/calendar.service';
|
||||
|
||||
@Component({
|
||||
selector : 'calendar-settings',
|
||||
templateUrl : './settings.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation : ViewEncapsulation.None
|
||||
})
|
||||
export class CalendarSettingsComponent implements OnInit, OnDestroy
|
||||
{
|
||||
settingsForm: FormGroup;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _calendarService: CalendarService,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _formBuilder: FormBuilder
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for current year
|
||||
*/
|
||||
get year(): string
|
||||
{
|
||||
return new Date().getFullYear().toString();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Create the event form
|
||||
this.settingsForm = this._formBuilder.group({
|
||||
dateFormat : [''],
|
||||
timeFormat : [''],
|
||||
startWeekOn: ['']
|
||||
});
|
||||
|
||||
// Get settings
|
||||
this._calendarService.settings$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((settings) => {
|
||||
|
||||
// Fill the settings form
|
||||
this.settingsForm.patchValue(settings);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
updateSettings(): void
|
||||
{
|
||||
// Get the settings
|
||||
const settings = this.settingsForm.value;
|
||||
|
||||
// Update the settings on the server
|
||||
this._calendarService.updateSettings(settings).subscribe((updatedSettings) => {
|
||||
|
||||
// Reset the form with the updated settings
|
||||
this.settingsForm.reset(updatedSettings);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export const calendarColors = [
|
||||
'bg-gray-500',
|
||||
'bg-red-500',
|
||||
'bg-orange-500',
|
||||
'bg-yellow-500',
|
||||
'bg-green-500',
|
||||
'bg-teal-500',
|
||||
'bg-blue-500',
|
||||
'bg-indigo-500',
|
||||
'bg-purple-500',
|
||||
'bg-pink-500'
|
||||
];
|
||||
@@ -1,116 +0,0 @@
|
||||
<div class="flex flex-col flex-auto min-h-full p-8">
|
||||
<div class="pb-6 text-4xl font-extrabold tracking-tight">Calendar</div>
|
||||
|
||||
<!-- Calendars -->
|
||||
<div class="group flex items-center justify-between mb-3">
|
||||
<span class="text-lg font-medium">Calendars</span>
|
||||
<mat-icon
|
||||
class="hidden group-hover:inline-flex icon-size-5 cursor-pointer"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"
|
||||
(click)="addCalendar()"></mat-icon>
|
||||
</div>
|
||||
<div
|
||||
class="group flex items-center justify-between mt-2"
|
||||
*ngFor="let calendar of calendars">
|
||||
<div
|
||||
class="flex items-center"
|
||||
(click)="toggleCalendarVisibility(calendar)">
|
||||
<mat-icon
|
||||
class="cursor-pointer"
|
||||
[svgIcon]="calendar.visible ? 'check_box' : 'check_box_outline_blank'"></mat-icon>
|
||||
<span
|
||||
class="w-3 h-3 ml-2 rounded-full"
|
||||
[ngClass]="calendar.color"></span>
|
||||
<span class="ml-2 leading-none">{{calendar.title}}</span>
|
||||
</div>
|
||||
<mat-icon
|
||||
class="hidden group-hover:inline-flex icon-size-5 cursor-pointer"
|
||||
[svgIcon]="'heroicons_solid:pencil-alt'"
|
||||
(click)="openEditPanel(calendar)"></mat-icon>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="-mx-4 mt-auto">
|
||||
<a
|
||||
class="flex items-center w-full py-3 px-4 rounded-full hover:bg-hover"
|
||||
[routerLink]="['settings']">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:cog'"></mat-icon>
|
||||
<span class="ml-2 font-medium leading-none">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Edit panel -->
|
||||
<ng-template #editPanel>
|
||||
<div class="flex flex-col w-80 p-8 shadow-2xl rounded-lg bg-card">
|
||||
<div class="text-2xl font-semibold tracking-tight">
|
||||
<ng-container *ngIf="!calendar.id">Add calendar</ng-container>
|
||||
<ng-container *ngIf="calendar.id">Edit calendar</ng-container>
|
||||
</div>
|
||||
<div class="flex items-center mt-8">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="calendar.title"
|
||||
[placeholder]="'Title'"
|
||||
required>
|
||||
<mat-select
|
||||
[(value)]="calendar.color"
|
||||
[disableOptionCentering]="true"
|
||||
matPrefix>
|
||||
<mat-select-trigger class="h-6">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:color-swatch'"></mat-icon>
|
||||
</mat-select-trigger>
|
||||
<div class="px-4 pt-5 text-xl font-semibold">Calendar color</div>
|
||||
<div class="flex flex-wrap w-48 my-4 mx-3 -mr-5">
|
||||
<mat-option
|
||||
class="relative flex w-12 h-12 p-0 cursor-pointer rounded-full bg-transparent"
|
||||
*ngFor="let color of calendarColors"
|
||||
[value]="color"
|
||||
#matOption="matOption">
|
||||
<mat-icon
|
||||
class="absolute m-3 text-white"
|
||||
*ngIf="matOption.selected"
|
||||
[svgIcon]="'heroicons_outline:check'"></mat-icon>
|
||||
<span
|
||||
class="flex w-10 h-10 m-1 rounded-full"
|
||||
[ngClass]="color"></span>
|
||||
</mat-option>
|
||||
</div>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="ml-auto mt-8 space-x-2">
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="calendar.id"
|
||||
(click)="deleteCalendar(calendar)">
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
*ngIf="calendar.id"
|
||||
[color]="'primary'"
|
||||
[disabled]="!calendar.title"
|
||||
(click)="saveCalendar(calendar)">
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
*ngIf="!calendar.id"
|
||||
(click)="closeEditPanel()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
*ngIf="!calendar.id"
|
||||
[color]="'primary'"
|
||||
[disabled]="!calendar.title"
|
||||
(click)="saveCalendar(calendar)">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -1,217 +0,0 @@
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { TemplatePortal } from '@angular/cdk/portal';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { Calendar } from 'app/modules/admin/apps/calendar/calendar.types';
|
||||
import { CalendarService } from 'app/modules/admin/apps/calendar/calendar.service';
|
||||
import { calendarColors } from 'app/modules/admin/apps/calendar/sidebar/calendar-colors';
|
||||
|
||||
@Component({
|
||||
selector : 'calendar-sidebar',
|
||||
templateUrl : './sidebar.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CalendarSidebarComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@ViewChild('editPanel') private _editPanel: TemplateRef<any>;
|
||||
@Output() readonly calendarUpdated: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
calendar: Calendar | null;
|
||||
calendarColors: any = calendarColors;
|
||||
calendars: Calendar[];
|
||||
private _editPanelOverlayRef: OverlayRef;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _calendarService: CalendarService,
|
||||
private _overlay: Overlay,
|
||||
private _viewContainerRef: ViewContainerRef
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get calendars
|
||||
this._calendarService.calendars$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((calendars) => {
|
||||
|
||||
// Store the calendars
|
||||
this.calendars = calendars;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
|
||||
// Dispose the overlay
|
||||
if ( this._editPanelOverlayRef )
|
||||
{
|
||||
this._editPanelOverlayRef.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open edit panel
|
||||
*/
|
||||
openEditPanel(calendar: Calendar): void
|
||||
{
|
||||
// Set the calendar
|
||||
this.calendar = cloneDeep(calendar);
|
||||
|
||||
// Create the overlay if it doesn't exist
|
||||
if ( !this._editPanelOverlayRef )
|
||||
{
|
||||
this._createEditPanelOverlay();
|
||||
}
|
||||
|
||||
// Attach the portal to the overlay
|
||||
this._editPanelOverlayRef.attach(new TemplatePortal(this._editPanel, this._viewContainerRef));
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the edit panel
|
||||
*/
|
||||
closeEditPanel(): void
|
||||
{
|
||||
// Detach the overlay from the portal
|
||||
if ( this._editPanelOverlayRef )
|
||||
{
|
||||
this._editPanelOverlayRef.detach();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the calendar visibility
|
||||
*
|
||||
* @param calendar
|
||||
*/
|
||||
toggleCalendarVisibility(calendar: Calendar): void
|
||||
{
|
||||
// Toggle the visibility
|
||||
calendar.visible = !calendar.visible;
|
||||
|
||||
// Update the calendar
|
||||
this.saveCalendar(calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add calendar
|
||||
*/
|
||||
addCalendar(): void
|
||||
{
|
||||
// Create a new calendar with default values
|
||||
const calendar = {
|
||||
id : null,
|
||||
title : '',
|
||||
color : 'bg-blue-500',
|
||||
visible: true
|
||||
};
|
||||
|
||||
// Open the edit panel
|
||||
this.openEditPanel(calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the calendar
|
||||
*
|
||||
* @param calendar
|
||||
*/
|
||||
saveCalendar(calendar: Calendar): void
|
||||
{
|
||||
// If there is no id on the calendar...
|
||||
if ( !calendar.id )
|
||||
{
|
||||
// Add calendar to the server
|
||||
this._calendarService.addCalendar(calendar).subscribe(() => {
|
||||
|
||||
// Close the edit panel
|
||||
this.closeEditPanel();
|
||||
|
||||
// Emit the calendarUpdated event
|
||||
this.calendarUpdated.emit();
|
||||
});
|
||||
}
|
||||
// Otherwise...
|
||||
else
|
||||
{
|
||||
// Update the calendar on the server
|
||||
this._calendarService.updateCalendar(calendar.id, calendar).subscribe(() => {
|
||||
|
||||
// Close the edit panel
|
||||
this.closeEditPanel();
|
||||
|
||||
// Emit the calendarUpdated event
|
||||
this.calendarUpdated.emit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the calendar
|
||||
*
|
||||
* @param calendar
|
||||
*/
|
||||
deleteCalendar(calendar: Calendar): void
|
||||
{
|
||||
// Delete the calendar on the server
|
||||
this._calendarService.deleteCalendar(calendar.id).subscribe(() => {
|
||||
|
||||
// Close the edit panel
|
||||
this.closeEditPanel();
|
||||
|
||||
// Emit the calendarUpdated event
|
||||
this.calendarUpdated.emit();
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create the edit panel overlay
|
||||
* @private
|
||||
*/
|
||||
private _createEditPanelOverlay(): void
|
||||
{
|
||||
// Create the overlay
|
||||
this._editPanelOverlayRef = this._overlay.create({
|
||||
hasBackdrop : true,
|
||||
scrollStrategy : this._overlay.scrollStrategies.reposition(),
|
||||
positionStrategy: this._overlay.position()
|
||||
.global()
|
||||
.centerHorizontally()
|
||||
.centerVertically()
|
||||
});
|
||||
|
||||
// Detach the overlay from the portal on backdrop click
|
||||
this._editPanelOverlayRef.backdropClick().subscribe(() => {
|
||||
this.closeEditPanel();
|
||||
this.calendar = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector : 'contacts',
|
||||
templateUrl : './contacts.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ContactsComponent
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ContactsDetailsComponent } from 'app/modules/admin/apps/contacts/details/details.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CanDeactivateContactsDetails implements CanDeactivate<ContactsDetailsComponent>
|
||||
{
|
||||
canDeactivate(
|
||||
component: ContactsDetailsComponent,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState: RouterStateSnapshot
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
|
||||
{
|
||||
// Get the next route
|
||||
let nextRoute: ActivatedRouteSnapshot = nextState.root;
|
||||
while ( nextRoute.firstChild )
|
||||
{
|
||||
nextRoute = nextRoute.firstChild;
|
||||
}
|
||||
|
||||
// If the next state doesn't contain '/contacts'
|
||||
// it means we are navigating away from the
|
||||
// contacts app
|
||||
if ( !nextState.url.includes('/contacts') )
|
||||
{
|
||||
// Let it navigate
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we are navigating to another contact...
|
||||
if ( nextRoute.paramMap.get('id') )
|
||||
{
|
||||
// Just navigate
|
||||
return true;
|
||||
}
|
||||
// Otherwise...
|
||||
else
|
||||
{
|
||||
// Close the drawer first, and then navigate
|
||||
return component.closeDrawer().then(() => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MAT_DATE_FORMATS, MatRippleModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatMomentDateModule } from '@angular/material-moment-adapter';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import * as moment from 'moment';
|
||||
import { FuseAutogrowModule } from '@fuse/directives/autogrow';
|
||||
import { FuseFindByKeyPipeModule } from '@fuse/pipes/find-by-key';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { contactsRoutes } from 'app/modules/admin/apps/contacts/contacts.routing';
|
||||
import { ContactsComponent } from 'app/modules/admin/apps/contacts/contacts.component';
|
||||
import { ContactsDetailsComponent } from 'app/modules/admin/apps/contacts/details/details.component';
|
||||
import { ContactsListComponent } from 'app/modules/admin/apps/contacts/list/list.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ContactsComponent,
|
||||
ContactsListComponent,
|
||||
ContactsDetailsComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(contactsRoutes),
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDatepickerModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatMomentDateModule,
|
||||
MatProgressBarModule,
|
||||
MatRadioModule,
|
||||
MatRippleModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatTableModule,
|
||||
MatTooltipModule,
|
||||
FuseAutogrowModule,
|
||||
FuseFindByKeyPipeModule,
|
||||
SharedModule
|
||||
],
|
||||
providers : [
|
||||
{
|
||||
provide : MAT_DATE_FORMATS,
|
||||
useValue: {
|
||||
parse : {
|
||||
dateInput: moment.ISO_8601
|
||||
},
|
||||
display: {
|
||||
dateInput : 'LL',
|
||||
monthYearLabel : 'MMM YYYY',
|
||||
dateA11yLabel : 'LL',
|
||||
monthYearA11yLabel: 'MMMM YYYY'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
export class ContactsModule
|
||||
{
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { ContactsService } from 'app/modules/admin/apps/contacts/contacts.service';
|
||||
import { Contact, Country, Tag } from 'app/modules/admin/apps/contacts/contacts.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _contactsService: ContactsService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Contact[]>
|
||||
{
|
||||
return this._contactsService.getContacts();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactsContactResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _contactsService: ContactsService,
|
||||
private _router: Router
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Contact>
|
||||
{
|
||||
return this._contactsService.getContactById(route.paramMap.get('id'))
|
||||
.pipe(
|
||||
// Error here means the requested contact is not available
|
||||
catchError((error) => {
|
||||
|
||||
// Log the error
|
||||
console.error(error);
|
||||
|
||||
// Get the parent url
|
||||
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
||||
|
||||
// Navigate to there
|
||||
this._router.navigateByUrl(parentUrl);
|
||||
|
||||
// Throw an error
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactsCountriesResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _contactsService: ContactsService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Country[]>
|
||||
{
|
||||
return this._contactsService.getCountries();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactsTagsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _contactsService: ContactsService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Tag[]>
|
||||
{
|
||||
return this._contactsService.getTags();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { CanDeactivateContactsDetails } from 'app/modules/admin/apps/contacts/contacts.guards';
|
||||
import { ContactsContactResolver, ContactsCountriesResolver, ContactsResolver, ContactsTagsResolver } from 'app/modules/admin/apps/contacts/contacts.resolvers';
|
||||
import { ContactsComponent } from 'app/modules/admin/apps/contacts/contacts.component';
|
||||
import { ContactsListComponent } from 'app/modules/admin/apps/contacts/list/list.component';
|
||||
import { ContactsDetailsComponent } from 'app/modules/admin/apps/contacts/details/details.component';
|
||||
|
||||
export const contactsRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
component: ContactsComponent,
|
||||
resolve : {
|
||||
tags: ContactsTagsResolver
|
||||
},
|
||||
children : [
|
||||
{
|
||||
path : '',
|
||||
component: ContactsListComponent,
|
||||
resolve : {
|
||||
tasks : ContactsResolver,
|
||||
countries: ContactsCountriesResolver
|
||||
},
|
||||
children : [
|
||||
{
|
||||
path : ':id',
|
||||
component : ContactsDetailsComponent,
|
||||
resolve : {
|
||||
task : ContactsContactResolver,
|
||||
countries: ContactsCountriesResolver
|
||||
},
|
||||
canDeactivate: [CanDeactivateContactsDetails]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,389 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { Contact, Country, Tag } from 'app/modules/admin/apps/contacts/contacts.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ContactsService
|
||||
{
|
||||
// Private
|
||||
private _contact: BehaviorSubject<Contact | null> = new BehaviorSubject(null);
|
||||
private _contacts: BehaviorSubject<Contact[] | null> = new BehaviorSubject(null);
|
||||
private _countries: BehaviorSubject<Country[] | null> = new BehaviorSubject(null);
|
||||
private _tags: BehaviorSubject<Tag[] | null> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _httpClient: HttpClient)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for contact
|
||||
*/
|
||||
get contact$(): Observable<Contact>
|
||||
{
|
||||
return this._contact.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for contacts
|
||||
*/
|
||||
get contacts$(): Observable<Contact[]>
|
||||
{
|
||||
return this._contacts.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for countries
|
||||
*/
|
||||
get countries$(): Observable<Country[]>
|
||||
{
|
||||
return this._countries.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for tags
|
||||
*/
|
||||
get tags$(): Observable<Tag[]>
|
||||
{
|
||||
return this._tags.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get contacts
|
||||
*/
|
||||
getContacts(): Observable<Contact[]>
|
||||
{
|
||||
return this._httpClient.get<Contact[]>('api/apps/contacts/all').pipe(
|
||||
tap((contacts) => {
|
||||
this._contacts.next(contacts);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search contacts with given query
|
||||
*
|
||||
* @param query
|
||||
*/
|
||||
searchContacts(query: string): Observable<Contact[]>
|
||||
{
|
||||
return this._httpClient.get<Contact[]>('api/apps/contacts/search', {
|
||||
params: {query}
|
||||
}).pipe(
|
||||
tap((contacts) => {
|
||||
this._contacts.next(contacts);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contact by id
|
||||
*/
|
||||
getContactById(id: string): Observable<Contact>
|
||||
{
|
||||
return this._contacts.pipe(
|
||||
take(1),
|
||||
map((contacts) => {
|
||||
|
||||
// Find the contact
|
||||
const contact = contacts.find(item => item.id === id) || null;
|
||||
|
||||
// Update the contact
|
||||
this._contact.next(contact);
|
||||
|
||||
// Return the contact
|
||||
return contact;
|
||||
}),
|
||||
switchMap((contact) => {
|
||||
|
||||
if ( !contact )
|
||||
{
|
||||
return throwError('Could not found contact with id of ' + id + '!');
|
||||
}
|
||||
|
||||
return of(contact);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create contact
|
||||
*/
|
||||
createContact(): Observable<Contact>
|
||||
{
|
||||
return this.contacts$.pipe(
|
||||
take(1),
|
||||
switchMap((contacts) => this._httpClient.post<Contact>('api/apps/contacts/contact', {}).pipe(
|
||||
map((newContact) => {
|
||||
|
||||
// Update the contacts with the new contact
|
||||
this._contacts.next([newContact, ...contacts]);
|
||||
|
||||
// Return the new contact
|
||||
return newContact;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update contact
|
||||
*
|
||||
* @param id
|
||||
* @param contact
|
||||
*/
|
||||
updateContact(id: string, contact: Contact): Observable<Contact>
|
||||
{
|
||||
return this.contacts$.pipe(
|
||||
take(1),
|
||||
switchMap(contacts => this._httpClient.patch<Contact>('api/apps/contacts/contact', {
|
||||
id,
|
||||
contact
|
||||
}).pipe(
|
||||
map((updatedContact) => {
|
||||
|
||||
// Find the index of the updated contact
|
||||
const index = contacts.findIndex(item => item.id === id);
|
||||
|
||||
// Update the contact
|
||||
contacts[index] = updatedContact;
|
||||
|
||||
// Update the contacts
|
||||
this._contacts.next(contacts);
|
||||
|
||||
// Return the updated contact
|
||||
return updatedContact;
|
||||
}),
|
||||
switchMap(updatedContact => this.contact$.pipe(
|
||||
take(1),
|
||||
filter(item => item && item.id === id),
|
||||
tap(() => {
|
||||
|
||||
// Update the contact if it's selected
|
||||
this._contact.next(updatedContact);
|
||||
|
||||
// Return the updated contact
|
||||
return updatedContact;
|
||||
})
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the contact
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
deleteContact(id: string): Observable<boolean>
|
||||
{
|
||||
return this.contacts$.pipe(
|
||||
take(1),
|
||||
switchMap(contacts => this._httpClient.delete('api/apps/contacts/contact', {params: {id}}).pipe(
|
||||
map((isDeleted: boolean) => {
|
||||
|
||||
// Find the index of the deleted contact
|
||||
const index = contacts.findIndex(item => item.id === id);
|
||||
|
||||
// Delete the contact
|
||||
contacts.splice(index, 1);
|
||||
|
||||
// Update the contacts
|
||||
this._contacts.next(contacts);
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get countries
|
||||
*/
|
||||
getCountries(): Observable<Country[]>
|
||||
{
|
||||
return this._httpClient.get<Country[]>('api/apps/contacts/countries').pipe(
|
||||
tap((countries) => {
|
||||
this._countries.next(countries);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags
|
||||
*/
|
||||
getTags(): Observable<Tag[]>
|
||||
{
|
||||
return this._httpClient.get<Tag[]>('api/apps/contacts/tags').pipe(
|
||||
tap((tags) => {
|
||||
this._tags.next(tags);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tag
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
createTag(tag: Tag): Observable<Tag>
|
||||
{
|
||||
return this.tags$.pipe(
|
||||
take(1),
|
||||
switchMap(tags => this._httpClient.post<Tag>('api/apps/contacts/tag', {tag}).pipe(
|
||||
map((newTag) => {
|
||||
|
||||
// Update the tags with the new tag
|
||||
this._tags.next([...tags, newTag]);
|
||||
|
||||
// Return new tag from observable
|
||||
return newTag;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tag
|
||||
*
|
||||
* @param id
|
||||
* @param tag
|
||||
*/
|
||||
updateTag(id: string, tag: Tag): Observable<Tag>
|
||||
{
|
||||
return this.tags$.pipe(
|
||||
take(1),
|
||||
switchMap(tags => this._httpClient.patch<Tag>('api/apps/contacts/tag', {
|
||||
id,
|
||||
tag
|
||||
}).pipe(
|
||||
map((updatedTag) => {
|
||||
|
||||
// Find the index of the updated tag
|
||||
const index = tags.findIndex(item => item.id === id);
|
||||
|
||||
// Update the tag
|
||||
tags[index] = updatedTag;
|
||||
|
||||
// Update the tags
|
||||
this._tags.next(tags);
|
||||
|
||||
// Return the updated tag
|
||||
return updatedTag;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the tag
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
deleteTag(id: string): Observable<boolean>
|
||||
{
|
||||
return this.tags$.pipe(
|
||||
take(1),
|
||||
switchMap(tags => this._httpClient.delete('api/apps/contacts/tag', {params: {id}}).pipe(
|
||||
map((isDeleted: boolean) => {
|
||||
|
||||
// Find the index of the deleted tag
|
||||
const index = tags.findIndex(item => item.id === id);
|
||||
|
||||
// Delete the tag
|
||||
tags.splice(index, 1);
|
||||
|
||||
// Update the tags
|
||||
this._tags.next(tags);
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
}),
|
||||
filter(isDeleted => isDeleted),
|
||||
switchMap(isDeleted => this.contacts$.pipe(
|
||||
take(1),
|
||||
map((contacts) => {
|
||||
|
||||
// Iterate through the contacts
|
||||
contacts.forEach((contact) => {
|
||||
|
||||
const tagIndex = contact.tags.findIndex(tag => tag === id);
|
||||
|
||||
// If the contact has the tag, remove it
|
||||
if ( tagIndex > -1 )
|
||||
{
|
||||
contact.tags.splice(tagIndex, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
})
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the avatar of the given contact
|
||||
*
|
||||
* @param id
|
||||
* @param avatar
|
||||
*/
|
||||
uploadAvatar(id: string, avatar: File): Observable<Contact>
|
||||
{
|
||||
return this.contacts$.pipe(
|
||||
take(1),
|
||||
switchMap(contacts => this._httpClient.post<Contact>('api/apps/contacts/avatar', {
|
||||
id,
|
||||
avatar
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': avatar.type
|
||||
}
|
||||
}).pipe(
|
||||
map((updatedContact) => {
|
||||
|
||||
// Find the index of the updated contact
|
||||
const index = contacts.findIndex(item => item.id === id);
|
||||
|
||||
// Update the contact
|
||||
contacts[index] = updatedContact;
|
||||
|
||||
// Update the contacts
|
||||
this._contacts.next(contacts);
|
||||
|
||||
// Return the updated contact
|
||||
return updatedContact;
|
||||
}),
|
||||
switchMap(updatedContact => this.contact$.pipe(
|
||||
take(1),
|
||||
filter(item => item && item.id === id),
|
||||
tap(() => {
|
||||
|
||||
// Update the contact if it's selected
|
||||
this._contact.next(updatedContact);
|
||||
|
||||
// Return the updated contact
|
||||
return updatedContact;
|
||||
})
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export interface Contact
|
||||
{
|
||||
id: string;
|
||||
avatar?: string | null;
|
||||
background?: string | null;
|
||||
name: string;
|
||||
emails?: {
|
||||
email: string,
|
||||
label: string
|
||||
}[];
|
||||
phoneNumbers?: {
|
||||
country: string;
|
||||
number: string;
|
||||
label: string
|
||||
}[];
|
||||
title?: string;
|
||||
company?: string;
|
||||
birthday?: string | null;
|
||||
address?: string | null;
|
||||
notes?: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface Country
|
||||
{
|
||||
id: string;
|
||||
iso: string;
|
||||
name: string;
|
||||
code: string;
|
||||
flagImagePos: string;
|
||||
}
|
||||
|
||||
export interface Tag
|
||||
{
|
||||
id?: string;
|
||||
title?: string;
|
||||
}
|
||||
@@ -1,641 +0,0 @@
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
<!-- View mode -->
|
||||
<ng-container *ngIf="!editMode">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="relative w-full h-40 sm:h-48 px-8 sm:px-12 bg-accent-100 dark:bg-accent-700">
|
||||
<!-- Background -->
|
||||
<ng-container *ngIf="contact.background">
|
||||
<img
|
||||
class="absolute inset-0 object-cover w-full h-full"
|
||||
[src]="contact.background">
|
||||
</ng-container>
|
||||
<!-- Close button -->
|
||||
<div class="flex items-center justify-end w-full max-w-3xl mx-auto pt-6">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="'Close'"
|
||||
[routerLink]="['../']">
|
||||
<mat-icon
|
||||
class="text-white"
|
||||
[svgIcon]="'heroicons_outline:x'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact -->
|
||||
<div class="relative flex flex-col flex-auto items-center p-6 pt-0 sm:p-12 sm:pt-0">
|
||||
<div class="w-full max-w-3xl">
|
||||
|
||||
<!-- Avatar and actions -->
|
||||
<div class="flex flex-auto items-end -mt-16">
|
||||
<!-- Avatar -->
|
||||
<div class="flex items-center justify-center w-32 h-32 rounded-full overflow-hidden ring-4 ring-bg-card">
|
||||
<img
|
||||
class="object-cover w-full h-full"
|
||||
*ngIf="contact.avatar"
|
||||
[src]="contact.avatar">
|
||||
<div
|
||||
class="flex items-center justify-center w-full h-full rounded overflow-hidden uppercase text-8xl font-bold leading-none bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
*ngIf="!contact.avatar">
|
||||
{{contact.name.charAt(0)}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center ml-auto mb-1">
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="toggleEditMode(true)">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:pencil-alt'"></mat-icon>
|
||||
<span class="ml-2">Edit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="mt-3 text-4xl font-bold truncate">{{contact.name}}</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<ng-container *ngIf="contact.tags.length">
|
||||
<div class="flex flex-wrap items-center mt-2">
|
||||
<!-- Tag -->
|
||||
<ng-container *ngFor="let tag of (contact.tags | fuseFindByKey:'id':tags); trackBy: trackByFn">
|
||||
<div class="flex items-center justify-center py-1 px-3 mr-3 mb-3 rounded-full leading-normal text-gray-500 bg-gray-100 dark:text-gray-300 dark:bg-gray-700">
|
||||
<span class="text-sm font-medium whitespace-nowrap">{{tag.title}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="flex flex-col mt-4 pt-6 border-t space-y-8">
|
||||
<!-- Title -->
|
||||
<ng-container *ngIf="contact.title">
|
||||
<div class="flex sm:items-center">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:briefcase'"></mat-icon>
|
||||
<div class="ml-6 leading-6">{{contact.title}}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Company -->
|
||||
<ng-container *ngIf="contact.company">
|
||||
<div class="flex sm:items-center">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:office-building'"></mat-icon>
|
||||
<div class="ml-6 leading-6">{{contact.company}}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Emails -->
|
||||
<ng-container *ngIf="contact.emails.length">
|
||||
<div class="flex">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:mail'"></mat-icon>
|
||||
<div class="min-w-0 ml-6 space-y-1">
|
||||
<ng-container *ngFor="let email of contact.emails; trackBy: trackByFn">
|
||||
<div class="flex items-center leading-6">
|
||||
<a
|
||||
class="hover:underline text-primary-500"
|
||||
[href]="'mailto:' + email.email"
|
||||
target="_blank">
|
||||
{{email.email}}
|
||||
</a>
|
||||
<div
|
||||
class="text-md truncate text-secondary"
|
||||
*ngIf="email.label">
|
||||
<span class="mx-2">•</span>
|
||||
<span class="font-medium">{{email.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Phone -->
|
||||
<ng-container *ngIf="contact.phoneNumbers.length">
|
||||
<div class="flex">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:phone'"></mat-icon>
|
||||
<div class="min-w-0 ml-6 space-y-1">
|
||||
<ng-container *ngFor="let phoneNumber of contact.phoneNumbers; trackBy: trackByFn">
|
||||
<div class="flex items-center leading-6">
|
||||
<div
|
||||
class="hidden sm:flex w-6 h-4 overflow-hidden"
|
||||
[matTooltip]="getCountryByIso(phoneNumber.country).name"
|
||||
[style.background]="'url(\'/assets/images/apps/contacts/flags.png\') no-repeat 0 0'"
|
||||
[style.backgroundSize]="'24px 3876px'"
|
||||
[style.backgroundPosition]="getCountryByIso(phoneNumber.country).flagImagePos"></div>
|
||||
<div class="sm:ml-3 font-mono">{{getCountryByIso(phoneNumber.country).code}}</div>
|
||||
<div class="ml-2.5 font-mono">{{phoneNumber.number}}</div>
|
||||
<div
|
||||
class="text-md truncate text-secondary"
|
||||
*ngIf="phoneNumber.label">
|
||||
<span class="mx-2">•</span>
|
||||
<span class="font-medium">{{phoneNumber.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Address -->
|
||||
<ng-container *ngIf="contact.address">
|
||||
<div class="flex sm:items-center">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:location-marker'"></mat-icon>
|
||||
<div class="ml-6 leading-6">{{contact.address}}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Birthday -->
|
||||
<ng-container *ngIf="contact.birthday">
|
||||
<div class="flex sm:items-center">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:cake'"></mat-icon>
|
||||
<div class="ml-6 leading-6">{{contact.birthday | date:'longDate'}}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Notes -->
|
||||
<ng-container *ngIf="contact.notes">
|
||||
<div class="flex">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:menu-alt-2'"></mat-icon>
|
||||
<div
|
||||
class="max-w-none ml-6 prose prose-sm"
|
||||
[innerHTML]="contact.notes"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<ng-container *ngIf="editMode">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="relative w-full h-40 sm:h-48 px-8 sm:px-12 bg-accent-100 dark:bg-accent-700">
|
||||
<!-- Background -->
|
||||
<ng-container *ngIf="contact.background">
|
||||
<img
|
||||
class="absolute inset-0 object-cover w-full h-full"
|
||||
[src]="contact.background">
|
||||
</ng-container>
|
||||
<!-- Close button -->
|
||||
<div class="flex items-center justify-end w-full max-w-3xl mx-auto pt-6">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="'Close'"
|
||||
[routerLink]="['../']">
|
||||
<mat-icon
|
||||
class="text-white"
|
||||
[svgIcon]="'heroicons_outline:x'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact form -->
|
||||
<div class="relative flex flex-col flex-auto items-center px-6 sm:px-12">
|
||||
<div class="w-full max-w-3xl">
|
||||
<form [formGroup]="contactForm">
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="flex flex-auto items-end -mt-16">
|
||||
<div class="relative flex items-center justify-center w-32 h-32 rounded-full overflow-hidden ring-4 ring-bg-card">
|
||||
<!-- Upload / Remove avatar -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50 z-10"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center z-20">
|
||||
<div>
|
||||
<input
|
||||
id="avatar-file-input"
|
||||
class="absolute h-0 w-0 opacity-0 invisible pointer-events-none"
|
||||
type="file"
|
||||
[multiple]="false"
|
||||
[accept]="'image/jpeg, image/png'"
|
||||
(change)="uploadAvatar(avatarFileInput.files)"
|
||||
#avatarFileInput>
|
||||
<label
|
||||
class="flex items-center justify-center w-10 h-10 rounded-full cursor-pointer hover:bg-hover"
|
||||
for="avatar-file-input"
|
||||
matRipple>
|
||||
<mat-icon
|
||||
class="text-white"
|
||||
[svgIcon]="'heroicons_outline:camera'"></mat-icon>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="removeAvatar()">
|
||||
<mat-icon
|
||||
class="text-white"
|
||||
[svgIcon]="'heroicons_outline:trash'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Image/Letter -->
|
||||
<img
|
||||
class="object-cover w-full h-full"
|
||||
*ngIf="contact.avatar"
|
||||
[src]="contact.avatar">
|
||||
<div
|
||||
class="flex items-center justify-center w-full h-full rounded overflow-hidden uppercase text-8xl font-bold leading-none bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
*ngIf="!contact.avatar">
|
||||
{{contact.name.charAt(0)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="mt-8">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<mat-label>Name</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:user-circle'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'name'"
|
||||
[placeholder]="'Name'"
|
||||
[spellcheck]="false">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap items-center -m-1.5 mt-6">
|
||||
<!-- Tags -->
|
||||
<ng-container *ngIf="contact.tags.length">
|
||||
<ng-container *ngFor="let tag of (contact.tags | fuseFindByKey:'id':tags); trackBy: trackByFn">
|
||||
<div class="flex items-center justify-center px-4 m-1.5 rounded-full leading-9 text-gray-500 bg-gray-100 dark:text-gray-300 dark:bg-gray-700">
|
||||
<span class="text-md font-medium whitespace-nowrap">{{tag.title}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<!-- Tags panel and its button -->
|
||||
<div
|
||||
class="flex items-center justify-center px-4 m-1.5 rounded-full leading-9 cursor-pointer text-gray-500 bg-gray-100 dark:text-gray-300 dark:bg-gray-700"
|
||||
(click)="openTagsPanel()"
|
||||
#tagsPanelOrigin>
|
||||
|
||||
<ng-container *ngIf="contact.tags.length">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:pencil-alt'"></mat-icon>
|
||||
<span class="ml-1.5 text-md font-medium whitespace-nowrap">Edit</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!contact.tags.length">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<span class="ml-1.5 text-md font-medium whitespace-nowrap">Add</span>
|
||||
</ng-container>
|
||||
|
||||
<!-- Tags panel -->
|
||||
<ng-template #tagsPanel>
|
||||
<div class="w-60 rounded border shadow-md bg-card">
|
||||
<!-- Tags panel header -->
|
||||
<div class="flex items-center m-3 mr-2">
|
||||
<div class="flex items-center">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:search'"></mat-icon>
|
||||
<div class="ml-2">
|
||||
<input
|
||||
class="w-full min-w-0 py-1 border-0"
|
||||
type="text"
|
||||
placeholder="Enter tag name"
|
||||
(input)="filterTags($event)"
|
||||
(keydown)="filterTagsInputKeyDown($event)"
|
||||
[maxLength]="30"
|
||||
#newTagInput>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="ml-1"
|
||||
mat-icon-button
|
||||
(click)="toggleTagsEditMode()">
|
||||
<mat-icon
|
||||
*ngIf="!tagsEditMode"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:pencil-alt'"></mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="tagsEditMode"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:check'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col max-h-64 py-2 border-t overflow-y-auto">
|
||||
<!-- Tags -->
|
||||
<ng-container *ngIf="!tagsEditMode">
|
||||
<div
|
||||
*ngFor="let tag of filteredTags; trackBy: trackByFn"
|
||||
class="flex items-center h-10 min-h-10 px-4 cursor-pointer hover:bg-hover"
|
||||
(click)="toggleContactTag(tag)"
|
||||
matRipple>
|
||||
<mat-checkbox
|
||||
class="flex items-center h-10 min-h-10"
|
||||
[color]="'primary'"
|
||||
[checked]="contact.tags.includes(tag.id)">
|
||||
</mat-checkbox>
|
||||
<div class="ml-1">{{tag.title}}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Tags editing -->
|
||||
<ng-container *ngIf="tagsEditMode">
|
||||
<div class="py-2 space-y-2">
|
||||
<div
|
||||
class="flex items-center"
|
||||
*ngFor="let tag of filteredTags; trackBy: trackByFn">
|
||||
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript w-full mx-4">
|
||||
<input
|
||||
matInput
|
||||
[value]="tag.title"
|
||||
(input)="updateTagTitle(tag, $event)">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="deleteTag(tag)"
|
||||
matSuffix>
|
||||
<mat-icon
|
||||
class="icon-size-5 ml-2"
|
||||
[svgIcon]="'heroicons_solid:trash'"></mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Create tag -->
|
||||
<div
|
||||
class="flex items-center h-10 min-h-10 -ml-0.5 pl-4 pr-3 leading-none cursor-pointer hover:bg-hover"
|
||||
*ngIf="shouldShowCreateTagButton(newTagInput.value)"
|
||||
(click)="createTag(newTagInput.value); newTagInput.value = ''"
|
||||
matRipple>
|
||||
<mat-icon
|
||||
class="mr-2 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<div class="break-all">Create "<b>{{newTagInput.value}}</b>"</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="mt-8">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<mat-label>Title</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:briefcase'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'title'"
|
||||
[placeholder]="'Job title'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Company -->
|
||||
<div class="mt-8">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<mat-label>Company</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:office-building'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'company'"
|
||||
[placeholder]="'Company'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Emails -->
|
||||
<div class="mt-8">
|
||||
<div class="space-y-4">
|
||||
<ng-container *ngFor="let email of contactForm.get('emails')['controls']; let i = index; let first = first; let last = last; trackBy: trackByFn">
|
||||
<div class="flex">
|
||||
<mat-form-field class="fuse-mat-no-subscript flex-auto">
|
||||
<mat-label *ngIf="first">Email</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:mail'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="email.get('email')"
|
||||
[placeholder]="'Email address'"
|
||||
[spellcheck]="false">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="fuse-mat-no-subscript flex-auto w-full max-w-24 sm:max-w-40 ml-2 sm:ml-4">
|
||||
<mat-label *ngIf="first">Label</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:tag'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="email.get('label')"
|
||||
[placeholder]="'Label'">
|
||||
</mat-form-field>
|
||||
<!-- Remove email -->
|
||||
<ng-container *ngIf="!(first && last)">
|
||||
<div
|
||||
class="flex items-center w-10 pl-2"
|
||||
[ngClass]="{'mt-6': first}">
|
||||
<button
|
||||
class="w-8 h-8 min-h-8"
|
||||
mat-icon-button
|
||||
(click)="removeEmailField(i)"
|
||||
matTooltip="Remove">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:trash'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="group inline-flex items-center mt-2 -ml-4 py-2 px-4 rounded cursor-pointer"
|
||||
(click)="addEmailField()">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<span class="ml-2 font-medium text-secondary group-hover:underline">Add an email address</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone numbers -->
|
||||
<div class="mt-8">
|
||||
<div class="space-y-4">
|
||||
<ng-container *ngFor="let phoneNumber of contactForm.get('phoneNumbers')['controls']; let i = index; let first = first; let last = last; trackBy: trackByFn">
|
||||
<div class="relative flex">
|
||||
<mat-form-field class="fuse-mat-no-subscript flex-auto">
|
||||
<mat-label *ngIf="first">Phone</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="phoneNumber.get('number')"
|
||||
[placeholder]="'Phone'">
|
||||
<mat-select
|
||||
class="mr-1.5"
|
||||
[formControl]="phoneNumber.get('country')"
|
||||
matPrefix>
|
||||
<mat-select-trigger>
|
||||
<span class="flex items-center">
|
||||
<span
|
||||
class="hidden sm:flex w-6 h-4 mr-1 overflow-hidden"
|
||||
[style.background]="'url(\'/assets/images/apps/contacts/flags.png\') no-repeat 0 0'"
|
||||
[style.backgroundSize]="'24px 3876px'"
|
||||
[style.backgroundPosition]="getCountryByIso(phoneNumber.get('country').value).flagImagePos"></span>
|
||||
<span class="sm:mx-0.5 font-medium text-default">{{getCountryByIso(phoneNumber.get('country').value).code}}</span>
|
||||
</span>
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let country of countries; trackBy: trackByFn"
|
||||
[value]="country.iso">
|
||||
<span class="flex items-center">
|
||||
<span
|
||||
class="w-6 h-4 overflow-hidden"
|
||||
[style.background]="'url(\'/assets/images/apps/contacts/flags.png\') no-repeat 0 0'"
|
||||
[style.backgroundSize]="'24px 3876px'"
|
||||
[style.backgroundPosition]="country.flagImagePos"></span>
|
||||
<span class="ml-2">{{country.name}}</span>
|
||||
<span class="ml-2 font-medium">{{country.code}}</span>
|
||||
</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="fuse-mat-no-subscript flex-auto w-full max-w-24 sm:max-w-40 ml-2 sm:ml-4">
|
||||
<mat-label *ngIf="first">Label</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:tag'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="phoneNumber.get('label')"
|
||||
[placeholder]="'Label'">
|
||||
</mat-form-field>
|
||||
<!-- Remove phone number -->
|
||||
<ng-container *ngIf="!(first && last)">
|
||||
<div
|
||||
class="flex items-center w-10 pl-2"
|
||||
[ngClass]="{'mt-6': first}">
|
||||
<button
|
||||
class="w-8 h-8 min-h-8"
|
||||
mat-icon-button
|
||||
(click)="removePhoneNumberField(i)"
|
||||
matTooltip="Remove">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:trash'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="group inline-flex items-center mt-2 -ml-4 py-2 px-4 rounded cursor-pointer"
|
||||
(click)="addPhoneNumberField()">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<span class="ml-2 font-medium text-secondary group-hover:underline">Add a phone number</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="mt-8">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<mat-label>Address</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:location-marker'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'address'"
|
||||
[placeholder]="'Address'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Birthday -->
|
||||
<div class="mt-8">
|
||||
<mat-form-field class="fuse-mat-no-subscript w-full">
|
||||
<mat-label>Birthday</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:cake'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="birthdayDatepicker"
|
||||
[formControlName]="'birthday'"
|
||||
[placeholder]="'Birthday'">
|
||||
<mat-datepicker-toggle
|
||||
matSuffix
|
||||
[for]="birthdayDatepicker">
|
||||
</mat-datepicker-toggle>
|
||||
<mat-datepicker #birthdayDatepicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="mt-8">
|
||||
<mat-form-field class="fuse-mat-textarea fuse-mat-no-subscript w-full">
|
||||
<mat-label>Notes</mat-label>
|
||||
<mat-icon
|
||||
matPrefix
|
||||
class="hidden sm:flex icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:menu-alt-2'"></mat-icon>
|
||||
<textarea
|
||||
matInput
|
||||
fuseAutogrow
|
||||
[rows]="5"
|
||||
[formControlName]="'notes'"
|
||||
[placeholder]="'Notes'"
|
||||
[spellcheck]="false"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center mt-10 -mx-6 sm:-mx-12 py-4 pr-4 pl-1 sm:pr-12 sm:pl-7 border-t bg-gray-50 dark:bg-transparent">
|
||||
<!-- Delete -->
|
||||
<button
|
||||
mat-button
|
||||
[color]="'warn'"
|
||||
[matTooltip]="'Delete'"
|
||||
(click)="deleteContact()">
|
||||
Delete
|
||||
</button>
|
||||
<!-- Cancel -->
|
||||
<button
|
||||
class="ml-auto"
|
||||
mat-button
|
||||
[matTooltip]="'Cancel'"
|
||||
(click)="toggleEditMode(false)">
|
||||
Cancel
|
||||
</button>
|
||||
<!-- Save -->
|
||||
<button
|
||||
class="ml-2"
|
||||
mat-flat-button
|
||||
[color]="'primary'"
|
||||
[disabled]="contactForm.invalid"
|
||||
[matTooltip]="'Save'"
|
||||
(click)="updateContact()">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -1,711 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { TemplatePortal } from '@angular/cdk/portal';
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { MatDrawerToggleResult } from '@angular/material/sidenav';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { Contact, Country, Tag } from 'app/modules/admin/apps/contacts/contacts.types';
|
||||
import { ContactsListComponent } from 'app/modules/admin/apps/contacts/list/list.component';
|
||||
import { ContactsService } from 'app/modules/admin/apps/contacts/contacts.service';
|
||||
|
||||
@Component({
|
||||
selector : 'contacts-details',
|
||||
templateUrl : './details.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ContactsDetailsComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@ViewChild('avatarFileInput') private _avatarFileInput: ElementRef;
|
||||
@ViewChild('tagsPanel') private _tagsPanel: TemplateRef<any>;
|
||||
@ViewChild('tagsPanelOrigin') private _tagsPanelOrigin: ElementRef;
|
||||
|
||||
editMode: boolean = false;
|
||||
tags: Tag[];
|
||||
tagsEditMode: boolean = false;
|
||||
filteredTags: Tag[];
|
||||
contact: Contact;
|
||||
contactForm: FormGroup;
|
||||
contacts: Contact[];
|
||||
countries: Country[];
|
||||
private _tagsPanelOverlayRef: OverlayRef;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _contactsListComponent: ContactsListComponent,
|
||||
private _contactsService: ContactsService,
|
||||
private _formBuilder: FormBuilder,
|
||||
private _renderer2: Renderer2,
|
||||
private _router: Router,
|
||||
private _overlay: Overlay,
|
||||
private _viewContainerRef: ViewContainerRef
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Open the drawer
|
||||
this._contactsListComponent.matDrawer.open();
|
||||
|
||||
// Create the contact form
|
||||
this.contactForm = this._formBuilder.group({
|
||||
id : [''],
|
||||
avatar : [null],
|
||||
name : ['', [Validators.required]],
|
||||
emails : this._formBuilder.array([]),
|
||||
phoneNumbers: this._formBuilder.array([]),
|
||||
title : [''],
|
||||
company : [''],
|
||||
birthday : [null],
|
||||
address : [null],
|
||||
notes : [null],
|
||||
tags : [[]]
|
||||
});
|
||||
|
||||
// Get the contacts
|
||||
this._contactsService.contacts$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((contacts: Contact[]) => {
|
||||
this.contacts = contacts;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the contact
|
||||
this._contactsService.contact$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((contact: Contact) => {
|
||||
|
||||
// Open the drawer in case it is closed
|
||||
this._contactsListComponent.matDrawer.open();
|
||||
|
||||
// Get the contact
|
||||
this.contact = contact;
|
||||
|
||||
// Clear the emails and phoneNumbers form arrays
|
||||
(this.contactForm.get('emails') as FormArray).clear();
|
||||
(this.contactForm.get('phoneNumbers') as FormArray).clear();
|
||||
|
||||
// Patch values to the form
|
||||
this.contactForm.patchValue(contact);
|
||||
|
||||
// Setup the emails form array
|
||||
const emailFormGroups = [];
|
||||
|
||||
if ( contact.emails.length > 0 )
|
||||
{
|
||||
// Iterate through them
|
||||
contact.emails.forEach((email) => {
|
||||
|
||||
// Create an email form group
|
||||
emailFormGroups.push(
|
||||
this._formBuilder.group({
|
||||
email: [email.email],
|
||||
label: [email.label]
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create an email form group
|
||||
emailFormGroups.push(
|
||||
this._formBuilder.group({
|
||||
email: [''],
|
||||
label: ['']
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add the email form groups to the emails form array
|
||||
emailFormGroups.forEach((emailFormGroup) => {
|
||||
(this.contactForm.get('emails') as FormArray).push(emailFormGroup);
|
||||
});
|
||||
|
||||
// Setup the phone numbers form array
|
||||
const phoneNumbersFormGroups = [];
|
||||
|
||||
if ( contact.phoneNumbers.length > 0 )
|
||||
{
|
||||
// Iterate through them
|
||||
contact.phoneNumbers.forEach((phoneNumber) => {
|
||||
|
||||
// Create an email form group
|
||||
phoneNumbersFormGroups.push(
|
||||
this._formBuilder.group({
|
||||
country: [phoneNumber.country],
|
||||
number : [phoneNumber.number],
|
||||
label : [phoneNumber.label]
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a phone number form group
|
||||
phoneNumbersFormGroups.push(
|
||||
this._formBuilder.group({
|
||||
country: ['us'],
|
||||
number : [''],
|
||||
label : ['']
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add the phone numbers form groups to the phone numbers form array
|
||||
phoneNumbersFormGroups.forEach((phoneNumbersFormGroup) => {
|
||||
(this.contactForm.get('phoneNumbers') as FormArray).push(phoneNumbersFormGroup);
|
||||
});
|
||||
|
||||
// Toggle the edit mode off
|
||||
this.toggleEditMode(false);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the country telephone codes
|
||||
this._contactsService.countries$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((codes: Country[]) => {
|
||||
this.countries = codes;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the tags
|
||||
this._contactsService.tags$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((tags: Tag[]) => {
|
||||
this.tags = tags;
|
||||
this.filteredTags = tags;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
|
||||
// Dispose the overlays if they are still on the DOM
|
||||
if ( this._tagsPanelOverlayRef )
|
||||
{
|
||||
this._tagsPanelOverlayRef.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close the drawer
|
||||
*/
|
||||
closeDrawer(): Promise<MatDrawerToggleResult>
|
||||
{
|
||||
return this._contactsListComponent.matDrawer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle edit mode
|
||||
*
|
||||
* @param editMode
|
||||
*/
|
||||
toggleEditMode(editMode: boolean | null = null): void
|
||||
{
|
||||
if ( editMode === null )
|
||||
{
|
||||
this.editMode = !this.editMode;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.editMode = editMode;
|
||||
}
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the contact
|
||||
*/
|
||||
updateContact(): void
|
||||
{
|
||||
// Get the contact object
|
||||
const contact = this.contactForm.getRawValue();
|
||||
|
||||
// Go through the contact object and clear empty values
|
||||
contact.emails = contact.emails.filter((email) => {
|
||||
return email.email;
|
||||
});
|
||||
|
||||
contact.phoneNumbers = contact.phoneNumbers.filter((phoneNumber) => {
|
||||
return phoneNumber.number;
|
||||
});
|
||||
|
||||
// Update the contact on the server
|
||||
this._contactsService.updateContact(contact.id, contact).subscribe(() => {
|
||||
|
||||
// Toggle the edit mode off
|
||||
this.toggleEditMode(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the contact
|
||||
*/
|
||||
deleteContact(): void
|
||||
{
|
||||
// Get the current contact's id
|
||||
const id = this.contact.id;
|
||||
|
||||
// Get the next/previous contact's id
|
||||
const currentContactIndex = this.contacts.findIndex(item => item.id === id);
|
||||
const nextContactIndex = currentContactIndex + ((currentContactIndex === (this.contacts.length - 1)) ? -1 : 1);
|
||||
const nextContactId = (this.contacts.length === 1 && this.contacts[0].id === id) ? null : this.contacts[nextContactIndex].id;
|
||||
|
||||
// Delete the contact
|
||||
this._contactsService.deleteContact(id)
|
||||
.subscribe((isDeleted) => {
|
||||
|
||||
// Return if the contact wasn't deleted...
|
||||
if ( !isDeleted )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current activated route
|
||||
let route = this._activatedRoute;
|
||||
while ( route.firstChild )
|
||||
{
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
// Navigate to the next contact if available
|
||||
if ( nextContactId )
|
||||
{
|
||||
this._router.navigate(['../', nextContactId], {relativeTo: route});
|
||||
}
|
||||
// Otherwise, navigate to the parent
|
||||
else
|
||||
{
|
||||
this._router.navigate(['../'], {relativeTo: route});
|
||||
}
|
||||
|
||||
// Toggle the edit mode off
|
||||
this.toggleEditMode(false);
|
||||
});
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload avatar
|
||||
*
|
||||
* @param fileList
|
||||
*/
|
||||
uploadAvatar(fileList: FileList): void
|
||||
{
|
||||
// Return if canceled
|
||||
if ( !fileList.length )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/png'];
|
||||
const file = fileList[0];
|
||||
|
||||
// Return if the file is not allowed
|
||||
if ( !allowedTypes.includes(file.type) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the avatar
|
||||
this._contactsService.uploadAvatar(this.contact.id, file).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the avatar
|
||||
*/
|
||||
removeAvatar(): void
|
||||
{
|
||||
// Get the form control for 'avatar'
|
||||
const avatarFormControl = this.contactForm.get('avatar');
|
||||
|
||||
// Set the avatar as null
|
||||
avatarFormControl.setValue(null);
|
||||
|
||||
// Set the file input value as null
|
||||
this._avatarFileInput.nativeElement.value = null;
|
||||
|
||||
// Update the contact
|
||||
this.contact.avatar = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open tags panel
|
||||
*/
|
||||
openTagsPanel(): void
|
||||
{
|
||||
// Create the overlay
|
||||
this._tagsPanelOverlayRef = this._overlay.create({
|
||||
backdropClass : '',
|
||||
hasBackdrop : true,
|
||||
scrollStrategy : this._overlay.scrollStrategies.block(),
|
||||
positionStrategy: this._overlay.position()
|
||||
.flexibleConnectedTo(this._tagsPanelOrigin.nativeElement)
|
||||
.withFlexibleDimensions()
|
||||
.withViewportMargin(64)
|
||||
.withLockedPosition()
|
||||
.withPositions([
|
||||
{
|
||||
originX : 'start',
|
||||
originY : 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top'
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
// Subscribe to the attachments observable
|
||||
this._tagsPanelOverlayRef.attachments().subscribe(() => {
|
||||
|
||||
// Add a class to the origin
|
||||
this._renderer2.addClass(this._tagsPanelOrigin.nativeElement, 'panel-opened');
|
||||
|
||||
// Focus to the search input once the overlay has been attached
|
||||
this._tagsPanelOverlayRef.overlayElement.querySelector('input').focus();
|
||||
});
|
||||
|
||||
// Create a portal from the template
|
||||
const templatePortal = new TemplatePortal(this._tagsPanel, this._viewContainerRef);
|
||||
|
||||
// Attach the portal to the overlay
|
||||
this._tagsPanelOverlayRef.attach(templatePortal);
|
||||
|
||||
// Subscribe to the backdrop click
|
||||
this._tagsPanelOverlayRef.backdropClick().subscribe(() => {
|
||||
|
||||
// Remove the class from the origin
|
||||
this._renderer2.removeClass(this._tagsPanelOrigin.nativeElement, 'panel-opened');
|
||||
|
||||
// If overlay exists and attached...
|
||||
if ( this._tagsPanelOverlayRef && this._tagsPanelOverlayRef.hasAttached() )
|
||||
{
|
||||
// Detach it
|
||||
this._tagsPanelOverlayRef.detach();
|
||||
|
||||
// Reset the tag filter
|
||||
this.filteredTags = this.tags;
|
||||
|
||||
// Toggle the edit mode off
|
||||
this.tagsEditMode = false;
|
||||
}
|
||||
|
||||
// If template portal exists and attached...
|
||||
if ( templatePortal && templatePortal.isAttached )
|
||||
{
|
||||
// Detach it
|
||||
templatePortal.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the tags edit mode
|
||||
*/
|
||||
toggleTagsEditMode(): void
|
||||
{
|
||||
this.tagsEditMode = !this.tagsEditMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter tags
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
filterTags(event): void
|
||||
{
|
||||
// Get the value
|
||||
const value = event.target.value.toLowerCase();
|
||||
|
||||
// Filter the tags
|
||||
this.filteredTags = this.tags.filter(tag => tag.title.toLowerCase().includes(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter tags input key down event
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
filterTagsInputKeyDown(event): void
|
||||
{
|
||||
// Return if the pressed key is not 'Enter'
|
||||
if ( event.key !== 'Enter' )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is no tag available...
|
||||
if ( this.filteredTags.length === 0 )
|
||||
{
|
||||
// Create the tag
|
||||
this.createTag(event.target.value);
|
||||
|
||||
// Clear the input
|
||||
event.target.value = '';
|
||||
|
||||
// Return
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a tag...
|
||||
const tag = this.filteredTags[0];
|
||||
const isTagApplied = this.contact.tags.find((id) => id === tag.id);
|
||||
|
||||
// If the found tag is already applied to the contact...
|
||||
if ( isTagApplied )
|
||||
{
|
||||
// Remove the tag from the contact
|
||||
this.removeTagFromContact(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise add the tag to the contact
|
||||
this.addTagToContact(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tag
|
||||
*
|
||||
* @param title
|
||||
*/
|
||||
createTag(title: string): void
|
||||
{
|
||||
const tag = {
|
||||
title
|
||||
};
|
||||
|
||||
// Create tag on the server
|
||||
this._contactsService.createTag(tag)
|
||||
.subscribe((response) => {
|
||||
|
||||
// Add the tag to the contact
|
||||
this.addTagToContact(response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tag title
|
||||
*
|
||||
* @param tag
|
||||
* @param event
|
||||
*/
|
||||
updateTagTitle(tag: Tag, event): void
|
||||
{
|
||||
// Update the title on the tag
|
||||
tag.title = event.target.value;
|
||||
|
||||
// Update the tag on the server
|
||||
this._contactsService.updateTag(tag.id, tag)
|
||||
.pipe(debounceTime(300))
|
||||
.subscribe();
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the tag
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
deleteTag(tag: Tag): void
|
||||
{
|
||||
// Delete the tag from the server
|
||||
this._contactsService.deleteTag(tag.id).subscribe();
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tag to the contact
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
addTagToContact(tag: Tag): void
|
||||
{
|
||||
// Add the tag
|
||||
this.contact.tags.unshift(tag.id);
|
||||
|
||||
// Update the contact form
|
||||
this.contactForm.get('tags').patchValue(this.contact.tags);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tag from the contact
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
removeTagFromContact(tag: Tag): void
|
||||
{
|
||||
// Remove the tag
|
||||
this.contact.tags.splice(this.contact.tags.findIndex(item => item === tag.id), 1);
|
||||
|
||||
// Update the contact form
|
||||
this.contactForm.get('tags').patchValue(this.contact.tags);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle contact tag
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
toggleContactTag(tag: Tag): void
|
||||
{
|
||||
if ( this.contact.tags.includes(tag.id) )
|
||||
{
|
||||
this.removeTagFromContact(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.addTagToContact(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the create tag button be visible
|
||||
*
|
||||
* @param inputValue
|
||||
*/
|
||||
shouldShowCreateTagButton(inputValue: string): boolean
|
||||
{
|
||||
return !!!(inputValue === '' || this.tags.findIndex(tag => tag.title.toLowerCase() === inputValue.toLowerCase()) > -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the email field
|
||||
*/
|
||||
addEmailField(): void
|
||||
{
|
||||
// Create an empty email form group
|
||||
const emailFormGroup = this._formBuilder.group({
|
||||
email: [''],
|
||||
label: ['']
|
||||
});
|
||||
|
||||
// Add the email form group to the emails form array
|
||||
(this.contactForm.get('emails') as FormArray).push(emailFormGroup);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the email field
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
removeEmailField(index: number): void
|
||||
{
|
||||
// Get form array for emails
|
||||
const emailsFormArray = this.contactForm.get('emails') as FormArray;
|
||||
|
||||
// Remove the email field
|
||||
emailsFormArray.removeAt(index);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an empty phone number field
|
||||
*/
|
||||
addPhoneNumberField(): void
|
||||
{
|
||||
// Create an empty phone number form group
|
||||
const phoneNumberFormGroup = this._formBuilder.group({
|
||||
country: ['us'],
|
||||
number : [''],
|
||||
label : ['']
|
||||
});
|
||||
|
||||
// Add the phone number form group to the phoneNumbers form array
|
||||
(this.contactForm.get('phoneNumbers') as FormArray).push(phoneNumberFormGroup);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the phone number field
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
removePhoneNumberField(index: number): void
|
||||
{
|
||||
// Get form array for phone numbers
|
||||
const phoneNumbersFormArray = this.contactForm.get('phoneNumbers') as FormArray;
|
||||
|
||||
// Remove the phone number field
|
||||
phoneNumbersFormArray.removeAt(index);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country info by iso code
|
||||
*
|
||||
* @param iso
|
||||
*/
|
||||
getCountryByIso(iso: string): Country
|
||||
{
|
||||
return this.countries.find((country) => country.iso === iso);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden">
|
||||
|
||||
<mat-drawer-container
|
||||
class="flex-auto h-full bg-card dark:bg-transparent"
|
||||
(backdropClick)="onBackdropClicked()">
|
||||
|
||||
<!-- Drawer -->
|
||||
<mat-drawer
|
||||
class="w-full md:w-160 dark:bg-gray-900"
|
||||
[mode]="drawerMode"
|
||||
[opened]="false"
|
||||
[position]="'end'"
|
||||
[disableClose]="true"
|
||||
#matDrawer>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-drawer>
|
||||
|
||||
<mat-drawer-content class="flex flex-col">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row md:flex-col flex-auto justify-between py-8 px-6 md:px-8 border-b">
|
||||
|
||||
<!-- Title -->
|
||||
<div>
|
||||
<div class="text-4xl font-extrabold tracking-tight leading-none">Contacts</div>
|
||||
<div class="ml-0.5 font-medium text-secondary">
|
||||
<ng-container *ngIf="contactsCount > 0">
|
||||
{{contactsCount}}
|
||||
</ng-container>
|
||||
{{contactsCount | i18nPlural: {
|
||||
'=0' : 'No contacts',
|
||||
'=1' : 'contact',
|
||||
'other': 'contacts'
|
||||
} }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main actions -->
|
||||
<div class="flex items-center mt-4 sm:mt-0 md:mt-4">
|
||||
<!-- Search -->
|
||||
<div class="flex-auto">
|
||||
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript w-full min-w-50">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
matPrefix
|
||||
[svgIcon]="'heroicons_solid:search'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="searchInputControl"
|
||||
[autocomplete]="'off'"
|
||||
[placeholder]="'Search contacts'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- Add contact button -->
|
||||
<button
|
||||
class="ml-4"
|
||||
mat-flat-button
|
||||
[color]="'primary'"
|
||||
(click)="createContact()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:plus'"></mat-icon>
|
||||
<span class="ml-2 mr-1">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contacts list -->
|
||||
<div class="relative">
|
||||
<ng-container *ngIf="contacts$ | async as contacts">
|
||||
<ng-container *ngIf="contacts.length; else noContacts">
|
||||
<ng-container *ngFor="let contact of contacts; let i = index; trackBy: trackByFn">
|
||||
<!-- Group -->
|
||||
<ng-container *ngIf="i === 0 || contact.name.charAt(0) !== contacts[i - 1].name.charAt(0)">
|
||||
<div class="z-10 sticky top-0 -mt-px px-6 py-1 md:px-8 border-t border-b font-medium uppercase text-secondary bg-gray-50 dark:bg-gray-900">
|
||||
{{contact.name.charAt(0)}}
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Contact -->
|
||||
<div
|
||||
class="z-20 flex items-center px-6 py-4 md:px-8 cursor-pointer hover:bg-hover border-b"
|
||||
[ngClass]="{'bg-primary-50 dark:bg-hover': selectedContact && selectedContact.id === contact.id}"
|
||||
(click)="goToContact(contact.id)">
|
||||
<div class="flex flex-0 items-center justify-center w-10 h-10 rounded-full overflow-hidden">
|
||||
<ng-container *ngIf="contact.avatar">
|
||||
<img
|
||||
class="object-cover w-full h-full"
|
||||
[src]="contact.avatar"
|
||||
alt="Contact avatar"/>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!contact.avatar">
|
||||
<div class="flex items-center justify-center w-full h-full rounded-full text-lg uppercase bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
||||
{{contact.name.charAt(0)}}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="min-w-0 ml-4">
|
||||
<div class="font-medium leading-5 truncate">{{contact.name}}</div>
|
||||
<div class="leading-5 truncate text-secondary">{{contact.title}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- No contacts -->
|
||||
<ng-template #noContacts>
|
||||
<div class="p-8 sm:p-16 border-t text-4xl font-semibold tracking-tight text-center">There are no contacts!</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</mat-drawer-content>
|
||||
|
||||
</mat-drawer-container>
|
||||
|
||||
</div>
|
||||
@@ -1,241 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { fromEvent, Observable, Subject } from 'rxjs';
|
||||
import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
|
||||
import { Contact, Country } from 'app/modules/admin/apps/contacts/contacts.types';
|
||||
import { ContactsService } from 'app/modules/admin/apps/contacts/contacts.service';
|
||||
|
||||
@Component({
|
||||
selector : 'contacts-list',
|
||||
templateUrl : './list.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ContactsListComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@ViewChild('matDrawer', {static: true}) matDrawer: MatDrawer;
|
||||
|
||||
contacts$: Observable<Contact[]>;
|
||||
|
||||
contactsCount: number = 0;
|
||||
contactsTableColumns: string[] = ['name', 'email', 'phoneNumber', 'job'];
|
||||
countries: Country[];
|
||||
drawerMode: 'side' | 'over';
|
||||
searchInputControl: FormControl = new FormControl();
|
||||
selectedContact: Contact;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _contactsService: ContactsService,
|
||||
@Inject(DOCUMENT) private _document: any,
|
||||
private _router: Router,
|
||||
private _fuseMediaWatcherService: FuseMediaWatcherService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the contacts
|
||||
this.contacts$ = this._contactsService.contacts$;
|
||||
this._contactsService.contacts$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((contacts: Contact[]) => {
|
||||
|
||||
// Update the counts
|
||||
this.contactsCount = contacts.length;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the contact
|
||||
this._contactsService.contact$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((contact: Contact) => {
|
||||
|
||||
// Update the selected contact
|
||||
this.selectedContact = contact;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the countries
|
||||
this._contactsService.countries$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((countries: Country[]) => {
|
||||
|
||||
// Update the countries
|
||||
this.countries = countries;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Subscribe to search input field value changes
|
||||
this.searchInputControl.valueChanges
|
||||
.pipe(
|
||||
takeUntil(this._unsubscribeAll),
|
||||
switchMap((query) => {
|
||||
|
||||
// Search
|
||||
return this._contactsService.searchContacts(query);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Subscribe to MatDrawer opened change
|
||||
this.matDrawer.openedChange.subscribe((opened) => {
|
||||
if ( !opened )
|
||||
{
|
||||
// Remove the selected contact when drawer closed
|
||||
this.selectedContact = null;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to media changes
|
||||
this._fuseMediaWatcherService.onMediaChange$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(({matchingAliases}) => {
|
||||
|
||||
// Set the drawerMode if the given breakpoint is active
|
||||
if ( matchingAliases.includes('lg') )
|
||||
{
|
||||
this.drawerMode = 'side';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.drawerMode = 'over';
|
||||
}
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Listen for shortcuts
|
||||
fromEvent(this._document, 'keydown')
|
||||
.pipe(
|
||||
takeUntil(this._unsubscribeAll),
|
||||
filter<KeyboardEvent>((event) => {
|
||||
return (event.ctrlKey === true || event.metaKey) // Ctrl or Cmd
|
||||
&& (event.key === '/'); // '/'
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.createContact();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Go to contact
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
goToContact(id: string): void
|
||||
{
|
||||
// Get the current activated route
|
||||
let route = this._activatedRoute;
|
||||
while ( route.firstChild )
|
||||
{
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
// Go to contact
|
||||
this._router.navigate(['../', id], {relativeTo: route});
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* On backdrop clicked
|
||||
*/
|
||||
onBackdropClicked(): void
|
||||
{
|
||||
// Get the current activated route
|
||||
let route = this._activatedRoute;
|
||||
while ( route.firstChild )
|
||||
{
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
// Go to the parent route
|
||||
this._router.navigate(['../'], {relativeTo: route});
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create contact
|
||||
*/
|
||||
createContact(): void
|
||||
{
|
||||
// Create the contact
|
||||
this._contactsService.createContact().subscribe((newContact) => {
|
||||
|
||||
// Go to new contact
|
||||
this.goToContact(newContact.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country code
|
||||
*
|
||||
* @param iso
|
||||
*/
|
||||
getCountryCode(iso: string): string
|
||||
{
|
||||
if ( !iso )
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.countries.find((country) => country.iso === iso).code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatRippleModule } from '@angular/material/core';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { InventoryComponent } from 'app/modules/admin/apps/ecommerce/inventory/inventory.component';
|
||||
import { InventoryListComponent } from 'app/modules/admin/apps/ecommerce/inventory/list/inventory.component';
|
||||
import { ecommerceRoutes } from 'app/modules/admin/apps/ecommerce/ecommerce.routing';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
InventoryComponent,
|
||||
InventoryListComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(ecommerceRoutes),
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatRippleModule,
|
||||
MatSortModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
MatTableModule,
|
||||
MatTooltipModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class ECommerceModule
|
||||
{
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { InventoryComponent } from 'app/modules/admin/apps/ecommerce/inventory/inventory.component';
|
||||
import { InventoryListComponent } from 'app/modules/admin/apps/ecommerce/inventory/list/inventory.component';
|
||||
import { InventoryBrandsResolver, InventoryCategoriesResolver, InventoryProductsResolver, InventoryTagsResolver, InventoryVendorsResolver } from 'app/modules/admin/apps/ecommerce/inventory/inventory.resolvers';
|
||||
|
||||
export const ecommerceRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
pathMatch : 'full',
|
||||
redirectTo: 'inventory'
|
||||
},
|
||||
{
|
||||
path : 'inventory',
|
||||
component: InventoryComponent,
|
||||
children : [
|
||||
{
|
||||
path : '',
|
||||
component: InventoryListComponent,
|
||||
resolve : {
|
||||
brands : InventoryBrandsResolver,
|
||||
categories: InventoryCategoriesResolver,
|
||||
products : InventoryProductsResolver,
|
||||
tags : InventoryTagsResolver,
|
||||
vendors : InventoryVendorsResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
/*children : [
|
||||
{
|
||||
path : '',
|
||||
component: ContactsListComponent,
|
||||
resolve : {
|
||||
tasks : ContactsResolver,
|
||||
countries: ContactsCountriesResolver
|
||||
},
|
||||
children : [
|
||||
{
|
||||
path : ':id',
|
||||
component : ContactsDetailsComponent,
|
||||
resolve : {
|
||||
task : ContactsContactResolver,
|
||||
countries: ContactsCountriesResolver
|
||||
},
|
||||
canDeactivate: [CanDeactivateContactsDetails]
|
||||
}
|
||||
]
|
||||
}
|
||||
]*/
|
||||
}
|
||||
];
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector : 'inventory',
|
||||
templateUrl : './inventory.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class InventoryComponent
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { InventoryService } from 'app/modules/admin/apps/ecommerce/inventory/inventory.service';
|
||||
import { InventoryBrand, InventoryCategory, InventoryPagination, InventoryProduct, InventoryTag, InventoryVendor } from 'app/modules/admin/apps/ecommerce/inventory/inventory.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryBrandsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _inventoryService: InventoryService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<InventoryBrand[]>
|
||||
{
|
||||
return this._inventoryService.getBrands();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryCategoriesResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _inventoryService: InventoryService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<InventoryCategory[]>
|
||||
{
|
||||
return this._inventoryService.getCategories();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryProductResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _inventoryService: InventoryService,
|
||||
private _router: Router
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<InventoryProduct>
|
||||
{
|
||||
return this._inventoryService.getProductById(route.paramMap.get('id'))
|
||||
.pipe(
|
||||
// Error here means the requested product is not available
|
||||
catchError((error) => {
|
||||
|
||||
// Log the error
|
||||
console.error(error);
|
||||
|
||||
// Get the parent url
|
||||
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
||||
|
||||
// Navigate to there
|
||||
this._router.navigateByUrl(parentUrl);
|
||||
|
||||
// Throw an error
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryProductsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _inventoryService: InventoryService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ pagination: InventoryPagination, products: InventoryProduct[] }>
|
||||
{
|
||||
return this._inventoryService.getProducts();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryTagsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _inventoryService: InventoryService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<InventoryTag[]>
|
||||
{
|
||||
return this._inventoryService.getTags();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryVendorsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _inventoryService: InventoryService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<InventoryVendor[]>
|
||||
{
|
||||
return this._inventoryService.getVendors();
|
||||
}
|
||||
}
|
||||
@@ -1,441 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { InventoryBrand, InventoryCategory, InventoryPagination, InventoryProduct, InventoryTag, InventoryVendor } from 'app/modules/admin/apps/ecommerce/inventory/inventory.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InventoryService
|
||||
{
|
||||
// Private
|
||||
private _brands: BehaviorSubject<InventoryBrand[] | null> = new BehaviorSubject(null);
|
||||
private _categories: BehaviorSubject<InventoryCategory[] | null> = new BehaviorSubject(null);
|
||||
private _pagination: BehaviorSubject<InventoryPagination | null> = new BehaviorSubject(null);
|
||||
private _product: BehaviorSubject<InventoryProduct | null> = new BehaviorSubject(null);
|
||||
private _products: BehaviorSubject<InventoryProduct[] | null> = new BehaviorSubject(null);
|
||||
private _tags: BehaviorSubject<InventoryTag[] | null> = new BehaviorSubject(null);
|
||||
private _vendors: BehaviorSubject<InventoryVendor[] | null> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _httpClient: HttpClient)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for brands
|
||||
*/
|
||||
get brands$(): Observable<InventoryBrand[]>
|
||||
{
|
||||
return this._brands.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for categories
|
||||
*/
|
||||
get categories$(): Observable<InventoryCategory[]>
|
||||
{
|
||||
return this._categories.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for pagination
|
||||
*/
|
||||
get pagination$(): Observable<InventoryPagination>
|
||||
{
|
||||
return this._pagination.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for product
|
||||
*/
|
||||
get product$(): Observable<InventoryProduct>
|
||||
{
|
||||
return this._product.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for products
|
||||
*/
|
||||
get products$(): Observable<InventoryProduct[]>
|
||||
{
|
||||
return this._products.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for tags
|
||||
*/
|
||||
get tags$(): Observable<InventoryTag[]>
|
||||
{
|
||||
return this._tags.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for vendors
|
||||
*/
|
||||
get vendors$(): Observable<InventoryVendor[]>
|
||||
{
|
||||
return this._vendors.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get brands
|
||||
*/
|
||||
getBrands(): Observable<InventoryBrand[]>
|
||||
{
|
||||
return this._httpClient.get<InventoryBrand[]>('api/apps/ecommerce/inventory/brands').pipe(
|
||||
tap((brands) => {
|
||||
this._brands.next(brands);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories
|
||||
*/
|
||||
getCategories(): Observable<InventoryCategory[]>
|
||||
{
|
||||
return this._httpClient.get<InventoryCategory[]>('api/apps/ecommerce/inventory/categories').pipe(
|
||||
tap((categories) => {
|
||||
this._categories.next(categories);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get products
|
||||
*
|
||||
*
|
||||
* @param page
|
||||
* @param size
|
||||
* @param sort
|
||||
* @param order
|
||||
* @param search
|
||||
*/
|
||||
getProducts(page: number = 0, size: number = 10, sort: string = 'name', order: 'asc' | 'desc' | '' = 'asc', search: string = ''):
|
||||
Observable<{ pagination: InventoryPagination, products: InventoryProduct[] }>
|
||||
{
|
||||
return this._httpClient.get<{ pagination: InventoryPagination, products: InventoryProduct[] }>('api/apps/ecommerce/inventory/products', {
|
||||
params: {
|
||||
page: '' + page,
|
||||
size: '' + size,
|
||||
sort,
|
||||
order,
|
||||
search
|
||||
}
|
||||
}).pipe(
|
||||
tap((response) => {
|
||||
this._pagination.next(response.pagination);
|
||||
this._products.next(response.products);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product by id
|
||||
*/
|
||||
getProductById(id: string): Observable<InventoryProduct>
|
||||
{
|
||||
return this._products.pipe(
|
||||
take(1),
|
||||
map((products) => {
|
||||
|
||||
// Find the product
|
||||
const product = products.find(item => item.id === id) || null;
|
||||
|
||||
// Update the product
|
||||
this._product.next(product);
|
||||
|
||||
// Return the product
|
||||
return product;
|
||||
}),
|
||||
switchMap((product) => {
|
||||
|
||||
if ( !product )
|
||||
{
|
||||
return throwError('Could not found product with id of ' + id + '!');
|
||||
}
|
||||
|
||||
return of(product);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create product
|
||||
*/
|
||||
createProduct(): Observable<InventoryProduct>
|
||||
{
|
||||
return this.products$.pipe(
|
||||
take(1),
|
||||
switchMap((products) => this._httpClient.post<InventoryProduct>('api/apps/ecommerce/inventory/product', {}).pipe(
|
||||
map((newProduct) => {
|
||||
|
||||
// Update the products with the new product
|
||||
this._products.next([newProduct, ...products]);
|
||||
|
||||
// Return the new product
|
||||
return newProduct;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update product
|
||||
*
|
||||
* @param id
|
||||
* @param product
|
||||
*/
|
||||
updateProduct(id: string, product: InventoryProduct): Observable<InventoryProduct>
|
||||
{
|
||||
return this.products$.pipe(
|
||||
take(1),
|
||||
switchMap(products => this._httpClient.patch<InventoryProduct>('api/apps/ecommerce/inventory/product', {
|
||||
id,
|
||||
product
|
||||
}).pipe(
|
||||
map((updatedProduct) => {
|
||||
|
||||
// Find the index of the updated product
|
||||
const index = products.findIndex(item => item.id === id);
|
||||
|
||||
// Update the product
|
||||
products[index] = updatedProduct;
|
||||
|
||||
// Update the products
|
||||
this._products.next(products);
|
||||
|
||||
// Return the updated product
|
||||
return updatedProduct;
|
||||
}),
|
||||
switchMap(updatedProduct => this.product$.pipe(
|
||||
take(1),
|
||||
filter(item => item && item.id === id),
|
||||
tap(() => {
|
||||
|
||||
// Update the product if it's selected
|
||||
this._product.next(updatedProduct);
|
||||
|
||||
// Return the updated product
|
||||
return updatedProduct;
|
||||
})
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the product
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
deleteProduct(id: string): Observable<boolean>
|
||||
{
|
||||
return this.products$.pipe(
|
||||
take(1),
|
||||
switchMap(products => this._httpClient.delete('api/apps/ecommerce/inventory/product', {params: {id}}).pipe(
|
||||
map((isDeleted: boolean) => {
|
||||
|
||||
// Find the index of the deleted product
|
||||
const index = products.findIndex(item => item.id === id);
|
||||
|
||||
// Delete the product
|
||||
products.splice(index, 1);
|
||||
|
||||
// Update the products
|
||||
this._products.next(products);
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags
|
||||
*/
|
||||
getTags(): Observable<InventoryTag[]>
|
||||
{
|
||||
return this._httpClient.get<InventoryTag[]>('api/apps/ecommerce/inventory/tags').pipe(
|
||||
tap((tags) => {
|
||||
this._tags.next(tags);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tag
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
createTag(tag: InventoryTag): Observable<InventoryTag>
|
||||
{
|
||||
return this.tags$.pipe(
|
||||
take(1),
|
||||
switchMap(tags => this._httpClient.post<InventoryTag>('api/apps/ecommerce/inventory/tag', {tag}).pipe(
|
||||
map((newTag) => {
|
||||
|
||||
// Update the tags with the new tag
|
||||
this._tags.next([...tags, newTag]);
|
||||
|
||||
// Return new tag from observable
|
||||
return newTag;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tag
|
||||
*
|
||||
* @param id
|
||||
* @param tag
|
||||
*/
|
||||
updateTag(id: string, tag: InventoryTag): Observable<InventoryTag>
|
||||
{
|
||||
return this.tags$.pipe(
|
||||
take(1),
|
||||
switchMap(tags => this._httpClient.patch<InventoryTag>('api/apps/ecommerce/inventory/tag', {
|
||||
id,
|
||||
tag
|
||||
}).pipe(
|
||||
map((updatedTag) => {
|
||||
|
||||
// Find the index of the updated tag
|
||||
const index = tags.findIndex(item => item.id === id);
|
||||
|
||||
// Update the tag
|
||||
tags[index] = updatedTag;
|
||||
|
||||
// Update the tags
|
||||
this._tags.next(tags);
|
||||
|
||||
// Return the updated tag
|
||||
return updatedTag;
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the tag
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
deleteTag(id: string): Observable<boolean>
|
||||
{
|
||||
return this.tags$.pipe(
|
||||
take(1),
|
||||
switchMap(tags => this._httpClient.delete('api/apps/ecommerce/inventory/tag', {params: {id}}).pipe(
|
||||
map((isDeleted: boolean) => {
|
||||
|
||||
// Find the index of the deleted tag
|
||||
const index = tags.findIndex(item => item.id === id);
|
||||
|
||||
// Delete the tag
|
||||
tags.splice(index, 1);
|
||||
|
||||
// Update the tags
|
||||
this._tags.next(tags);
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
}),
|
||||
filter(isDeleted => isDeleted),
|
||||
switchMap(isDeleted => this.products$.pipe(
|
||||
take(1),
|
||||
map((products) => {
|
||||
|
||||
// Iterate through the contacts
|
||||
products.forEach((product) => {
|
||||
|
||||
const tagIndex = product.tags.findIndex(tag => tag === id);
|
||||
|
||||
// If the contact has the tag, remove it
|
||||
if ( tagIndex > -1 )
|
||||
{
|
||||
product.tags.splice(tagIndex, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the deleted status
|
||||
return isDeleted;
|
||||
})
|
||||
))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vendors
|
||||
*/
|
||||
getVendors(): Observable<InventoryVendor[]>
|
||||
{
|
||||
return this._httpClient.get<InventoryVendor[]>('api/apps/ecommerce/inventory/vendors').pipe(
|
||||
tap((vendors) => {
|
||||
this._vendors.next(vendors);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the avatar of the given contact
|
||||
*
|
||||
* @param id
|
||||
* @param avatar
|
||||
*/
|
||||
/*uploadAvatar(id: string, avatar: File): Observable<Contact>
|
||||
{
|
||||
return this.contacts$.pipe(
|
||||
take(1),
|
||||
switchMap(contacts => this._httpClient.post<Contact>('api/apps/contacts/avatar', {
|
||||
id,
|
||||
avatar
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': avatar.type
|
||||
}
|
||||
}).pipe(
|
||||
map((updatedContact) => {
|
||||
|
||||
// Find the index of the updated contact
|
||||
const index = contacts.findIndex(item => item.id === id);
|
||||
|
||||
// Update the contact
|
||||
contacts[index] = updatedContact;
|
||||
|
||||
// Update the contacts
|
||||
this._contacts.next(contacts);
|
||||
|
||||
// Return the updated contact
|
||||
return updatedContact;
|
||||
}),
|
||||
switchMap(updatedContact => this.contact$.pipe(
|
||||
take(1),
|
||||
filter(item => item && item.id === id),
|
||||
tap(() => {
|
||||
|
||||
// Update the contact if it's selected
|
||||
this._contact.next(updatedContact);
|
||||
|
||||
// Return the updated contact
|
||||
return updatedContact;
|
||||
})
|
||||
))
|
||||
))
|
||||
);
|
||||
}*/
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
export interface InventoryProduct
|
||||
{
|
||||
id: string;
|
||||
category?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
sku?: string | null;
|
||||
barcode?: string | null;
|
||||
brand?: string | null;
|
||||
vendor: string | null;
|
||||
stock: number;
|
||||
reserved: number;
|
||||
cost: number;
|
||||
basePrice: number;
|
||||
taxPercent: number;
|
||||
price: number;
|
||||
weight: number;
|
||||
thumbnail: string;
|
||||
images: string[];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface InventoryPagination
|
||||
{
|
||||
length: number;
|
||||
size: number;
|
||||
page: number;
|
||||
lastPage: number;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
export interface InventoryCategory
|
||||
{
|
||||
id: string;
|
||||
parentId: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface InventoryBrand
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface InventoryTag
|
||||
{
|
||||
id?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface InventoryVendor
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
@@ -1,567 +0,0 @@
|
||||
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden bg-card dark:bg-transparent">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="relative flex flex-col sm:flex-row flex-0 sm:items-center sm:justify-between py-8 px-6 md:px-8 border-b">
|
||||
<!-- Loader -->
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0"
|
||||
*ngIf="isLoading">
|
||||
<mat-progress-bar [mode]="'indeterminate'"></mat-progress-bar>
|
||||
</div>
|
||||
<!-- Title -->
|
||||
<div class="text-4xl font-extrabold tracking-tight">Inventory</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-shrink-0 items-center mt-6 sm:mt-0 sm:ml-4">
|
||||
<!-- Search -->
|
||||
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript min-w-50">
|
||||
<mat-icon
|
||||
matPrefix
|
||||
[svgIcon]="'heroicons_outline:search'"></mat-icon>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="searchInputControl"
|
||||
[autocomplete]="'off'"
|
||||
[placeholder]="'Search products'">
|
||||
</mat-form-field>
|
||||
<!-- Add product button -->
|
||||
<button
|
||||
class="ml-4"
|
||||
mat-flat-button
|
||||
[color]="'primary'"
|
||||
(click)="createProduct()">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:plus'"></mat-icon>
|
||||
<span class="ml-2 mr-1">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-auto overflow-hidden">
|
||||
|
||||
<!-- Products list -->
|
||||
<div class="flex flex-col flex-auto sm:mb-18 overflow-hidden">
|
||||
|
||||
<ng-container *ngIf="productsCount > 0; else noProducts">
|
||||
|
||||
<!-- Table wrapper -->
|
||||
<div
|
||||
class="overflow-x-auto sm:overflow-y-auto"
|
||||
cdkScrollable>
|
||||
|
||||
<!-- Table -->
|
||||
<table
|
||||
class="w-full min-w-320 table-fixed bg-transparent"
|
||||
[ngClass]="{'pointer-events-none': isLoading}"
|
||||
mat-table
|
||||
matSort
|
||||
[matSortActive]="'name'"
|
||||
[matSortDisableClear]="true"
|
||||
[matSortDirection]="'asc'"
|
||||
[multiTemplateDataRows]="true"
|
||||
[dataSource]="products$"
|
||||
[trackBy]="trackByFn">
|
||||
|
||||
<!-- SKU -->
|
||||
<ng-container matColumnDef="sku">
|
||||
<th
|
||||
class="w-56 pl-26 bg-gray-50 dark:bg-black dark:bg-opacity-5"
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
disableClear>
|
||||
SKU
|
||||
</th>
|
||||
<td
|
||||
class="px-8"
|
||||
mat-cell
|
||||
*matCellDef="let product">
|
||||
<div class="flex items-center">
|
||||
<span class="relative flex flex-0 items-center justify-center w-12 h-12 mr-6 rounded overflow-hidden border">
|
||||
<img
|
||||
class="w-8"
|
||||
*ngIf="product.thumbnail"
|
||||
[src]="product.thumbnail">
|
||||
<span
|
||||
class="flex items-center justify-center w-full h-full text-xs font-semibold leading-none text-center uppercase"
|
||||
*ngIf="!product.thumbnail">
|
||||
No Image
|
||||
</span>
|
||||
</span>
|
||||
<span class="truncate">{{product.sku}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th
|
||||
class="bg-gray-50 dark:bg-black dark:bg-opacity-5"
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
disableClear>
|
||||
Name
|
||||
</th>
|
||||
<td
|
||||
class="pr-8 truncate"
|
||||
mat-cell
|
||||
*matCellDef="let product">
|
||||
{{product.name}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Price -->
|
||||
<ng-container matColumnDef="price">
|
||||
<th
|
||||
class="w-40 bg-gray-50 dark:bg-black dark:bg-opacity-5"
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
disableClear>
|
||||
Price
|
||||
</th>
|
||||
<td
|
||||
class="pr-4"
|
||||
mat-cell
|
||||
*matCellDef="let product">
|
||||
{{product.price | currency:'USD':'symbol':'1.2-2'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Stock -->
|
||||
<ng-container matColumnDef="stock">
|
||||
<th
|
||||
class="w-24 bg-gray-50 dark:bg-black dark:bg-opacity-5"
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
disableClear>
|
||||
Stock
|
||||
</th>
|
||||
<td
|
||||
class="pr-4"
|
||||
mat-cell
|
||||
*matCellDef="let product">
|
||||
<span class="flex items-center">
|
||||
<span class="min-w-4">{{product.stock}}</span>
|
||||
<!-- Low stock -->
|
||||
<span
|
||||
class="flex items-end ml-2 w-1 h-4 bg-red-200 rounded overflow-hidden"
|
||||
*ngIf="product.stock < 20">
|
||||
<span class="flex w-full h-1/3 bg-red-600"></span>
|
||||
</span>
|
||||
<!-- Medium stock -->
|
||||
<span
|
||||
class="flex items-end ml-2 w-1 h-4 bg-orange-200 rounded overflow-hidden"
|
||||
*ngIf="product.stock >= 20 && product.stock < 30">
|
||||
<span class="flex w-full h-2/4 bg-orange-400"></span>
|
||||
</span>
|
||||
<!-- High stock -->
|
||||
<span
|
||||
class="flex items-end ml-2 w-1 h-4 bg-green-100 rounded overflow-hidden"
|
||||
*ngIf="product.stock >= 30">
|
||||
<span class="flex w-full h-full bg-green-400"></span>
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Active -->
|
||||
<ng-container matColumnDef="active">
|
||||
<th
|
||||
class="w-24 bg-gray-50 dark:bg-black dark:bg-opacity-5"
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
disableClear>
|
||||
Active
|
||||
</th>
|
||||
<td
|
||||
class="pr-4"
|
||||
mat-cell
|
||||
*matCellDef="let product">
|
||||
<mat-icon
|
||||
class="text-green-400 icon-size-5"
|
||||
*ngIf="product.active"
|
||||
[svgIcon]="'heroicons_solid:check'"></mat-icon>
|
||||
<mat-icon
|
||||
class="text-gray-400 icon-size-5"
|
||||
*ngIf="!product.active"
|
||||
[svgIcon]="'heroicons_solid:x'"></mat-icon>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Details -->
|
||||
<ng-container matColumnDef="details">
|
||||
<th
|
||||
class="w-24 pr-8 bg-gray-50 dark:bg-black dark:bg-opacity-5"
|
||||
mat-header-cell
|
||||
*matHeaderCellDef>
|
||||
Details
|
||||
</th>
|
||||
<td
|
||||
class="pr-8"
|
||||
mat-cell
|
||||
*matCellDef="let product">
|
||||
<button
|
||||
class="min-w-10 min-h-7 h-7 px-2 leading-6"
|
||||
mat-stroked-button
|
||||
(click)="toggleDetails(product.id)">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="selectedProduct?.id === product.id ? 'heroicons_solid:chevron-up' : 'heroicons_solid:chevron-down'"></mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Product details row -->
|
||||
<ng-container matColumnDef="productDetails">
|
||||
<td
|
||||
class="p-0 border-b-0"
|
||||
mat-cell
|
||||
*matCellDef="let product"
|
||||
[attr.colspan]="productsTableColumns.length">
|
||||
<div
|
||||
class="shadow-lg overflow-hidden"
|
||||
[@expandCollapse]="selectedProduct?.id === product.id ? 'expanded' : 'collapsed'">
|
||||
<div class="flex border-b">
|
||||
<!-- Selected product form -->
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
[formGroup]="selectedProductForm">
|
||||
|
||||
<div class="flex p-8">
|
||||
|
||||
<!-- Product images and status -->
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="p-3 border rounded">
|
||||
<ng-container *ngIf="selectedProductForm.get('images').value.length; else noImage">
|
||||
<img
|
||||
class="w-30 min-w-30"
|
||||
[src]="selectedProductForm.get('images').value[selectedProductForm.get('currentImageIndex').value]">
|
||||
</ng-container>
|
||||
<ng-template #noImage>
|
||||
<span class="flex items-center min-h-20 text-lg font-semibold">NO IMAGE</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center mt-2"
|
||||
*ngIf="selectedProductForm.get('images').value.length">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="cycleImages(false)">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:arrow-narrow-left'"></mat-icon>
|
||||
</button>
|
||||
<span class="font-sm mx-2">
|
||||
{{selectedProductForm.get('currentImageIndex').value + 1}} of {{selectedProductForm.get('images').value.length}}
|
||||
</span>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="cycleImages(true)">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:arrow-narrow-right'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col mt-8">
|
||||
<span class="font-semibold mb-2">Product status</span>
|
||||
<mat-slide-toggle
|
||||
[formControlName]="'active'"
|
||||
[color]="'primary'">
|
||||
{{selectedProductForm.get('active').value === true ? 'Active' : 'Disabled'}}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-auto">
|
||||
<div class="flex flex-col w-2/4 pl-8">
|
||||
|
||||
<!-- Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Name</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'name'">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- SKU and Barcode -->
|
||||
<div class="flex">
|
||||
<mat-form-field class="w-1/3 pr-2">
|
||||
<mat-label>SKU</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'sku'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-2/3 pl-2">
|
||||
<mat-label>Barcode</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'barcode'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Category, Brand & Vendor -->
|
||||
<div class="flex">
|
||||
<mat-form-field class="w-1/3 pr-2">
|
||||
<mat-label>Category</mat-label>
|
||||
<mat-select [formControlName]="'category'">
|
||||
<mat-option
|
||||
*ngFor="let category of categories"
|
||||
[value]="category.id">
|
||||
{{category.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-1/3 px-2">
|
||||
<mat-label>Brand</mat-label>
|
||||
<mat-select [formControlName]="'brand'">
|
||||
<mat-option
|
||||
*ngFor="let brand of brands"
|
||||
[value]="brand.id">
|
||||
{{brand.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-1/3 pl-2">
|
||||
<mat-label>Vendor</mat-label>
|
||||
<mat-select [formControlName]="'vendor'">
|
||||
<mat-option
|
||||
*ngFor="let vendor of vendors"
|
||||
[value]="vendor.id">
|
||||
{{vendor.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Stock and Reserved -->
|
||||
<div class="flex">
|
||||
<mat-form-field class="w-1/3 pr-2">
|
||||
<mat-label>Stock</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[formControlName]="'stock'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-1/3 pl-2">
|
||||
<mat-label>Reserved</mat-label>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[formControlName]="'reserved'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost, Base price, Tax & Price -->
|
||||
<div class="flex flex-col w-1/4 pl-8">
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Cost</mat-label>
|
||||
<span matPrefix>$</span>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'cost'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Base Price</mat-label>
|
||||
<span matPrefix>$</span>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'basePrice'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Tax</mat-label>
|
||||
<span matSuffix>%</span>
|
||||
<input
|
||||
type="number"
|
||||
matInput
|
||||
[formControlName]="'taxPercent'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Price</mat-label>
|
||||
<span matSuffix>$</span>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'price'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Weight & Tags -->
|
||||
<div class="flex flex-col w-1/4 pl-8">
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Weight</mat-label>
|
||||
<span matSuffix>lbs.</span>
|
||||
<input
|
||||
matInput
|
||||
[formControlName]="'weight'">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Tags -->
|
||||
<ng-container *ngIf="selectedProduct && selectedProduct.tags.length">
|
||||
<span class="font-semibold">Tags</span>
|
||||
<div class="mt-1 rounded-md border shadow-sm overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center my-2 mx-3">
|
||||
<div class="flex items-center flex-auto min-w-0">
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:search'"></mat-icon>
|
||||
<input
|
||||
class="min-w-0 ml-2 py-1 border-0"
|
||||
type="text"
|
||||
placeholder="Enter tag name"
|
||||
(input)="filterTags($event)"
|
||||
(keydown)="filterTagsInputKeyDown($event)"
|
||||
[maxLength]="50"
|
||||
#newTagInput>
|
||||
</div>
|
||||
<button
|
||||
class="ml-3 w-8 h-8 min-h-8"
|
||||
mat-icon-button
|
||||
(click)="toggleTagsEditMode()">
|
||||
<mat-icon
|
||||
*ngIf="!tagsEditMode"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:pencil-alt'"></mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="tagsEditMode"
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:check'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Available tags -->
|
||||
<div class="max-h-40 leading-none overflow-y-auto border-t">
|
||||
<!-- Tags -->
|
||||
<ng-container *ngIf="!tagsEditMode">
|
||||
<ng-container *ngFor="let tag of filteredTags; trackBy: trackByFn">
|
||||
<mat-checkbox
|
||||
class="flex items-center h-10 min-h-10 px-4"
|
||||
[color]="'primary'"
|
||||
[checked]="selectedProduct.tags.includes(tag.id)"
|
||||
(change)="toggleProductTag(tag, $event)">
|
||||
{{tag.title}}
|
||||
</mat-checkbox>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<!-- Tags editing -->
|
||||
<ng-container *ngIf="tagsEditMode">
|
||||
<div class="p-4 space-y-2">
|
||||
<ng-container *ngFor="let tag of filteredTags; trackBy: trackByFn">
|
||||
<mat-form-field class="fuse-mat-dense fuse-mat-no-subscript w-full">
|
||||
<input
|
||||
matInput
|
||||
[value]="tag.title"
|
||||
(input)="updateTagTitle(tag, $event)">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="deleteTag(tag)"
|
||||
matSuffix>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:trash'"></mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center h-10 min-h-10 -ml-0.5 pl-4 pr-3 leading-none cursor-pointer border-t hover:bg-hover"
|
||||
*ngIf="shouldShowCreateTagButton(newTagInput.value)"
|
||||
(click)="createTag(newTagInput.value); newTagInput.value = ''"
|
||||
matRipple>
|
||||
<mat-icon
|
||||
class="mr-2 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:plus-circle'"></mat-icon>
|
||||
<div class="break-all">Create "<b>{{newTagInput.value}}</b>"</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full border-t px-8 py-4">
|
||||
<button
|
||||
class="-ml-4"
|
||||
mat-button
|
||||
[color]="'warn'"
|
||||
(click)="deleteSelectedProduct()">
|
||||
Delete
|
||||
</button>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="flex items-center mr-4"
|
||||
*ngIf="flashMessage">
|
||||
<ng-container *ngIf="flashMessage === 'success'">
|
||||
<mat-icon
|
||||
class="text-green-500"
|
||||
[svgIcon]="'heroicons_outline:check'"></mat-icon>
|
||||
<span class="ml-2">Product updated</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="flashMessage === 'error'">
|
||||
<mat-icon
|
||||
class="text-red-500"
|
||||
[svgIcon]="'heroicons_outline:x'"></mat-icon>
|
||||
<span class="ml-2">An error occurred, try again!</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
[color]="'primary'"
|
||||
(click)="updateSelectedProduct()">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr
|
||||
class="shadow"
|
||||
mat-header-row
|
||||
*matHeaderRowDef="productsTableColumns; sticky: true"></tr>
|
||||
<tr
|
||||
class="h-18 hover:bg-hover"
|
||||
mat-row
|
||||
*matRowDef="let product; columns: productsTableColumns;"></tr>
|
||||
<tr
|
||||
class="h-0"
|
||||
mat-row
|
||||
*matRowDef="let row; columns: ['productDetails']"></tr>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<mat-paginator
|
||||
class="sm:absolute sm:inset-x-0 sm:bottom-0 border-b sm:border-t sm:border-b-0 z-10 bg-gray-50 dark:bg-transparent"
|
||||
[ngClass]="{'pointer-events-none': isLoading}"
|
||||
[length]="pagination.length"
|
||||
[pageIndex]="pagination.page"
|
||||
[pageSize]="pagination.size"
|
||||
[pageSizeOptions]="[5, 10, 25, 100]"
|
||||
[showFirstLastButtons]="true"></mat-paginator>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noProducts>
|
||||
<div class="p-8 sm:p-16 border-t text-4xl font-semibold tracking-tight text-center">There are no products!</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,552 +0,0 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { merge, Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { FuseAnimations } from '@fuse/animations';
|
||||
import { InventoryBrand, InventoryCategory, InventoryPagination, InventoryProduct, InventoryTag, InventoryVendor } from 'app/modules/admin/apps/ecommerce/inventory/inventory.types';
|
||||
import { InventoryService } from 'app/modules/admin/apps/ecommerce/inventory/inventory.service';
|
||||
|
||||
@Component({
|
||||
selector : 'inventory-list',
|
||||
templateUrl : './inventory.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations : FuseAnimations
|
||||
})
|
||||
export class InventoryListComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
@ViewChild(MatPaginator) private _paginator: MatPaginator;
|
||||
@ViewChild(MatSort) private _sort: MatSort;
|
||||
|
||||
products$: Observable<InventoryProduct[]>;
|
||||
|
||||
brands: InventoryBrand[];
|
||||
categories: InventoryCategory[];
|
||||
filteredTags: InventoryTag[];
|
||||
flashMessage: 'success' | 'error' | null = null;
|
||||
isLoading: boolean = false;
|
||||
pagination: InventoryPagination;
|
||||
productsCount: number = 0;
|
||||
productsTableColumns: string[] = ['sku', 'name', 'price', 'stock', 'active', 'details'];
|
||||
searchInputControl: FormControl = new FormControl();
|
||||
selectedProduct: InventoryProduct | null = null;
|
||||
selectedProductForm: FormGroup;
|
||||
tags: InventoryTag[];
|
||||
tagsEditMode: boolean = false;
|
||||
vendors: InventoryVendor[];
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _formBuilder: FormBuilder,
|
||||
private _inventoryService: InventoryService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Create the selected product form
|
||||
this.selectedProductForm = this._formBuilder.group({
|
||||
id : [''],
|
||||
category : [''],
|
||||
name : ['', [Validators.required]],
|
||||
description : [''],
|
||||
tags : [[]],
|
||||
sku : [''],
|
||||
barcode : [''],
|
||||
brand : [''],
|
||||
vendor : [''],
|
||||
stock : [''],
|
||||
reserved : [''],
|
||||
cost : [''],
|
||||
basePrice : [''],
|
||||
taxPercent : [''],
|
||||
price : [''],
|
||||
weight : [''],
|
||||
thumbnail : [''],
|
||||
images : [[]],
|
||||
currentImageIndex: [0], // Image index that is currently being viewed
|
||||
active : [false]
|
||||
});
|
||||
|
||||
// Get the brands
|
||||
this._inventoryService.brands$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((brands: InventoryBrand[]) => {
|
||||
|
||||
// Update the brands
|
||||
this.brands = brands;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the categories
|
||||
this._inventoryService.categories$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((categories: InventoryCategory[]) => {
|
||||
|
||||
// Update the categories
|
||||
this.categories = categories;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the pagination
|
||||
this._inventoryService.pagination$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((pagination: InventoryPagination) => {
|
||||
|
||||
// Update the pagination
|
||||
this.pagination = pagination;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the products
|
||||
this.products$ = this._inventoryService.products$;
|
||||
this._inventoryService.products$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((products: InventoryProduct[]) => {
|
||||
|
||||
// Update the counts
|
||||
this.productsCount = products.length;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the tags
|
||||
this._inventoryService.tags$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((tags: InventoryTag[]) => {
|
||||
|
||||
// Update the tags
|
||||
this.tags = tags;
|
||||
this.filteredTags = tags;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the vendors
|
||||
this._inventoryService.vendors$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((vendors: InventoryVendor[]) => {
|
||||
|
||||
// Update the vendors
|
||||
this.vendors = vendors;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Subscribe to search input field value changes
|
||||
this.searchInputControl.valueChanges
|
||||
.pipe(
|
||||
takeUntil(this._unsubscribeAll),
|
||||
debounceTime(300),
|
||||
switchMap((query) => {
|
||||
this.closeDetails();
|
||||
this.isLoading = true;
|
||||
return this._inventoryService.getProducts(0, 10, 'name', 'asc', query);
|
||||
}),
|
||||
map(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* After view init
|
||||
*/
|
||||
ngAfterViewInit(): void
|
||||
{
|
||||
// If the user changes the sort order...
|
||||
this._sort.sortChange
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe(() => {
|
||||
// Reset back to the first page
|
||||
this._paginator.pageIndex = 0;
|
||||
|
||||
// Close the details
|
||||
this.closeDetails();
|
||||
});
|
||||
|
||||
// Get products if sort or page changes
|
||||
merge(this._sort.sortChange, this._paginator.page).pipe(
|
||||
switchMap(() => {
|
||||
this.closeDetails();
|
||||
this.isLoading = true;
|
||||
return this._inventoryService.getProducts(this._paginator.pageIndex, this._paginator.pageSize, this._sort.active, this._sort.direction);
|
||||
}),
|
||||
map(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle product details
|
||||
*
|
||||
* @param productId
|
||||
*/
|
||||
toggleDetails(productId: string): void
|
||||
{
|
||||
// If the product is already selected...
|
||||
if ( this.selectedProduct && this.selectedProduct.id === productId )
|
||||
{
|
||||
// Close the details
|
||||
this.closeDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the product by id
|
||||
this._inventoryService.getProductById(productId)
|
||||
.subscribe((product) => {
|
||||
|
||||
// Set the selected product
|
||||
this.selectedProduct = product;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Fill the form
|
||||
this.selectedProductForm.patchValue(product);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the details
|
||||
*/
|
||||
closeDetails(): void
|
||||
{
|
||||
this.selectedProduct = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle through images of selected product
|
||||
*/
|
||||
cycleImages(forward: boolean = true): void
|
||||
{
|
||||
// Get the image count and current image index
|
||||
const count = this.selectedProductForm.get('images').value.length;
|
||||
const currentIndex = this.selectedProductForm.get('currentImageIndex').value;
|
||||
|
||||
// Calculate the next and previous index
|
||||
const nextIndex = currentIndex + 1 === count ? 0 : currentIndex + 1;
|
||||
const prevIndex = currentIndex - 1 < 0 ? count - 1 : currentIndex - 1;
|
||||
|
||||
// If cycling forward...
|
||||
if ( forward )
|
||||
{
|
||||
this.selectedProductForm.get('currentImageIndex').setValue(nextIndex);
|
||||
}
|
||||
// If cycling backwards...
|
||||
else
|
||||
{
|
||||
this.selectedProductForm.get('currentImageIndex').setValue(prevIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the tags edit mode
|
||||
*/
|
||||
toggleTagsEditMode(): void
|
||||
{
|
||||
this.tagsEditMode = !this.tagsEditMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter tags
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
filterTags(event): void
|
||||
{
|
||||
// Get the value
|
||||
const value = event.target.value.toLowerCase();
|
||||
|
||||
// Filter the tags
|
||||
this.filteredTags = this.tags.filter(tag => tag.title.toLowerCase().includes(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter tags input key down event
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
filterTagsInputKeyDown(event): void
|
||||
{
|
||||
// Return if the pressed key is not 'Enter'
|
||||
if ( event.key !== 'Enter' )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is no tag available...
|
||||
if ( this.filteredTags.length === 0 )
|
||||
{
|
||||
// Create the tag
|
||||
this.createTag(event.target.value);
|
||||
|
||||
// Clear the input
|
||||
event.target.value = '';
|
||||
|
||||
// Return
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a tag...
|
||||
const tag = this.filteredTags[0];
|
||||
const isTagApplied = this.selectedProduct.tags.find((id) => id === tag.id);
|
||||
|
||||
// If the found tag is already applied to the contact...
|
||||
if ( isTagApplied )
|
||||
{
|
||||
// Remove the tag from the contact
|
||||
this.removeTagFromProduct(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise add the tag to the contact
|
||||
this.addTagToProduct(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tag
|
||||
*
|
||||
* @param title
|
||||
*/
|
||||
createTag(title: string): void
|
||||
{
|
||||
const tag = {
|
||||
title
|
||||
};
|
||||
|
||||
// Create tag on the server
|
||||
this._inventoryService.createTag(tag)
|
||||
.subscribe((response) => {
|
||||
|
||||
// Add the tag to the product
|
||||
this.addTagToProduct(response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tag title
|
||||
*
|
||||
* @param tag
|
||||
* @param event
|
||||
*/
|
||||
updateTagTitle(tag: InventoryTag, event): void
|
||||
{
|
||||
// Update the title on the tag
|
||||
tag.title = event.target.value;
|
||||
|
||||
// Update the tag on the server
|
||||
this._inventoryService.updateTag(tag.id, tag)
|
||||
.pipe(debounceTime(300))
|
||||
.subscribe();
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the tag
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
deleteTag(tag: InventoryTag): void
|
||||
{
|
||||
// Delete the tag from the server
|
||||
this._inventoryService.deleteTag(tag.id).subscribe();
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tag to the product
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
addTagToProduct(tag: InventoryTag): void
|
||||
{
|
||||
// Add the tag
|
||||
this.selectedProduct.tags.unshift(tag.id);
|
||||
|
||||
// Update the selected product form
|
||||
this.selectedProductForm.get('tags').patchValue(this.selectedProduct.tags);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tag from the product
|
||||
*
|
||||
* @param tag
|
||||
*/
|
||||
removeTagFromProduct(tag: InventoryTag): void
|
||||
{
|
||||
// Remove the tag
|
||||
this.selectedProduct.tags.splice(this.selectedProduct.tags.findIndex(item => item === tag.id), 1);
|
||||
|
||||
// Update the selected product form
|
||||
this.selectedProductForm.get('tags').patchValue(this.selectedProduct.tags);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle product tag
|
||||
*
|
||||
* @param tag
|
||||
* @param change
|
||||
*/
|
||||
toggleProductTag(tag: InventoryTag, change: MatCheckboxChange): void
|
||||
{
|
||||
if ( change.checked )
|
||||
{
|
||||
this.addTagToProduct(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.removeTagFromProduct(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the create tag button be visible
|
||||
*
|
||||
* @param inputValue
|
||||
*/
|
||||
shouldShowCreateTagButton(inputValue: string): boolean
|
||||
{
|
||||
return !!!(inputValue === '' || this.tags.findIndex(tag => tag.title.toLowerCase() === inputValue.toLowerCase()) > -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create product
|
||||
*/
|
||||
createProduct(): void
|
||||
{
|
||||
// Create the product
|
||||
this._inventoryService.createProduct().subscribe((newProduct) => {
|
||||
|
||||
// Go to new product
|
||||
this.selectedProduct = newProduct;
|
||||
|
||||
// Fill the form
|
||||
this.selectedProductForm.patchValue(newProduct);
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected product using the form mock-api
|
||||
*/
|
||||
updateSelectedProduct(): void
|
||||
{
|
||||
// Get the product object
|
||||
const product = this.selectedProductForm.getRawValue();
|
||||
|
||||
// Remove the currentImageIndex field
|
||||
delete product.currentImageIndex;
|
||||
|
||||
// Update the product on the server
|
||||
this._inventoryService.updateProduct(product.id, product).subscribe(() => {
|
||||
|
||||
// Show a success message
|
||||
this.showFlashMessage('success');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the selected product using the form mock-api
|
||||
*/
|
||||
deleteSelectedProduct(): void
|
||||
{
|
||||
// Get the product object
|
||||
const product = this.selectedProductForm.getRawValue();
|
||||
|
||||
// Delete the product on the server
|
||||
this._inventoryService.deleteProduct(product.id).subscribe(() => {
|
||||
|
||||
// Close the details
|
||||
this.closeDetails();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show flash message
|
||||
*/
|
||||
showFlashMessage(type: 'success' | 'error'): void
|
||||
{
|
||||
// Show the message
|
||||
this.flashMessage = type;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Hide it after 3 seconds
|
||||
setTimeout(() => {
|
||||
|
||||
this.flashMessage = null;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<div class="flex flex-col flex-auto p-6 md:p-8">
|
||||
|
||||
<!-- Close button -->
|
||||
<div class="flex items-center justify-end">
|
||||
<button
|
||||
mat-icon-button
|
||||
[routerLink]="['../']">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="aspect-w-9 aspect-h-6 mt-8">
|
||||
<div class="flex items-center justify-center border rounded-lg bg-gray-50 dark:bg-card">
|
||||
<ng-container *ngIf="item.type === 'folder'">
|
||||
<mat-icon
|
||||
class="icon-size-14 text-hint"
|
||||
[svgIcon]="'iconsmind:folder'"></mat-icon>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.type !== 'folder'">
|
||||
<mat-icon
|
||||
class="icon-size-14 text-hint"
|
||||
[svgIcon]="'iconsmind:file'"></mat-icon>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name & Type -->
|
||||
<div class="flex flex-col items-start mt-8">
|
||||
<div class="text-xl font-medium">{{item.name}}</div>
|
||||
<div
|
||||
class="mt-1 px-1.5 rounded text-sm font-semibold leading-5 text-white"
|
||||
[class.bg-indigo-600]="item.type === 'folder'"
|
||||
[class.bg-red-600]="item.type === 'PDF'"
|
||||
[class.bg-blue-600]="item.type === 'DOC'"
|
||||
[class.bg-green-600]="item.type === 'XLS'"
|
||||
[class.bg-gray-600]="item.type === 'TXT'"
|
||||
[class.bg-amber-600]="item.type === 'JPG'">
|
||||
{{item.type.toUpperCase()}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Information -->
|
||||
<div class="text-lg font-medium mt-8">Information</div>
|
||||
<div class="flex flex-col mt-4 border-t border-b divide-y font-medium">
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div class="text-secondary">Created By</div>
|
||||
<div>{{item.createdBy}}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div class="text-secondary">Created At</div>
|
||||
<div>{{item.createdAt}}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div class="text-secondary">Modified At</div>
|
||||
<div>{{item.modifiedAt}}</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div class="text-secondary">Size</div>
|
||||
<div>{{item.size}}</div>
|
||||
</div>
|
||||
<ng-container *ngIf="item.contents">
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div class="text-secondary">Contents</div>
|
||||
<div>{{item.contents}}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="flex items-center justify-between mt-8">
|
||||
<div class="text-lg font-medium">Description</div>
|
||||
<button mat-icon-button>
|
||||
<mat-icon
|
||||
class="icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:pencil'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex mt-2 border-t">
|
||||
<div class="py-3">
|
||||
<ng-container *ngIf="item.description">
|
||||
<div>{{item.description}}</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!item.description">
|
||||
<div class="italic text-secondary">Click here to add a description</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="grid grid-cols-2 gap-4 w-full mt-8">
|
||||
<button
|
||||
class="flex-auto"
|
||||
mat-flat-button
|
||||
[color]="'primary'">
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
class="flex-auto"
|
||||
mat-stroked-button>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,91 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { MatDrawerToggleResult } from '@angular/material/sidenav';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { FileManagerListComponent } from 'app/modules/admin/apps/file-manager/list/list.component';
|
||||
import { FileManagerService } from 'app/modules/admin/apps/file-manager/file-manager.service';
|
||||
import { Item } from 'app/modules/admin/apps/file-manager/file-manager.types';
|
||||
|
||||
@Component({
|
||||
selector : 'file-manager-details',
|
||||
templateUrl : './details.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FileManagerDetailsComponent implements OnInit, OnDestroy
|
||||
{
|
||||
item: Item;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _fileManagerListComponent: FileManagerListComponent,
|
||||
private _fileManagerService: FileManagerService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Open the drawer
|
||||
this._fileManagerListComponent.matDrawer.open();
|
||||
|
||||
// Get the item
|
||||
this._fileManagerService.item$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((item: Item) => {
|
||||
|
||||
// Open the drawer in case it is closed
|
||||
this._fileManagerListComponent.matDrawer.open();
|
||||
|
||||
// Get the item
|
||||
this.item = item;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close the drawer
|
||||
*/
|
||||
closeDrawer(): Promise<MatDrawerToggleResult>
|
||||
{
|
||||
return this._fileManagerListComponent.matDrawer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector : 'file-manager',
|
||||
templateUrl : './file-manager.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FileManagerComponent
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FileManagerDetailsComponent } from 'app/modules/admin/apps/file-manager/details/details.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CanDeactivateFileManagerDetails implements CanDeactivate<FileManagerDetailsComponent>
|
||||
{
|
||||
canDeactivate(
|
||||
component: FileManagerDetailsComponent,
|
||||
currentRoute: ActivatedRouteSnapshot,
|
||||
currentState: RouterStateSnapshot,
|
||||
nextState: RouterStateSnapshot
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
|
||||
{
|
||||
// Get the next route
|
||||
let nextRoute: ActivatedRouteSnapshot = nextState.root;
|
||||
while ( nextRoute.firstChild )
|
||||
{
|
||||
nextRoute = nextRoute.firstChild;
|
||||
}
|
||||
|
||||
// If the next state doesn't contain '/files'
|
||||
// it means we are navigating away from the
|
||||
// tasks app
|
||||
if ( !nextState.url.includes('/file-manager') )
|
||||
{
|
||||
// Let it navigate
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we are navigating to another task...
|
||||
if ( nextRoute.paramMap.get('id') )
|
||||
{
|
||||
// Just navigate
|
||||
return true;
|
||||
}
|
||||
// Otherwise...
|
||||
else
|
||||
{
|
||||
// Close the drawer first, and then navigate
|
||||
return component.closeDrawer().then(() => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { fileManagerRoutes } from 'app/modules/admin/apps/file-manager/file-manager.routing';
|
||||
import { FileManagerComponent } from 'app/modules/admin/apps/file-manager/file-manager.component';
|
||||
import { FileManagerDetailsComponent } from 'app/modules/admin/apps/file-manager/details/details.component';
|
||||
import { FileManagerListComponent } from 'app/modules/admin/apps/file-manager/list/list.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FileManagerComponent,
|
||||
FileManagerDetailsComponent,
|
||||
FileManagerListComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild(fileManagerRoutes),
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatSidenavModule,
|
||||
MatTooltipModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class FileManagerModule
|
||||
{
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { FileManagerService } from 'app/modules/admin/apps/file-manager/file-manager.service';
|
||||
import { Item } from 'app/modules/admin/apps/file-manager/file-manager.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FileManagerItemsResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _fileManagerService: FileManagerService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Item[]>
|
||||
{
|
||||
return this._fileManagerService.getItems();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FileManagerItemResolver implements Resolve<any>
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _router: Router,
|
||||
private _fileManagerService: FileManagerService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolver
|
||||
*
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Item>
|
||||
{
|
||||
return this._fileManagerService.getItemById(route.paramMap.get('id'))
|
||||
.pipe(
|
||||
// Error here means the requested task is not available
|
||||
catchError((error) => {
|
||||
|
||||
// Log the error
|
||||
console.error(error);
|
||||
|
||||
// Get the parent url
|
||||
const parentUrl = state.url.split('/').slice(0, -1).join('/');
|
||||
|
||||
// Navigate to there
|
||||
this._router.navigateByUrl(parentUrl);
|
||||
|
||||
// Throw an error
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { CanDeactivateFileManagerDetails } from 'app/modules/admin/apps/file-manager/file-manager.guards';
|
||||
import { FileManagerComponent } from 'app/modules/admin/apps/file-manager/file-manager.component';
|
||||
import { FileManagerListComponent } from 'app/modules/admin/apps/file-manager/list/list.component';
|
||||
import { FileManagerDetailsComponent } from 'app/modules/admin/apps/file-manager/details/details.component';
|
||||
import { FileManagerItemResolver, FileManagerItemsResolver } from 'app/modules/admin/apps/file-manager/file-manager.resolvers';
|
||||
|
||||
export const fileManagerRoutes: Route[] = [
|
||||
{
|
||||
path : '',
|
||||
component: FileManagerComponent,
|
||||
children : [
|
||||
{
|
||||
path : '',
|
||||
component: FileManagerListComponent,
|
||||
resolve : {
|
||||
items: FileManagerItemsResolver
|
||||
},
|
||||
children : [
|
||||
{
|
||||
path : ':id',
|
||||
component : FileManagerDetailsComponent,
|
||||
resolve : {
|
||||
item: FileManagerItemResolver
|
||||
},
|
||||
canDeactivate: [CanDeactivateFileManagerDetails]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
||||
import { map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { Item, Items } from 'app/modules/admin/apps/file-manager/file-manager.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FileManagerService
|
||||
{
|
||||
// Private
|
||||
private _item: BehaviorSubject<Item | null> = new BehaviorSubject(null);
|
||||
private _items: BehaviorSubject<Items | null> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _httpClient: HttpClient)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for items
|
||||
*/
|
||||
get items$(): Observable<Items>
|
||||
{
|
||||
return this._items.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for item
|
||||
*/
|
||||
get item$(): Observable<Item>
|
||||
{
|
||||
return this._item.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get items
|
||||
*/
|
||||
getItems(): Observable<Item[]>
|
||||
{
|
||||
return this._httpClient.get<Items>('api/apps/file-manager').pipe(
|
||||
tap((response: any) => {
|
||||
this._items.next(response);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item by id
|
||||
*/
|
||||
getItemById(id: string): Observable<Item>
|
||||
{
|
||||
return this._items.pipe(
|
||||
take(1),
|
||||
map((items) => {
|
||||
|
||||
// Find within the folders and files
|
||||
const item = [...items.folders, ...items.files].find(value => value.id === id) || null;
|
||||
|
||||
// Update the item
|
||||
this._item.next(item);
|
||||
|
||||
// Return the item
|
||||
return item;
|
||||
}),
|
||||
switchMap((item) => {
|
||||
|
||||
if ( !item )
|
||||
{
|
||||
return throwError('Could not found the item with id of ' + id + '!');
|
||||
}
|
||||
|
||||
return of(item);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export interface Items
|
||||
{
|
||||
folders: Item[];
|
||||
files: Item[];
|
||||
}
|
||||
|
||||
export interface Item
|
||||
{
|
||||
id?: string;
|
||||
name?: string;
|
||||
createdBy?: string;
|
||||
createdAt?: string;
|
||||
modifiedAt?: string;
|
||||
size?: string;
|
||||
type?: string;
|
||||
contents?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
<div class="absolute inset-0 flex flex-col min-w-0 overflow-hidden">
|
||||
|
||||
<mat-drawer-container
|
||||
class="flex-auto h-full bg-card dark:bg-transparent"
|
||||
(backdropClick)="onBackdropClicked()">
|
||||
|
||||
<!-- Drawer -->
|
||||
<mat-drawer
|
||||
class="w-full sm:w-100 dark:bg-gray-900"
|
||||
[mode]="drawerMode"
|
||||
[opened]="false"
|
||||
[position]="'end'"
|
||||
[disableClose]="true"
|
||||
#matDrawer>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-drawer>
|
||||
|
||||
<mat-drawer-content class="flex flex-col bg-gray-100 dark:bg-transparent">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col flex-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center sm:justify-between p-6 sm:py-12 md:px-8 border-b bg-card dark:bg-transparent">
|
||||
<!-- Title -->
|
||||
<div>
|
||||
<div class="text-4xl font-extrabold tracking-tight leading-none">File Manager</div>
|
||||
<div class="flex items-center mt-0.5 font-medium text-secondary">
|
||||
{{items.folders.length}} folders, {{items.files.length}} files
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="mt-4 sm:mt-0">
|
||||
<!-- Upload button -->
|
||||
<button
|
||||
mat-flat-button
|
||||
[color]="'primary'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:plus'"></mat-icon>
|
||||
<span class="ml-2 mr-1">Upload file</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items list -->
|
||||
<ng-container *ngIf="items && items.folders.length && items.files.length > 0; else noItems">
|
||||
<div class="p-6 md:p-8">
|
||||
<!-- Folders -->
|
||||
<div class="font-medium">Folders</div>
|
||||
<div
|
||||
class="grid gap-4 mt-4"
|
||||
style="grid-template-columns: repeat(auto-fill,minmax(160px,1fr))">
|
||||
<ng-container *ngFor="let folder of items.folders; trackBy:trackByFn">
|
||||
<ng-container *ngTemplateOutlet="item, context: {$implicit: folder}"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Files -->
|
||||
<div class="font-medium mt-8">Files</div>
|
||||
<div
|
||||
class="grid gap-4 mt-4"
|
||||
style="grid-template-columns: repeat(auto-fill,minmax(160px,1fr))">
|
||||
<ng-container *ngFor="let file of items.files; trackBy:trackByFn">
|
||||
<ng-container *ngTemplateOutlet="item, context: {$implicit: file}"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template
|
||||
#item
|
||||
let-item>
|
||||
<div
|
||||
class="flex flex-col shadow rounded-2xl cursor-pointer bg-card"
|
||||
(click)="goToItem(item.id)">
|
||||
<div class="aspect-w-9 aspect-h-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<!-- Icons -->
|
||||
<ng-container [ngSwitch]="item.type">
|
||||
<!-- Folder -->
|
||||
<ng-container *ngSwitchCase="'folder'">
|
||||
<mat-icon
|
||||
class="icon-size-14 text-hint"
|
||||
[svgIcon]="'iconsmind:folder'"></mat-icon>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngSwitchDefault>
|
||||
<div class="relative">
|
||||
<mat-icon
|
||||
class="icon-size-14 text-hint"
|
||||
[svgIcon]="'iconsmind:file'"></mat-icon>
|
||||
<div
|
||||
class="absolute left-0 bottom-0 px-1.5 rounded text-sm font-semibold leading-5 text-white"
|
||||
[class.bg-red-600]="item.type === 'PDF'"
|
||||
[class.bg-blue-600]="item.type === 'DOC'"
|
||||
[class.bg-green-600]="item.type === 'XLS'"
|
||||
[class.bg-gray-600]="item.type === 'TXT'"
|
||||
[class.bg-amber-600]="item.type === 'JPG'">
|
||||
{{item.type.toUpperCase()}}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pb-4 px-4 text-center text-sm font-medium"
|
||||
[matTooltip]="item.name">
|
||||
<div class="truncate">{{item.name}}</div>
|
||||
<ng-container *ngIf="item.contents">
|
||||
<div class="mt-0.5 text-secondary truncate">{{item.contents}}</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- No items template -->
|
||||
<ng-template #noItems>
|
||||
<div class="flex flex-auto flex-col items-center justify-center bg-gray-100 dark:bg-transparent">
|
||||
<mat-icon
|
||||
class="icon-size-24"
|
||||
[svgIcon]="'iconsmind:file_hide'"></mat-icon>
|
||||
<div class="mt-4 text-2xl font-semibold tracking-tight text-secondary">There are no items!</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
</mat-drawer-content>
|
||||
|
||||
</mat-drawer-container>
|
||||
|
||||
</div>
|
||||
@@ -1,147 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { FuseMediaWatcherService } from '@fuse/services/media-watcher';
|
||||
import { FuseNavigationService } from '@fuse/components/navigation';
|
||||
import { FileManagerService } from 'app/modules/admin/apps/file-manager/file-manager.service';
|
||||
import { Item, Items } from 'app/modules/admin/apps/file-manager/file-manager.types';
|
||||
|
||||
@Component({
|
||||
selector : 'file-manager-list',
|
||||
templateUrl : './list.component.html',
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FileManagerListComponent implements OnInit, OnDestroy
|
||||
{
|
||||
@ViewChild('matDrawer', {static: true}) matDrawer: MatDrawer;
|
||||
drawerMode: 'side' | 'over';
|
||||
selectedItem: Item;
|
||||
items: Items;
|
||||
private _unsubscribeAll: Subject<any> = new Subject<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(DOCUMENT) private _document: any,
|
||||
private _router: Router,
|
||||
private _fileManagerService: FileManagerService,
|
||||
private _fuseMediaWatcherService: FuseMediaWatcherService,
|
||||
private _fuseNavigationService: FuseNavigationService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the items
|
||||
this._fileManagerService.items$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((items: Items) => {
|
||||
this.items = items;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Get the item
|
||||
this._fileManagerService.item$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((item: Item) => {
|
||||
this.selectedItem = item;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Subscribe to media query change
|
||||
this._fuseMediaWatcherService.onMediaQueryChange$('(min-width: 1440px)')
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((state) => {
|
||||
|
||||
// Calculate the drawer mode
|
||||
this.drawerMode = state.matches ? 'side' : 'over';
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Go to item
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
goToItem(id: string): void
|
||||
{
|
||||
// Get the current activated route
|
||||
let route = this._activatedRoute;
|
||||
while ( route.firstChild )
|
||||
{
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
// Go to item
|
||||
this._router.navigate(['../', id], {relativeTo: route});
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* On backdrop clicked
|
||||
*/
|
||||
onBackdropClicked(): void
|
||||
{
|
||||
// Get the current activated route
|
||||
let route = this._activatedRoute;
|
||||
while ( route.firstChild )
|
||||
{
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
// Go back to the parent route
|
||||
this._router.navigate(['../'], {relativeTo: route});
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<div class="flex flex-col flex-auto min-w-0">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col items-center p-6 sm:p-10">
|
||||
<div class="flex flex-col w-full max-w-4xl">
|
||||
<div class="-ml-4 sm:mt-8">
|
||||
<button
|
||||
mat-button
|
||||
[routerLink]="['../']"
|
||||
[color]="'primary'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
||||
<span class="ml-2">Back to Help Center</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-4xl sm:text-7xl font-extrabold tracking-tight leading-tight">
|
||||
Frequently Asked Questions
|
||||
</div>
|
||||
<ng-container *ngFor="let faqCategory of faqCategories; trackBy: trackByFn">
|
||||
<div class="mt-12 sm:mt-16 text-3xl font-bold leading-tight tracking-tight">{{faqCategory.title}}</div>
|
||||
<mat-accordion class="max-w-4xl mt-8">
|
||||
<mat-expansion-panel *ngFor="let faq of faqCategory.faqs; trackBy: trackByFn">
|
||||
<mat-expansion-panel-header [collapsedHeight]="'56px'">
|
||||
<mat-panel-title class="font-medium leading-tight">{{faq.question}}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
{{faq.answer}}
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { HelpCenterService } from 'app/modules/admin/apps/help-center/help-center.service';
|
||||
import { FaqCategory } from 'app/modules/admin/apps/help-center/help-center.type';
|
||||
|
||||
@Component({
|
||||
selector : 'help-center-faqs',
|
||||
templateUrl : './faqs.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class HelpCenterFaqsComponent implements OnInit, OnDestroy
|
||||
{
|
||||
faqCategories: FaqCategory[];
|
||||
private _unsubscribeAll: Subject<any> = new Subject();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _helpCenterService: HelpCenterService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the FAQs
|
||||
this._helpCenterService.faqs$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((faqCategories) => {
|
||||
this.faqCategories = faqCategories;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<div class="flex flex-col flex-auto min-w-0">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col items-center p-6 sm:p-10">
|
||||
<div class="flex flex-col w-full max-w-4xl">
|
||||
<div class="-ml-4 sm:mt-8">
|
||||
<button
|
||||
mat-button
|
||||
[routerLink]="['../../../../']"
|
||||
[color]="'primary'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
||||
<span class="ml-2">Back to Help Center</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-4xl sm:text-7xl font-extrabold tracking-tight leading-tight">
|
||||
{{guideCategory.title}}
|
||||
</div>
|
||||
<!-- Guides -->
|
||||
<div class="flex flex-col items-start mt-8 sm:mt-12 space-y-2">
|
||||
<ng-container *ngFor="let guide of guideCategory.guides; trackBy: trackByFn">
|
||||
<a
|
||||
class="font-medium hover:underline text-primary-500"
|
||||
[routerLink]="[guide.slug]">
|
||||
{{guide.title}}
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { HelpCenterService } from 'app/modules/admin/apps/help-center/help-center.service';
|
||||
import { GuideCategory } from 'app/modules/admin/apps/help-center/help-center.type';
|
||||
|
||||
@Component({
|
||||
selector : 'help-center-guides-category',
|
||||
templateUrl : './category.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class HelpCenterGuidesCategoryComponent implements OnInit, OnDestroy
|
||||
{
|
||||
guideCategory: GuideCategory;
|
||||
private _unsubscribeAll: Subject<any> = new Subject();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _helpCenterService: HelpCenterService,
|
||||
private _router: Router
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the Guides
|
||||
this._helpCenterService.guides$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((guideCategories) => {
|
||||
this.guideCategory = guideCategories[0];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<div class="flex flex-col flex-auto min-w-0">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col items-center p-6 sm:p-10">
|
||||
<div class="flex flex-col w-full max-w-4xl">
|
||||
<div class="-ml-4 sm:mt-8">
|
||||
<button
|
||||
mat-button
|
||||
[routerLink]="['../']"
|
||||
[color]="'primary'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
||||
<span class="ml-2">Back to {{guideCategory.title}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-4xl sm:text-7xl font-extrabold tracking-tight leading-tight">{{guideCategory.guides[0].title}}</div>
|
||||
<div class="mt-1 sm:text-2xl tracking-tight text-secondary">{{guideCategory.guides[0].subtitle}}</div>
|
||||
|
||||
<!-- Guide -->
|
||||
<div
|
||||
class="mt-8 sm:mt-12 max-w-none prose prose-sm"
|
||||
[innerHTML]="guideCategory.guides[0].content"></div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-10 pt-8 border-t">
|
||||
<div class="text-sm font-medium text-secondary">Last updated 2 months ago</div>
|
||||
<div class="flex items-center mt-2 sm:mt-0">
|
||||
<div class="font-medium text-secondary">Was this page helpful?</div>
|
||||
<div class="ml-4">
|
||||
<button mat-icon-button>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:thumb-up'"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon [svgIcon]="'heroicons_outline:thumb-down'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next -->
|
||||
<a
|
||||
class="mt-8 flex items-center justify-between p-6 sm:px-10 rounded-2xl shadow hover:shadow-lg bg-card transform transition-shadow ease-in-out duration-150"
|
||||
[routerLink]="'.'">
|
||||
<div>
|
||||
<div class="text-secondary">Next</div>
|
||||
<div class="text-lg font-semibold">Removing a media from a project</div>
|
||||
</div>
|
||||
<mat-icon
|
||||
class="ml-3"
|
||||
[svgIcon]="'heroicons_outline:arrow-right'"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { HelpCenterService } from 'app/modules/admin/apps/help-center/help-center.service';
|
||||
import { GuideCategory } from 'app/modules/admin/apps/help-center/help-center.type';
|
||||
|
||||
@Component({
|
||||
selector : 'help-center-guides-guide',
|
||||
templateUrl : './guide.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class HelpCenterGuidesGuideComponent implements OnInit, OnDestroy
|
||||
{
|
||||
guideCategory: GuideCategory;
|
||||
private _unsubscribeAll: Subject<any> = new Subject();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _helpCenterService: HelpCenterService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the Guides
|
||||
this._helpCenterService.guide$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((guideCategory) => {
|
||||
this.guideCategory = guideCategory;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<div class="flex flex-col flex-auto min-w-0">
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex flex-col items-center p-6 sm:p-10">
|
||||
<div class="flex flex-col w-full max-w-4xl">
|
||||
<div class="-ml-4 sm:mt-8">
|
||||
<button
|
||||
mat-button
|
||||
[routerLink]="['../../../']"
|
||||
[color]="'primary'">
|
||||
<mat-icon [svgIcon]="'heroicons_outline:arrow-narrow-left'"></mat-icon>
|
||||
<span class="ml-2">Back to Help Center</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-4xl sm:text-7xl font-extrabold tracking-tight leading-tight">
|
||||
Guides & Resources
|
||||
</div>
|
||||
<!-- Guides -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 grid-flow-row gap-y-12 sm:gap-x-4 mt-8 sm:mt-12">
|
||||
<ng-container *ngFor="let guideCategory of guideCategories; trackBy: trackByFn">
|
||||
<div class="flex flex-col items-start">
|
||||
<a
|
||||
class="flex items-center mb-1 text-2xl font-semibold"
|
||||
[routerLink]="[guideCategory.slug]">
|
||||
{{guideCategory.title}}
|
||||
</a>
|
||||
<ng-container *ngFor="let guide of guideCategory.guides; trackBy: trackByFn">
|
||||
<a
|
||||
class="mt-3 font-medium hover:underline text-primary-500"
|
||||
[routerLink]="[guideCategory.slug, guide.slug]">
|
||||
{{guide.title}}
|
||||
</a>
|
||||
</ng-container>
|
||||
<a
|
||||
class="flex items-center mt-5 pl-4 pr-3 py-0.5 rounded-full cursor-pointer bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||
*ngIf="guideCategory.totalGuides > guideCategory.visibleGuides"
|
||||
[routerLink]="guideCategory.slug">
|
||||
<span class="text-sm font-medium text-secondary">View All</span>
|
||||
<mat-icon
|
||||
class="ml-2 icon-size-5"
|
||||
[svgIcon]="'heroicons_solid:arrow-narrow-right'"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { HelpCenterService } from 'app/modules/admin/apps/help-center/help-center.service';
|
||||
import { GuideCategory } from 'app/modules/admin/apps/help-center/help-center.type';
|
||||
|
||||
@Component({
|
||||
selector : 'help-center-guides',
|
||||
templateUrl : './guides.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class HelpCenterGuidesComponent implements OnInit, OnDestroy
|
||||
{
|
||||
guideCategories: GuideCategory[];
|
||||
private _unsubscribeAll: Subject<any> = new Subject();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _helpCenterService: HelpCenterService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the Guide categories
|
||||
this._helpCenterService.guides$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((guideCategories) => {
|
||||
this.guideCategories = guideCategories;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<div class="flex flex-col flex-auto min-w-0">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="relative pt-8 pb-28 px-4 sm:pt-20 sm:pb-48 sm:px-16 overflow-hidden bg-gray-800 dark">
|
||||
<!-- Background - @formatter:off -->
|
||||
<!-- Rings -->
|
||||
<svg class="absolute inset-0 pointer-events-none"
|
||||
viewBox="0 0 960 540" width="100%" height="100%" preserveAspectRatio="xMidYMax slice" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="text-gray-700 opacity-25" fill="none" stroke="currentColor" stroke-width="100">
|
||||
<circle r="234" cx="196" cy="23"></circle>
|
||||
<circle r="234" cx="790" cy="491"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<!-- @formatter:on -->
|
||||
<div class="z-10 relative flex flex-col items-center">
|
||||
<h2 class="text-xl font-semibold">HELP CENTER</h2>
|
||||
<div class="mt-1 text-4xl sm:text-7xl font-extrabold tracking-tight leading-tight text-center">
|
||||
How can we help you today?
|
||||
</div>
|
||||
<div class="mt-3 sm:text-2xl text-center tracking-tight text-secondary">
|
||||
Search for a topic or question, check out our FAQs and guides, contact us for detailed support
|
||||
</div>
|
||||
<mat-form-field class="fuse-mat-no-subscript fuse-mat-rounded fuse-mat-bold w-full max-w-80 sm:max-w-120 mt-10 sm:mt-20">
|
||||
<input
|
||||
matInput
|
||||
[placeholder]="'Enter a question, topic or keyword'">
|
||||
<mat-icon
|
||||
matPrefix
|
||||
[svgIcon]="'heroicons_outline:search'"></mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center pb-6 px-6 sm:pb-10 sm:px-10">
|
||||
<!-- Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-y-8 md:gap-y-0 md:gap-x-6 w-full max-w-sm md:max-w-4xl -mt-16 sm:-mt-24">
|
||||
<!-- FAQs card -->
|
||||
<div class="relative flex flex-col rounded-2xl shadow hover:shadow-lg overflow-hidden bg-card transform transition-shadow ease-in-out duration-150">
|
||||
<div class="flex flex-col flex-auto items-center p-8 text-center">
|
||||
<div class="text-2xl font-semibold">FAQs</div>
|
||||
<div class="md:max-w-40 mt-1 text-secondary">Frequently asked questions and answers</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center py-4 px-8 text-primary bg-gray-50 dark:bg-transparent dark:border-t">
|
||||
<a
|
||||
class="flex items-center"
|
||||
[routerLink]="['faqs']">
|
||||
<span class="absolute inset-0"></span>
|
||||
<span class="font-medium">Go to FAQs</span>
|
||||
<mat-icon
|
||||
class="ml-2 icon-size-5 text-current"
|
||||
[svgIcon]="'heroicons_solid:arrow-narrow-right'"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Guides card -->
|
||||
<div class="relative flex flex-col rounded-2xl shadow hover:shadow-lg overflow-hidden bg-card transform transition-shadow ease-in-out duration-150">
|
||||
<div class="flex flex-col flex-auto items-center p-8 text-center">
|
||||
<div class="text-2xl font-semibold">Guides</div>
|
||||
<div class="md:max-w-40 mt-1 text-secondary">Articles and resources to guide you</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center py-4 px-8 text-primary-500 bg-gray-50 dark:bg-transparent dark:border-t">
|
||||
<a
|
||||
class="flex items-center"
|
||||
[routerLink]="['guides']">
|
||||
<span class="absolute inset-0"></span>
|
||||
<span class="font-medium">Check guides</span>
|
||||
<mat-icon
|
||||
class="ml-2 icon-size-5 text-current"
|
||||
[svgIcon]="'heroicons_solid:arrow-narrow-right'"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Support card -->
|
||||
<div class="relative flex flex-col rounded-2xl shadow hover:shadow-lg overflow-hidden bg-card transform transition-shadow ease-in-out duration-150">
|
||||
<div class="flex flex-col flex-auto items-center p-8 text-center">
|
||||
<div class="text-2xl font-semibold">Support</div>
|
||||
<div class="md:max-w-40 mt-1 text-secondary">Contact us for more detailed support</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center py-4 px-8 text-primary-500 bg-gray-50 dark:bg-transparent dark:border-t">
|
||||
<a
|
||||
class="flex items-center"
|
||||
[routerLink]="['support']">
|
||||
<span class="absolute inset-0"></span>
|
||||
<span class="font-medium">Contact us</span>
|
||||
<mat-icon
|
||||
class="ml-2 icon-size-5 text-current"
|
||||
[svgIcon]="'heroicons_solid:arrow-narrow-right'"></mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- FAQs -->
|
||||
<div class="mt-24 text-3xl sm:text-5xl font-extrabold leading-tight tracking-tight text-center">Most frequently asked questions</div>
|
||||
<div class="mt-2 text-xl text-center text-secondary">Here are the most frequently asked questions you may check before getting started</div>
|
||||
<mat-accordion class="max-w-4xl mt-12">
|
||||
<mat-expansion-panel *ngFor="let faq of faqCategory.faqs; trackBy: trackByFn">
|
||||
<mat-expansion-panel-header [collapsedHeight]="'56px'">
|
||||
<mat-panel-title>{{faq.question}}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
{{faq.answer}}
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { HelpCenterService } from 'app/modules/admin/apps/help-center/help-center.service';
|
||||
import { FaqCategory } from 'app/modules/admin/apps/help-center/help-center.type';
|
||||
|
||||
@Component({
|
||||
selector : 'help-center',
|
||||
templateUrl : './help-center.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class HelpCenterComponent implements OnInit, OnDestroy
|
||||
{
|
||||
faqCategory: FaqCategory;
|
||||
private _unsubscribeAll: Subject<any> = new Subject();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(private _helpCenterService: HelpCenterService)
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the FAQs
|
||||
this._helpCenterService.faqs$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((faqCategories) => {
|
||||
this.faqCategory = faqCategories[0];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user