Feature/homepage (#1956)

* add  new card widget for empty services page

* add homepage

* fix icons

* copy and arrangement changes

* minor changes

* edit login page

* rcreate widget list component

* cchange change detection strategy

* show header in home, welcome in list (#1957)

* show hear in home but not in list

* adjust padding

Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Lucy C
2022-11-20 22:18:39 -07:00
committed by Aiden McClelland
parent 4042b8f026
commit 35cb81518c
33 changed files with 460 additions and 116 deletions

View File

@@ -16,7 +16,13 @@ const routes: Routes = [
import('./pages/login/login.module').then(m => m.LoginPageModule),
},
{
path: 'embassy',
path: 'home',
canActivate: [AuthGuard],
loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
},
{
path: 'settings',
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: () =>

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy } from '@angular/core'
import { merge, take } from 'rxjs'
import { merge } from 'rxjs'
import { AuthService } from './services/auth.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { PatchDataService } from './services/patch-data.service'
@@ -28,9 +28,9 @@ export class AppComponent implements OnDestroy {
) {}
ngOnInit() {
this.serverNameService.name$
.pipe(take(1))
.subscribe(({ current }) => this.titleService.setTitle(current))
this.serverNameService.name$.subscribe(({ current }) =>
this.titleService.setTitle(current),
)
}
splitPaneVisible({ detail }: any) {

View File

@@ -1,7 +1,6 @@
<a class="logo ion-padding" routerLink="/services">
<a class="logo ion-padding" routerLink="/home">
<img alt="Start9" src="assets/img/logo.png" />
</a>
<div class="divider"></div>
<ion-item-group class="menu">
<ion-menu-toggle *ngFor="let page of pages" auto-hide="false">
<ion-item
@@ -23,7 +22,7 @@
{{ page.title }}
</ion-label>
<ion-icon
*ngIf="page.url === '/embassy' && (showEOSUpdate$ | async)"
*ngIf="page.url === '/settings' && (showEOSUpdate$ | async)"
color="success"
size="small"
name="rocket-outline"

View File

@@ -23,25 +23,25 @@ export class MenuComponent {
icon: 'grid-outline',
},
{
title: 'Embassy',
url: '/embassy',
icon: 'cube-outline',
title: 'Marketplace',
url: '/marketplace',
icon: 'storefront-outline',
},
{
title: 'Updates',
url: '/updates',
icon: 'globe-outline',
},
{
title: 'Marketplace',
url: '/marketplace',
icon: 'storefront-outline',
},
{
title: 'Notifications',
url: '/notifications',
icon: 'notifications-outline',
},
{
title: 'System Settings',
url: '/settings',
icon: 'settings-outline',
},
]
readonly notificationCount$ = this.patch.watch$(

View File

@@ -28,9 +28,9 @@ const ICONS = [
'color-wand-outline',
'construct-outline',
'copy-outline',
'cube-outline',
'desktop-outline',
'download-outline',
'duplicate-outline',
'earth-outline',
'ellipsis-horizontal',
'eye-off-outline',
@@ -46,7 +46,6 @@ const ICONS = [
'information-circle-outline',
'key-outline',
'list-outline',
'lock-closed-outline',
'log-out-outline',
'logo-bitcoin',
'mail-outline',
@@ -72,6 +71,7 @@ const ICONS = [
'remove-outline',
'rocket-outline',
'save-outline',
'settings-outline',
'shield-checkmark-outline',
'stop-outline',
'storefront-outline',

View File

@@ -0,0 +1,17 @@
<ng-template #content>
<ng-content></ng-content>
</ng-template>
<a
*ngIf="externalLink; else internal"
[href]="link"
target="_blank"
rel="noreferrer"
>
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
<ng-template #internal>
<a [routerLink]="link">
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
</ng-template>

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { RouterModule } from '@angular/router'
import { AnyLinkComponent } from './any-link.component'
@NgModule({
declarations: [AnyLinkComponent],
imports: [CommonModule, RouterModule.forChild([])],
exports: [AnyLinkComponent],
})
export class AnyLinkModule {}

View File

@@ -0,0 +1,4 @@
a {
text-decoration: none;
color: unset;
}

View File

@@ -0,0 +1,26 @@
import {
Component,
Input,
ChangeDetectionStrategy,
OnInit,
} from '@angular/core'
@Component({
selector: 'any-link',
templateUrl: './any-link.component.html',
styleUrls: ['./any-link.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnyLinkComponent implements OnInit {
@Input() link!: string
externalLink: boolean = false
ngOnInit() {
try {
const _ = new URL(this.link)
this.externalLink = true
} catch {
this.externalLink = false
}
}
}

View File

@@ -0,0 +1,15 @@
<ion-card>
<any-link link="{{ link }}">
<div class="p1">
<ion-card-header>
<ion-card-title>{{ title }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-icon name="{{ icon }}" style="color: {{ color }}"></ion-icon>
</ion-card-content>
<ion-footer>
{{ description }}
</ion-footer>
</div>
</any-link>
</ion-card>

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { WidgetCardComponent } from './widget-card.component'
import { AnyLinkModule } from 'src/app/components/any-link/any-link.component.module'
@NgModule({
declarations: [WidgetCardComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
AnyLinkModule,
],
exports: [WidgetCardComponent],
})
export class WidgetCardComponentModule {}

View File

@@ -0,0 +1,50 @@
ion-card {
background: rgba(70, 70, 70, 0.31);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 44px;
margin: auto;
max-width: 22rem;
text-align: center;
transition: all 350ms ease;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transition-property: transform;
transform: scale(1.05);
transition-delay: 40ms;
}
ion-card-title {
font-family: 'Open Sans';
padding: 0.6rem;
font-weight: 600;
font-size: 1.3rem;
}
ion-card-content {
min-height: 9rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
ion-icon {
font-size: 8rem;
--ionicon-stroke-width: 1rem;
}
}
ion-footer {
padding: 1rem;
font-family: 'Open Sans';
font-size: 1.2rem;
}
.footer-md::before {
background-image: none;
}
}
.p1 {
padding: 1.2rem;
}

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
@Component({
selector: 'widget-card',
templateUrl: './widget-card.component.html',
styleUrls: ['./widget-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WidgetCardComponent {
@Input() title: string = ''
@Input() icon: string = ''
@Input() color: string = ''
@Input() description: string = ''
@Input() link: string = ''
constructor() {}
}

View File

@@ -0,0 +1,17 @@
<ion-grid>
<ion-row class="ion-justify-content-center ion-align-items-center">
<ion-col
*ngFor="let card of cards"
size="auto"
class="ion-align-self-center"
>
<widget-card
title="{{ card.title }}"
icon="{{ card.icon }}"
color="{{ card.color }}"
description="{{ card.description }}"
link="{{ card.link }}"
></widget-card>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { WidgetListComponent } from './widget-list.component'
import { AnyLinkModule } from 'src/app/components/any-link/any-link.component.module'
import { WidgetCardComponentModule } from '../widget-card/widget-card.component.module'
@NgModule({
declarations: [WidgetListComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
AnyLinkModule,
WidgetCardComponentModule,
],
exports: [WidgetListComponent],
})
export class WidgetListComponentModule {}

View File

@@ -0,0 +1,7 @@
ion-row {
grid-row-gap: 1rem;
}
ion-col {
padding: 0 0.5rem 0.5rem 1rem;
}

View File

@@ -0,0 +1,65 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
@Component({
selector: 'widget-list',
templateUrl: './widget-list.component.html',
styleUrls: ['./widget-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WidgetListComponent {
constructor() {}
cards: Card[] = [
{
title: 'Visit the Marketplace',
icon: 'storefront-outline',
color: 'var(--alt-blue)',
description: 'Shop for your favorite open source services',
link: '/marketplace/browse',
},
{
title: 'LAN Setup',
icon: 'home-outline',
color: 'var(--alt-orange)',
description:
'Install your Embassy certificate for a secure local connection',
link: '/settings/lan',
},
{
title: 'Create Backup',
icon: 'duplicate-outline',
color: 'var(--alt-purple)',
description: 'Back up your Embassy and service data',
link: '/settings/backup',
},
{
title: 'Embassy Info',
icon: 'information-circle-outline',
color: 'var(--alt-green)',
description: 'View basic information about your Embassy',
link: '/settings/specs',
},
{
title: 'User Manual',
icon: 'map-outline',
color: 'var(--alt-yellow)',
description: 'Discover what your Embassy can do',
link: 'https://docs.start9.com/latest/user-manual/index',
},
{
title: 'Contact Support',
icon: 'chatbubbles-outline',
color: 'var(--alt-red)',
description: 'Get help from the Start9 team and community',
link: 'https://docs.start9.com/latest/support/contact',
},
]
}
interface Card {
title: string
icon: string
color: string
description: string
link: string
}

View File

@@ -1,16 +0,0 @@
<div class="welcome">
<h2>
Welcome to
<ion-text color="danger" class="embassy">Embassy</ion-text>
</h2>
<p class="ion-text-wrap">Get started by installing your first service.</p>
</div>
<ion-button
color="dark"
routerLink="/marketplace"
routerDirection="root"
class="marketplace"
>
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>

View File

@@ -1,18 +0,0 @@
:host {
display: block;
}
.welcome {
display: flex;
flex-direction: column;
justify-content: center;
height: 40vh;
}
.embassy {
font-family: "Montserrat", sans-serif;
}
.marketplace {
width: 50%;
}

View File

@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'app-list-empty',
templateUrl: 'app-list-empty.component.html',
styleUrls: ['app-list-empty.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppListEmptyComponent {}

View File

@@ -12,9 +12,9 @@ import { StatusComponentModule } from 'src/app/components/status/status.componen
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
import { AppListIconComponent } from './app-list-icon/app-list-icon.component'
import { AppListEmptyComponent } from './app-list-empty/app-list-empty.component'
import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component'
import { PackageInfoPipe } from './package-info.pipe'
import { WidgetListComponentModule } from 'src/app/components/widget-list/widget-list.component.module'
const routes: Routes = [
{
@@ -34,11 +34,11 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
WidgetListComponentModule,
],
declarations: [
AppListPage,
AppListIconComponent,
AppListEmptyComponent,
AppListPkgComponent,
PackageInfoPipe,
],

View File

@@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title>Services</ion-title>
<ion-title>Installed Services</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
@@ -10,16 +10,14 @@
<ion-content class="ion-padding">
<!-- loaded -->
<ng-container *ngIf="pkgs$ | async as pkgs; else loading">
<app-list-empty
*ngIf="!pkgs.length; else list"
class="ion-text-center ion-padding"
></app-list-empty>
<ng-container *ngIf="!pkgs.length; else list">
<div class="welcome-header">
<h1>Welcome to embassyOS</h1>
</div>
<widget-list></widget-list>
</ng-container>
<ng-template #list>
<ion-item-divider class="ion-padding-bottom"
>Installed Services</ion-item-divider
>
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of pkgs" sizeSm="12" sizeLg="6">

View File

@@ -0,0 +1,9 @@
.welcome-header {
padding-bottom: 1rem;
text-align: center;
h1 {
font-weight: bold;
font-size: 2rem;
}
}

View File

@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { HomePage } from './home.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { WidgetListComponentModule } from 'src/app/components/widget-list/widget-list.component.module'
const routes: Routes = [
{
path: '',
component: HomePage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
WidgetListComponentModule,
],
declarations: [HomePage],
})
export class HomePageModule {}

View File

@@ -0,0 +1,13 @@
<ion-header>
<ion-toolbar>
<ion-title>Home</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div style="padding: 36px 0">
<widget-list></widget-list>
</div>
</ion-content>

View File

@@ -0,0 +1,8 @@
import { Component } from '@angular/core'
@Component({
selector: 'home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {}

View File

@@ -1,32 +1,55 @@
<ion-content>
<ion-grid style="height: 100%; max-width: 540px;">
<ion-row class="ion-align-items-center" style="height: 90%;">
<ion-grid style="height: 100%; max-width: 540px">
<ion-row class="ion-align-items-center" style="height: 90%">
<ion-col class="ion-text-center">
<div style="padding-bottom: 16px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
<div style="padding-bottom: 16px">
<img src="assets/img/logo.png" style="max-width: 240px" />
</div>
<ion-card color="dark">
<ion-card-header class="ion-text-center" style="padding-bottom: 8px;">
<ion-card-title>Log in to Embassy</ion-card-title>
<ion-card>
<ion-card-header class="ion-text-center" style="padding-bottom: 8px">
<ion-card-title>Embassy Login</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin">
<form (submit)="submit()" style="margin-bottom: 12px;">
<ion-card-content class="ion-margin ion-text-center">
<form
class="inline"
(submit)="submit()"
style="margin-bottom: 12px"
>
<ion-item-group>
<p class="input-label">Password</p>
<ion-item color="dark">
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
<ion-input [type]="unmasked ? 'text' : 'password'" name="password" [(ngModel)]="password" (ionChange)="error = ''"></ion-input>
<ion-icon
slot="start"
name="key-outline"
style="margin-right: 16px"
></ion-icon>
<ion-input
[type]="unmasked ? 'text' : 'password'"
name="password"
[(ngModel)]="password"
(ionChange)="error = ''"
placeholder="Password"
></ion-input>
<ion-button fill="clear" color="light" (click)="toggleMask()">
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
<ion-icon
slot="icon-only"
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
size="small"
></ion-icon>
</ion-button>
</ion-item>
<p style="text-align: left; padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>
<p style="text-align: left; padding-top: 4px">
<ion-text color="danger">{{ error }}</ion-text>
</p>
</ion-item-group>
<ion-button class="login-button" type="submit" expand="block">
<span style="font-size: larger; font-weight: bold;">Log In</span>
<ion-button
class="login-button"
type="submit"
expand="block"
color="tertiary"
>
<span style="font-size: larger; font-weight: bold">Login</span>
</ion-button>
</form>
</ion-card-content>
@@ -34,4 +57,4 @@
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
</ion-content>

View File

@@ -1,14 +1,33 @@
ion-card-title {
margin: 24px 0;
font-family: 'Montserrat';
font-weight: 500;
font-size: x-large;
--color: var(--ion-color-light);
font-variant: all-small-caps;
--color: var(--ion-color-dark);
}
ion-button {
--border-radius: 0 4px 4px 0;
}
ion-item {
--border-style: solid;
--border-width: 1px;
--border-color: var(--ion-color-light);
--border-radius: 4px 0 0 4px;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
ion-button {
--border-radius: 4px;
}
}
ion-card {
background: var(--ion-color-step-200);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 44px;
}
.input-label {
@@ -16,12 +35,22 @@ ion-item {
padding-bottom: 2px;
font-size: small;
font-weight: bold;
color: var(--ion-color-dark);
}
.login-button {
margin-inline-start: 0;
margin-inline-end: 0;
margin-top: 24px;
height: 48px;
--background: linear-gradient(45deg, var(--ion-color-light) 16%, var(--ion-color-dark) 150%);
height: 49px;
}
.inline {
* {
display: inline-block;
vertical-align: middle;
}
}
.item-interactive {
--highlight-background: var(--ion-color-tertiary) !important;
}

View File

@@ -1,13 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title *ngIf="name$ | async as name; else loadingTitle">
{{ name.current }}
</ion-title>
<ng-template #loadingTitle>
<ion-title>
<ion-title>Loading<span class="loading-dots"></span></ion-title>
</ion-title>
</ng-template>
<ion-title> System Settings </ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>

View File

@@ -33,7 +33,6 @@ export class ServerShowPage {
powerClicks = 0
readonly server$ = this.patch.watch$('server-info')
readonly name$ = this.serverNameService.name$
readonly showUpdate$ = this.eosService.showUpdate$
readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$
@@ -54,11 +53,11 @@ export class ServerShowPage {
) {}
async presentModalName(): Promise<void> {
const name = await firstValueFrom(this.name$)
const name = await firstValueFrom(this.serverNameService.name$)
const options: GenericInputOptions = {
title: 'Edit Device Name',
message: 'This is for your reference only.',
title: 'Set Device Name',
message: 'This will be displayed in your browser tab',
label: 'Device Name',
useMask: false,
placeholder: name.default,
@@ -349,8 +348,8 @@ export class ServerShowPage {
Backups: [
{
title: 'Create Backup',
description: 'Back up your Embassy and all its services',
icon: 'save-outline',
description: 'Back up your Embassy and service data',
icon: 'duplicate-outline',
action: () =>
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
detail: true,
@@ -358,7 +357,7 @@ export class ServerShowPage {
},
{
title: 'Restore From Backup',
description: 'Restore one or more services from a prior backup',
description: 'Restore one or more services from backup',
icon: 'color-wand-outline',
action: () =>
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
@@ -366,7 +365,7 @@ export class ServerShowPage {
disabled$: this.eosService.updatingOrBackingUp$,
},
],
Settings: [
Manage: [
{
title: 'Software Update',
description: 'Get the latest version of embassyOS',
@@ -379,8 +378,8 @@ export class ServerShowPage {
disabled$: this.eosService.updatingOrBackingUp$,
},
{
title: 'Device Name',
description: 'Edit the local display name of your Embassy',
title: 'Set Device Name',
description: 'Give your device a name for easy identification',
icon: 'pricetag-outline',
action: () => this.presentModalName(),
detail: false,
@@ -388,7 +387,8 @@ export class ServerShowPage {
},
{
title: 'LAN',
description: 'Access your Embassy on the Local Area Network',
description:
'Install your Embassy certificate for a secure local connection',
icon: 'home-outline',
action: () =>
this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
@@ -397,7 +397,8 @@ export class ServerShowPage {
},
{
title: 'SSH',
description: 'Access your Embassy from the command line',
description:
'Manage your SSH keys to access your Embassy from the command line',
icon: 'terminal-outline',
action: () =>
this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
@@ -480,7 +481,7 @@ export class ServerShowPage {
Support: [
{
title: 'User Manual',
description: 'View the Embassy user manual and FAQ',
description: 'Discover what your Embassy can do',
icon: 'map-outline',
action: () =>
window.open(

View File

@@ -26,6 +26,13 @@
src: url('/assets/fonts/Open_Sans/OpenSans-Regular.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: url('/assets/fonts/Open_Sans/OpenSans-SemiBold.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;