mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Update/misc UI fixes (#1961)
* fix login error message spacing and ensure not longer than 64 chars * spinner color to tertiary * totally responsive homepage cards * copy changes, back button for marketplace, minor styling * center setup wizard tiles; adjust external link style * remove cert note from setup success * convert launch card to go to login button * change system settings to system; update icons * refactor card widget input as full card details; more card resizing for home page * cleanup * clean up widget params * delete contructor Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
@@ -22,7 +22,7 @@ const routes: Routes = [
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
path: 'system',
|
||||
canActivate: [AuthGuard],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{{ page.title }}
|
||||
</ion-label>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/settings' && (showEOSUpdate$ | async)"
|
||||
*ngIf="page.url === '/system' && (showEOSUpdate$ | async)"
|
||||
color="success"
|
||||
size="small"
|
||||
name="rocket-outline"
|
||||
|
||||
@@ -38,9 +38,9 @@ export class MenuComponent {
|
||||
icon: 'notifications-outline',
|
||||
},
|
||||
{
|
||||
title: 'System Settings',
|
||||
url: '/settings',
|
||||
icon: 'settings-outline',
|
||||
title: 'System',
|
||||
url: '/system',
|
||||
icon: 'construct-outline',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ const ICONS = [
|
||||
'newspaper-outline',
|
||||
'notifications-outline',
|
||||
'open-outline',
|
||||
'options-outline',
|
||||
'pencil',
|
||||
'phone-portrait-outline',
|
||||
'play-circle-outline',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
<ng-template #internal>
|
||||
<a [routerLink]="link">
|
||||
<a [routerLink]="link" [queryParams]="qp">
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
})
|
||||
export class AnyLinkComponent implements OnInit {
|
||||
@Input() link!: string
|
||||
externalLink: boolean = false
|
||||
@Input() qp?: Record<string, string>
|
||||
externalLink = false
|
||||
|
||||
ngOnInit() {
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<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>
|
||||
<div
|
||||
class="outer-wrapper"
|
||||
#outerWrapper
|
||||
[ngStyle]="{ height: outerHeight, width: outerWidth }"
|
||||
>
|
||||
<div
|
||||
class="inner-wrapper"
|
||||
#innerWrapper
|
||||
[ngStyle]="{ transform: innerTransform }"
|
||||
>
|
||||
<ion-card>
|
||||
<any-link [link]="cardDetails.link" [qp]="cardDetails.qp">
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{ cardDetails.title }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-icon
|
||||
[name]="cardDetails.icon"
|
||||
[style.color]="cardDetails.color"
|
||||
></ion-icon>
|
||||
</ion-card-content>
|
||||
<ion-footer>
|
||||
<p>{{ cardDetails.description }}</p>
|
||||
</ion-footer>
|
||||
</any-link>
|
||||
</ion-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@ ion-card {
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
margin: auto;
|
||||
max-width: 22rem;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
transition: all 350ms ease;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -18,7 +19,8 @@ ion-card {
|
||||
font-family: 'Open Sans';
|
||||
padding: 0.6rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.3rem;
|
||||
font-size: calc(12px + 0.5vw);
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
@@ -29,7 +31,7 @@ ion-card {
|
||||
align-items: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: 8rem;
|
||||
font-size: calc(90px + 0.5vw);
|
||||
--ionicon-stroke-width: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +39,13 @@ ion-card {
|
||||
ion-footer {
|
||||
padding: 1rem;
|
||||
font-family: 'Open Sans';
|
||||
font-size: 1.2rem;
|
||||
font-size: clamp(1rem, calc(12px + 0.5vw), 1.3rem);
|
||||
height: 9rem;
|
||||
width: clamp(13rem, 80%, 18rem);
|
||||
margin: 0 auto;
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-md::before {
|
||||
@@ -45,6 +53,17 @@ ion-card {
|
||||
}
|
||||
}
|
||||
|
||||
.p1 {
|
||||
padding: 1.2rem;
|
||||
@media (max-width: 900px) {
|
||||
ion-card-title, ion-footer {
|
||||
height: auto !important;
|
||||
}
|
||||
ion-footer {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
ion-footer {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-card',
|
||||
@@ -7,11 +14,53 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WidgetCardComponent {
|
||||
@Input() title: string = ''
|
||||
@Input() icon: string = ''
|
||||
@Input() color: string = ''
|
||||
@Input() description: string = ''
|
||||
@Input() link: string = ''
|
||||
@Input() cardDetails!: Card
|
||||
@Input() containerDimensions!: Dimension
|
||||
@ViewChild('outerWrapper') outerWrapper: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@ViewChild('innerWrapper') innerWrapper: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.resize()
|
||||
}
|
||||
maxHeight = 0
|
||||
maxWidth = 0
|
||||
innerTransform = ''
|
||||
outerWidth: any
|
||||
outerHeight: any
|
||||
|
||||
constructor() {}
|
||||
ngAfterViewInit() {
|
||||
this.maxHeight = (<HTMLElement> (
|
||||
this.innerWrapper.nativeElement
|
||||
)).getBoundingClientRect().height
|
||||
this.maxWidth = (<HTMLElement> (
|
||||
this.innerWrapper.nativeElement
|
||||
)).getBoundingClientRect().width
|
||||
this.resize()
|
||||
}
|
||||
|
||||
resize() {
|
||||
const height = this.containerDimensions.height
|
||||
const width = this.containerDimensions.width
|
||||
const isMax = width >= this.maxWidth && height >= this.maxHeight
|
||||
const scale = Math.min(width / this.maxWidth, height / this.maxHeight)
|
||||
this.innerTransform = isMax ? '' : 'scale(' + scale + ')'
|
||||
this.outerWidth = isMax ? '' : this.maxWidth * scale
|
||||
this.outerHeight = isMax ? '' : this.maxHeight * scale
|
||||
}
|
||||
}
|
||||
|
||||
export interface Dimension {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
title: string
|
||||
icon: string
|
||||
color: string
|
||||
description: string
|
||||
link: string
|
||||
qp?: Record<string, string>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<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>
|
||||
<div #gridContent>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-justify-content-center ion-align-items-center">
|
||||
<ion-col
|
||||
*ngFor="let card of cards"
|
||||
sizeLg="4"
|
||||
sizeSm="6"
|
||||
sizeXs="12"
|
||||
class="ion-align-self-center"
|
||||
>
|
||||
<widget-card
|
||||
[cardDetails]="card"
|
||||
[containerDimensions]="containerDimensions"
|
||||
></widget-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
ion-row {
|
||||
grid-row-gap: 1rem;
|
||||
ion-col {
|
||||
max-width: 22rem !important;
|
||||
--ion-grid-column-padding: 1rem;
|
||||
}
|
||||
|
||||
ion-col {
|
||||
padding: 0 0.5rem 0.5rem 1rem;
|
||||
@media (min-width: 1800px) {
|
||||
div {
|
||||
padding: 0 20%;
|
||||
}
|
||||
ion-col {
|
||||
max-width: 24rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { Card, Dimension } from '../widget-card/widget-card.component'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-list',
|
||||
@@ -7,7 +14,27 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WidgetListComponent {
|
||||
constructor() {}
|
||||
@ViewChild('gridContent') gridContent: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.setContainerDimensions()
|
||||
}
|
||||
|
||||
containerDimensions: Dimension = {} as Dimension
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.setContainerDimensions()
|
||||
}
|
||||
|
||||
setContainerDimensions() {
|
||||
this.containerDimensions.height = (<HTMLElement> (
|
||||
this.gridContent.nativeElement
|
||||
)).getBoundingClientRect().height
|
||||
this.containerDimensions.width = (<HTMLElement> (
|
||||
this.gridContent.nativeElement
|
||||
)).getBoundingClientRect().width
|
||||
}
|
||||
|
||||
cards: Card[] = [
|
||||
{
|
||||
@@ -15,7 +42,8 @@ export class WidgetListComponent {
|
||||
icon: 'storefront-outline',
|
||||
color: 'var(--alt-blue)',
|
||||
description: 'Shop for your favorite open source services',
|
||||
link: '/marketplace/browse',
|
||||
link: '/marketplace',
|
||||
qp: { back: 'true' },
|
||||
},
|
||||
{
|
||||
title: 'LAN Setup',
|
||||
@@ -23,21 +51,21 @@ export class WidgetListComponent {
|
||||
color: 'var(--alt-orange)',
|
||||
description:
|
||||
'Install your Embassy certificate for a secure local connection',
|
||||
link: '/settings/lan',
|
||||
link: '/system/lan',
|
||||
},
|
||||
{
|
||||
title: 'Create Backup',
|
||||
icon: 'duplicate-outline',
|
||||
color: 'var(--alt-purple)',
|
||||
description: 'Back up your Embassy and service data',
|
||||
link: '/settings/backup',
|
||||
link: '/system/backup',
|
||||
},
|
||||
{
|
||||
title: 'Embassy Info',
|
||||
icon: 'information-circle-outline',
|
||||
color: 'var(--alt-green)',
|
||||
description: 'View basic information about your Embassy',
|
||||
link: '/settings/specs',
|
||||
link: '/system/specs',
|
||||
},
|
||||
{
|
||||
title: 'User Manual',
|
||||
@@ -55,11 +83,3 @@ export class WidgetListComponent {
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
interface Card {
|
||||
title: string
|
||||
icon: string
|
||||
color: string
|
||||
description: string
|
||||
link: string
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }),
|
||||
title: 'Config',
|
||||
description: `Customize ${pkgTitle}`,
|
||||
icon: 'construct-outline',
|
||||
icon: 'options-outline',
|
||||
},
|
||||
// properties
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<div style="padding: 36px 0">
|
||||
<div class="padding-top">
|
||||
<widget-list></widget-list>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.padding-top {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
.padding-top {
|
||||
padding-top: 10rem;
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,6 @@
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<p style="text-align: left; padding-top: 4px">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ion-item-group>
|
||||
<ion-button
|
||||
class="login-button"
|
||||
@@ -51,6 +48,9 @@
|
||||
>
|
||||
<span style="font-size: larger; font-weight: bold">Login</span>
|
||||
</ion-button>
|
||||
<p style="text-align: left; padding-top: 4px">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</form>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
@@ -28,6 +28,7 @@ ion-card {
|
||||
background: var(--ion-color-step-200);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
|
||||
@@ -40,6 +40,10 @@ export class LoginPage {
|
||||
|
||||
try {
|
||||
document.cookie = ''
|
||||
if (this.password.length > 64) {
|
||||
this.error = 'Password must be less than 65 characters'
|
||||
return
|
||||
}
|
||||
await this.api.login({
|
||||
password: this.password,
|
||||
metadata: { platforms: getPlatforms() },
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start" *ngIf="back">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Marketplace</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
@@ -15,6 +16,8 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListPage {
|
||||
readonly back = !!this.route.snapshot.queryParamMap.get('back')
|
||||
|
||||
readonly store$ = this.marketplaceService.getSelectedStore$().pipe(
|
||||
filter(Boolean),
|
||||
map(({ info, packages }) => {
|
||||
@@ -73,6 +76,7 @@ export class MarketplaceListPage {
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly config: ConfigService,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
category = 'featured'
|
||||
|
||||
@@ -4,11 +4,7 @@ import { Routes, RouterModule } from '@angular/router'
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'browse',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'browse',
|
||||
loadChildren: () =>
|
||||
import('./marketplace-list/marketplace-list.module').then(
|
||||
m => m.MarketplaceListPageModule,
|
||||
|
||||
@@ -26,7 +26,7 @@ export class NotificationsPage {
|
||||
notifications: ServerNotifications = []
|
||||
beforeCursor?: number
|
||||
needInfinite = false
|
||||
fromToast = false
|
||||
fromToast = !!this.route.snapshot.queryParamMap.get('toast')
|
||||
readonly perPage = 40
|
||||
readonly packageData$ = this.patch.watch$('package-data')
|
||||
|
||||
@@ -41,7 +41,6 @@ export class NotificationsPage {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.fromToast = !!this.route.snapshot.queryParamMap.get('toast')
|
||||
this.notifications = await this.getNotifications()
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title> System Settings </ion-title>
|
||||
<ion-title> System </ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
|
||||
Reference in New Issue
Block a user