Compare commits

...

33 Commits

Author SHA1 Message Date
sercan
fcfba4c9e4 Merge remote-tracking branch 'origin/demo' into starter 2021-04-30 19:40:30 +03:00
sercan
40894e0aa3 Merge remote-tracking branch 'origin/demo' into starter
# Conflicts:
#	src/app/app.routing.ts
#	src/app/mock-api/common/navigation/data.ts
#	src/app/modules/admin/apps/academy/academy.service.ts
#	src/app/modules/admin/apps/academy/details/details.component.html
#	src/app/modules/admin/apps/academy/list/list.component.html
#	src/app/modules/admin/apps/mailbox/list/list.component.ts
#	src/app/modules/admin/docs/changelog/changelog.ts
#	src/app/modules/admin/pages/pricing/modern/modern.component.html
#	src/app/modules/admin/pages/pricing/simple/simple.component.html
#	src/app/modules/admin/pages/pricing/single/single.component.html
#	src/app/modules/admin/pages/pricing/table/table.component.html
2021-04-30 19:40:07 +03:00
sercan
5dd60c816c (apps/chat) Small adjustments and tweaks for Dark mode 2021-04-30 19:39:02 +03:00
sercan
0ac967a945 Removed optional chaining operators to support Node v12
Set the version on .nvmrc to 12
2021-04-30 19:27:40 +03:00
sercan
e3821da077 (apps/chat) New and improved Chat app 2021-04-30 19:18:09 +03:00
sercan
ee48e11548 Increased the version number
(dependencies) Updated Angular, Angular Material and various other packages
(changelog) Updated the changelog
2021-04-30 19:07:53 +03:00
sercan
215546cc31 (apps/academy) Removed a misplaced import 2021-04-29 19:57:58 +03:00
sercan
072dbce6d4 (fuse/fullscreen) Added Fullscreen toggle component 2021-04-28 10:55:32 +03:00
sercan
5ffe0d0efa (pages/pricing) Improved the spacing of the CTA section on all pricing pages 2021-04-26 23:42:16 +03:00
sercan
e90fb9e618 (apps/academy) Added missing trackBy functions to '*ngFor's 2021-04-26 16:41:21 +03:00
sercan
88e98d002d (apps/mailbox) Removed unused methods 2021-04-26 16:08:19 +03:00
sercan
deeef323f9 (apps/academy) Better error handling on courses that are not exist 2021-04-26 15:59:44 +03:00
sercan
8dcf21cb1a Merge remote-tracking branch 'origin/demo' into starter 2021-04-26 10:23:15 +03:00
sercan
284e282761 Remove quotes from .nvmrc version info 2021-04-26 10:23:08 +03:00
sercan
d917f03883 Merge remote-tracking branch 'origin/demo' into starter 2021-04-26 10:20:06 +03:00
sercan
52e234325f Added .nvmrc with required Node version 2021-04-26 10:19:52 +03:00
sercan
0f2ddbda83 Merge remote-tracking branch 'origin/demo' into starter
# Conflicts:
#	src/app/mock-api/common/navigation/data.ts
#	src/app/modules/admin/docs/changelog/changelog.ts
2021-04-26 09:56:44 +03:00
sercan
fa0d74504b Merge remote-tracking branch 'origin/demo' into starter 2021-04-26 09:56:29 +03:00
sercan
42e0864538 (dependencies) Updated Angular, Angular Material and various other packages
(changelog) Added changelog and updated the version number
2021-04-26 09:55:41 +03:00
sercan
ad2b19a07a Merge remote-tracking branch 'origin/demo' into starter
# Conflicts:
#	src/app/app.routing.ts
#	src/app/mock-api/common/navigation/data.ts
#	src/app/modules/admin/apps/contacts/details/details.component.html
#	src/app/modules/admin/apps/file-manager/list/list.component.html
#	src/app/modules/admin/apps/file-manager/list/list.component.ts
#	src/app/modules/landing/home/home.component.html
2021-04-26 09:31:42 +03:00
sercan
6b6442b37f (apps/academy) Added page info in between the desktop navigation buttons 2021-04-26 09:29:59 +03:00
sercan
bb0efade72 (apps/academy) New version of the Academy app 2021-04-25 23:23:23 +03:00
sercan
63edc8d1f2 (layouts/classy) Disabled footer on Classy layout for more 'classy' look 2021-04-25 23:15:38 +03:00
sercan
9dde624bb5 (icons) Added Material Solid icons 2021-04-25 23:14:13 +03:00
sercan
a5a27d0a51 (icons) Added Material Solid icons 2021-04-25 13:23:26 +03:00
sercan
85ea34a6ce (Contacts) Fixed: Clicking on the tag checkbox on tag panel breaks the toggling 2021-04-25 12:42:41 +03:00
sercan
9b059f8d0d (icons/heroicons) Updated heroicons to v1.0.1 2021-04-22 22:05:59 +03:00
sercan
df48ad1c56 (apps/file-manager) Removed unnecessary imports 2021-04-22 11:24:16 +03:00
sercan
6a113a5317 (icons/heroicons-outline) Fixed: academic-cap icon path fills breaks the icon 2021-04-22 11:23:21 +03:00
sercan
4b268e5d1b (apps/file-manager) Better grid 2021-04-20 22:44:12 +03:00
sercan
4bf11591a2 (Assets) Added avatar images back 2021-04-19 13:08:24 +03:00
sercan
f45a605b4e Preparing the starter 2021-04-15 17:43:28 +03:00
sercan
c150a8902c Starter 2021-04-15 17:23:49 +03:00
689 changed files with 16387 additions and 78179 deletions

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
12

2797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,7 @@
<!-- Button -->
<button
mat-icon-button
[matTooltip]="'Toggle Fullscreen'"
(click)="toggleFullscreen()">
<mat-icon [svgIcon]="'heroicons_outline:arrows-expand'"></mat-icon>
</button>

View 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;
}
}
}

View 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
{
}

View 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;
}

View File

@@ -0,0 +1 @@
export * from '@fuse/components/fullscreen/public-api';

View 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';

View File

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

View File

@@ -56,7 +56,7 @@ const utilities = plugin(({
}
},
{
variants: ['dark', 'responsive']
variants: ['dark', 'responsive', 'group-hover', 'hover']
}
);

View File

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

View File

@@ -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)},
]
}
];

View File

@@ -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'));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &copy; {{currentYear}}</span>
</div>
</div>-->
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
];
});
}
}

View 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>&lt;li&gt;</code> doloribus, ducimus earum, est <code>&lt;p&gt;</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>&lt;p&gt;</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>&lt;p&gt;</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>Razors Edge</td>
</tr>
</tbody>
</table>
<p>
A aliquid autem lab doloremque, ea earum eum fuga fugit illo ipsa minus natus nisi <code>&lt;span&gt;</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}`
}
];

View 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];
});
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -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'
];

View File

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

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -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
}
}
];

View File

@@ -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);
})
);
}
}

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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);
});
}
}

View File

@@ -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'
];

View File

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

View File

@@ -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;
});
}
}

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -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()
{
}
}

View File

@@ -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;
});
}
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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]
}
]
}
]
}
];

View File

@@ -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;
})
))
))
);
}
}

View File

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

View File

@@ -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">&bull;</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">&bull;</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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]
}
]
}
]*/
}
];

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -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()
{
}
}

View File

@@ -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();
}
}

View File

@@ -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;
})
))
))
);
}*/
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -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()
{
}
}

View File

@@ -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;
});
}
}
}

View File

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

View File

@@ -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);
})
);
}
}

View File

@@ -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]
}
]
}
]
}
];

View File

@@ -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);
})
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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