Feature/marketplace redesign (#2395)

* wip

* update marketplace categories styling

* update logo icons

* add sort pipe

* update search component styling

* clean up categories component

* cleanup and remove unnecessary sort pipe

* query packages in selected category

* fix search styling

* add reg icon and font, adjust category styles

* fix build from rebasing integration/refactors

* adjust marketplace types for icon with store data, plus formatting

* formatting

* update categories and search

* hover styling for categories

* category styling

* refactor for category as a behavior subject

* more category styling

* base functionality with new marketplace components

* styling cleanup

* misc style fixes and fix category selection from package page

* fixes from review feedback

* add and style additional details

* implement release notes modal

* fix menu when on service show page mobile to display change marketplace

* style and responsiveness fixes

* rename header to sidebar

* input icon config to sidebar

* add mime type pipe and type fn

* review feedback fixes

* skeleton text, more abstraction

* reorder categories, clean up a little

* audit sidebar, categories, store-icon, marketplace-sidebar, search

* finish code cleanup and fix few bugs

* misc fixes and cleanup

* fix broken styles and markdown

* bump shared marketplace version

* more cleanup

* sync package lock

* rename sidebar component to menu

* wip preview sidebar

* sync package lock

* breakout package show elements into components

* link to brochure in preview; custom taiga button styles

* move marketplace preview component into ui; open preview when viewing service in marketplace

* sync changes post file struture rename

* further cleanup

* create service for sidebar toggle and cleanup marketplace components

* bump shared marketplace version

* bump shared for new images needed for brochure marketplace

* cleanup

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Lucy
2023-12-08 15:12:38 -05:00
committed by GitHub
parent ad13b5eb4e
commit 0469aab433
115 changed files with 6434 additions and 5051 deletions

View File

@@ -0,0 +1,145 @@
<header
class="z-50 overflow-hidden w-full fixed sm:w-[34vw] md:w-[28vw] lg:w-[22vw] 2xl:w-[280px] sm:bg-zinc-700/90 sm:backdrop-blur-2xl sm:min-h-screen overflow-y-auto flex flex-col sm:py-6 sm:after:block sm:after:absolute sm:after:top-0 sm:after:bottom-0 sm:after:right-0 sm:after:w-0.5 after:h-[calc(100vh - 20px)] sm:after:bg-gradient-to-b from-zinc-700 to-zinc-400"
>
<ng-container *tuiLet="store$ | async as store">
<div class="hidden sm:flex flex-col mx-6 pb-3 items-center">
<div
class="mb-3 rounded-full"
[class.tui-skeleton]="!store"
[class.tui-skeleton_rounded]="!store"
>
<store-icon
size="64px"
[url]="store?.url || ''"
[marketplace]="iconConfig"
></store-icon>
</div>
<h1
class="font-semibold text-2xl text-zinc-100 text-center mb-3"
[class.tui-skeleton]="!store"
>
{{ store?.info?.name || 'Loading store' }}
</h1>
<!-- change registry modal -->
<ng-content select="[slot=desktop]"></ng-content>
</div>
<!-- mobile nav -->
<div class="sm:hidden bg-zinc-700/90 backdrop-blur-3xl">
<div class="flex justify-between items-center py-4 px-4 w-[100vw]">
<!-- mobile search -->
<marketplace-search
class="max-w-fit"
[(query)]="query"
(queryChange)="onQueryChange($event)"
></marketplace-search>
<button
tuiButton
type="button"
appearance="flat"
[pseudoActive]="false"
(click)="toggleMenu(true)"
(tuiActiveZoneChange)="toggleMenu($event)"
[style.--tui-padding]="'1rem'"
>
<store-icon
size="48px"
[url]="store?.url || ''"
[marketplace]="iconConfig"
class="rounded-full"
[class.tui-skeleton]="!store"
[class.tui-skeleton_rounded]="!store"
></store-icon>
<nav
*tuiSidebar="open; direction: 'right'; autoWidth: true"
class="bg-zinc-700/90 h-screen w-[70vw]"
>
<div class="flex flex-col divide-y divide-zinc-500 h-full">
<div class="flex items-center p-4">
<h1
class="font-semibold text-xl text-zinc-200 flex-grow"
[class.tui-skeleton]="!store"
>
{{ store?.info?.name }}
</h1>
<button
[style.--tui-padding]="0"
class="place-self-end"
tuiButton
type="button"
appearance="icon"
icon="tuiIconClose"
(tuiActiveZoneChange)="toggleMenu($event)"
(click)="toggleMenu(false)"
></button>
</div>
<!-- change registry modal -->
<ng-content select="[slot=mobile]"></ng-content>
<div
class="flex flex-col justify-between divide-y divide-zinc-500 h-full"
>
<marketplace-categories
[categories]="store?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event); toggleMenu(false)"
class="grow pt-5 pl-5 pr-5"
></marketplace-categories>
<a
class="flex relative gap-2 hover:no-underline p-5"
target="_blank"
href="https://github.com/Start9Labs/service-pipeline#readme"
>
<img
alt="Launch icon"
width="24"
height="24"
class="opacity-70 invert"
src="svg/rocket-outline.svg"
/>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
>
Launch your project
</span>
</a>
</div>
</div>
</nav>
</button>
</div>
</div>
<!-- desktop nav -->
<nav class="hidden sm:flex grow flex-col py-3 px-4">
<!-- desktop search -->
<marketplace-search
class="place-self-center md:pb-8"
[query]="query"
(queryChange)="onQueryChange($event)"
></marketplace-search>
<div class="flex grow flex-col justify-between">
<marketplace-categories
[categories]="store?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event)"
></marketplace-categories>
<a
class="flex gap-2 p-2 hover:no-underline sm:hover:bg-[#222428] hover:cursor-pointer sm:w-[120%] z-50 rounded-l-lg ease-in-out delay-75 duration-300"
target="_blank"
href="https://github.com/Start9Labs/service-pipeline#readme"
>
<img
alt="Rocket Icon"
width="24"
height="24"
class="opacity-70 invert"
src="svg/rocket-outline.svg"
/>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
>
Launch your project
</span>
</a>
</div>
</nav>
</ng-container>
</header>

View File

@@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared'
import { MenuComponent } from './menu.component'
import { TuiButtonModule, TuiLoaderModule } from '@taiga-ui/core'
import { TuiActiveZoneModule, TuiLetModule } from '@taiga-ui/cdk'
import { TuiSidebarModule } from '@taiga-ui/addon-mobile'
import { SearchModule } from '../../pages/list/search/search.module'
import { CategoriesModule } from '../../pages/list/categories/categories.module'
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
@NgModule({
imports: [
CommonModule,
SharedPipesModule,
SearchModule,
CategoriesModule,
TuiActiveZoneModule,
TuiSidebarModule,
TuiLoaderModule,
TuiButtonModule,
CategoriesModule,
StoreIconComponentModule,
TuiLetModule,
],
declarations: [MenuComponent],
exports: [MenuComponent],
})
export class MenuModule {}

View File

@@ -0,0 +1,89 @@
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
OnDestroy,
} from '@angular/core'
import { combineLatest, map, Subject, takeUntil } from 'rxjs'
import { StoreIdentity } from '../../types'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
import { AbstractCategoryService } from '../../services/category.service'
import { Router } from '@angular/router'
import { MarketplaceConfig } from '@start9labs/shared'
@Component({
selector: 'menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MenuComponent implements OnDestroy {
@Input({ required: true })
iconConfig!: MarketplaceConfig
constructor(private readonly router: Router) {}
private destroy$ = new Subject<void>()
private readonly marketplaceService = inject(AbstractMarketplaceService)
private readonly categoryService = inject(AbstractCategoryService)
readonly store$ = this.marketplaceService.getSelectedStoreWithCategories$()
readonly alt$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getSelectedHost$(),
]).pipe(
map(([stores, selected]) =>
stores.filter(({ url }) => url != selected.url),
),
)
private hosts?: StoreIdentity[]
category = ''
query = ''
open = false
ngOnInit() {
this.categoryService
.getQuery$()
.pipe(takeUntil(this.destroy$))
.subscribe(val => {
this.query = val
})
this.categoryService
.getCategory$()
.pipe(takeUntil(this.destroy$))
.subscribe(val => {
this.category = val
})
this.marketplaceService
.getKnownHosts$()
.pipe(takeUntil(this.destroy$))
.subscribe(hosts => {
this.hosts = hosts
})
}
onCategoryChange(category: string): void {
this.category = category
this.query = ''
this.categoryService.resetQuery()
this.categoryService.changeCategory(category)
this.router.navigate(['/marketplace'], { replaceUrl: true })
}
onQueryChange(query: string): void {
this.query = query
this.categoryService.setQuery(query)
this.router.navigate(['/marketplace'], { replaceUrl: true })
}
toggleMenu(open: boolean): void {
this.open = open
}
ngOnDestroy(): void {
this.destroy$.next()
this.destroy$.complete()
}
}

View File

@@ -1,5 +1,6 @@
<img
*ngIf="icon; else noIcon"
class="rounded-full"
[style.max-width]="size || '100%'"
[src]="icon"
alt="Service Icon"

View File

@@ -1,9 +1,28 @@
<ion-button
*ngFor="let cat of categories"
fill="clear"
class="category"
[class.category_selected]="cat === category"
<button
*ngFor="let cat of categories || ['', '', '', '', '', '']"
class="flex relative gap-2 hover:no-underline hover:cursor-pointer sm:hover:bg-[#222428] sm:hover:opacity-90 sm:w-[120%] rounded-l-lg ease-in-out delay-75 duration-300 mb-5 sm:p-2 sm:mb-3 z-50"
routerLink="/marketplace"
(click)="switchCategory(cat)"
[class.category_selected]="cat === category"
>
{{ cat }}
</ion-button>
<div
class="relative flex"
[class.tui-skeleton]="!categories"
[class.tui-skeleton_rounded]="!categories"
>
<img
alt="Category Icon"
width="24"
height="24"
class="opacity-70 invert"
src="/svg/{{ determineIcon(cat) }}.svg"
/>
</div>
<span
class="text-base text-zinc-50 text-ellipsis overflow-hidden whitespace-nowrap"
[class.tui-skeleton]="!categories"
[class.tui-skeleton_rounded]="!categories"
>
{{ (cat | titlecase) || 'loading category...' }}
</span>
</button>

View File

@@ -1,14 +1,12 @@
:host {
display: block;
.category_selected {
font-weight: bold;
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
@media (min-width: 600px) {
.category_selected {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
background-color: #222428;
opacity: 90;
}
}
}

View File

@@ -10,14 +10,11 @@ import {
selector: 'marketplace-categories',
templateUrl: 'categories.component.html',
styleUrls: ['categories.component.scss'],
host: {
class: 'hidden-scrollbar ion-text-center',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CategoriesComponent {
@Input()
categories: readonly string[] = []
categories?: string[]
@Input()
category = ''
@@ -29,4 +26,31 @@ export class CategoriesComponent {
this.category = category
this.categoryChange.emit(category)
}
determineIcon(category: string): string {
switch (category.toLowerCase()) {
case 'all':
return 'apps-outline'
case 'bitcoin':
return 'logo-bitcoin'
case 'communications':
return 'chatbubbles-outline'
case 'data':
return 'document-outline'
case 'developer tools':
return 'code-slash-outline'
case 'featured':
return 'star-outline'
case 'lightning':
return 'flash-outline'
case 'media':
return 'play-outline'
case 'networking':
return 'globe-outline'
case 'social':
return 'people-outline'
default:
return 'cube-outline'
}
}
}

View File

@@ -1,12 +1,33 @@
<ion-item class="service-card" [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start">
<img alt="" [src]="pkg | mimeType | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2 class="montserrat">
<strong>{{ pkg.manifest.title }}</strong>
</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<ng-content></ng-content>
</ion-label>
</ion-item>
<div
class="h-full relative rounded-3xl pt-20 pb-10 px-8 gap-4 shadow-lg hover:scale-90 transition duration-500 ease-in-out min-w-[300px]"
>
<!-- color background -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 -z-50 rounded-3xl bg-zinc-800"
>
<img
[src]="'data:image/png;base64,' + pkg.icon | trustUrl"
class="absolute object-cover pointer-events-none w-[150%] h-[150%] max-w-[200%] blur-[100px]"
alt="{{ pkg.manifest.title }} Icon"
/>
</div>
<!-- darkening overlay -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 -z-50 rounded-3xl bg-zinc-800 opacity-40"
></div>
<!-- icon -->
<img
[src]="'data:image/png;base64,' + pkg.icon | trustUrl"
class="w-[5.5rem] h-[5.5rem] pointer-events-none absolute -top-10 rounded-full object-cover shadow-lg z-10"
alt="{{ pkg.manifest.title }} Icon"
style="transform: none"
/>
<div class="mt-3 text-zinc-5 mix-blend-plus-lighter">
<span class="block text-2xl font-medium line-clamp-1 mb-1">
{{ pkg.manifest.title }}
</span>
<span class="block text-base h-12 line-clamp-2">
{{ pkg.manifest.description.short }}
</span>
</div>
</div>

View File

@@ -1,20 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({
declarations: [ItemComponent],
exports: [ItemComponent],
imports: [
CommonModule,
IonicModule,
RouterModule,
SharedPipesModule,
MimeTypePipeModule,
],
imports: [CommonModule, RouterModule, SharedPipesModule],
})
export class ItemModule {}

View File

@@ -1,14 +1,20 @@
<ion-grid>
<ion-row>
<ion-col responsiveCol class="column" sizeSm="8" sizeLg="6">
<ion-toolbar color="transparent" class="ion-text-left">
<ion-searchbar
[color]="(theme$ | async) === 'Light' ? 'light' : 'dark'"
debounce="250"
[ngModel]="query"
(ngModelChange)="onModelChange($event)"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>
<div class="text-zinc-100 bg-zinc-500 px-3 rounded-full">
<div class="relative flex items-center">
<img
alt="Search icon"
width="24"
height="24"
class="invert"
src="svg/search.svg"
/>
<input
type="text"
name="search"
class="mt-1 bg-transparent placeholder-zinc-300 focus:outline-none px-3 py-2"
placeholder="Search..."
autocomplete="off"
[ngModel]="query"
(ngModelChange)="onModelChange($event)"
/>
</div>
</div>

View File

@@ -1,8 +0,0 @@
:host {
display: block;
padding-bottom: 32px;
}
.column {
margin: 0 auto;
}

View File

@@ -16,7 +16,7 @@ import { THEME } from '@start9labs/shared'
})
export class SearchComponent {
@Input()
query = ''
query?: string | null = ''
@Output()
readonly queryChange = new EventEmitter<string>()

View File

@@ -1,13 +1,11 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { ResponsiveColDirective } from '@start9labs/shared'
import { SearchComponent } from './search.component'
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { ResponsiveColDirective } from "@start9labs/shared";
import { SearchComponent } from "./search.component";
@NgModule({
imports: [IonicModule, FormsModule, CommonModule, ResponsiveColDirective],
imports: [FormsModule, CommonModule, ResponsiveColDirective],
declarations: [SearchComponent],
exports: [SearchComponent],
})

View File

@@ -1,4 +1,4 @@
<div class="hidden-scrollbar ion-text-center">
<div class="">
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
<ion-skeleton-text
animated
@@ -6,17 +6,47 @@
></ion-skeleton-text>
</ion-button>
</div>
<div class="hidden sm:flex flex-col mx-6 pb-3 items-center">
<div class="pb-3">
<ion-skeleton-text style="border-radius: 100%" animated></ion-skeleton-text>
</div>
<div class="divider" style="margin: 24px 0"></div>
<ng-content select="[slot=desktop]"></ng-content>
</div>
<ion-row>
<ion-col
*ngFor="let cat of ['', '', '', '', '', '', '']"
fill="clear"
responsiveCol
sizeXs="12"
sizeSm="12"
sizeMd="3"
>
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text
style="border-radius: 100%"
animated
></ion-skeleton-text>
</ion-thumbnail>
<ion-label>
<ion-skeleton-text
animated
style="width: 150px; height: 18px; margin-bottom: 8px"
></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
<ion-grid>
<ion-grid class="p-20">
<ion-row>
<ion-col
*ngFor="let pkg of ['', '', '', '']"
responsiveCol
sizeXs="12"
sizeSm="12"
sizeMd="6"
sizeMd="3"
>
<ion-item>
<ion-thumbnail slot="start">

View File

@@ -1,33 +1,58 @@
<ion-content class="with-widgets">
<ng-container *ngIf="notes$ | async as notes; else loading">
<div *ngFor="let note of notes | keyvalue : asIsOrder">
<ion-button
expand="full"
color="light"
class="version-button"
[class.ion-activated]="isSelected(note.key)"
(click)="setSelected(note.key)"
<div
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10 mt-6"
>
<div class="bg-zinc-800 rounded-xl p-7 grid grid-flow-row items-center gap-6">
<div class="block">
<h3 class="text-lg font-bold small-caps">What's new</h3>
<p
*ngIf="pkg['published-at'] as published"
class="text-base font-light mb-1 text-zinc-300"
>
<p class="version">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
tuiElement
#element="elementRef"
class="panel"
color="light"
[id]="note.key"
[style.maxHeight.px]="getDocSize(note.key, element)"
>
<ion-text
id="release-notes"
safeLinks
[innerHTML]="note.value | markdown | dompurify"
></ion-text>
</ion-card>
<span class="small-caps">Latest Release</span>
&nbsp;-&nbsp;
<span class="text-sm">{{ published | date : 'medium' }}</span>
</p>
</div>
<div class="flex flex-wrap justify-between gap-6">
<div class="text-base">
<p>Version {{ pkg.manifest.version }}</p>
<p
safeLinks
class="flex-wrap mt-1"
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
></p>
</div>
<button
tuiButton
type="button"
appearance="secondary"
size="m"
class="mt-3 place-self-end sm:place-self-start md:place-self-start lg:place-self-end"
(click)="showReleaseNotes(template)"
>
Previous releases
</button>
</div>
</div>
</div>
<ng-template #template let-observer>
<ng-container *ngIf="notes$ | async as notes; else loading">
<tui-accordion
class="max-w-lg"
[closeOthers]="false"
*ngFor="let note of notes | keyvalue : asIsOrder"
>
<tui-accordion-item class="my-1">
{{ note.key | displayEmver }}
<ng-template tuiAccordionItemContent>
<p safeLinks [innerHTML]="note.value | markdown | dompurify"></p>
</ng-template>
</tui-accordion-item>
</tui-accordion>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>
</ng-template>

View File

@@ -1,23 +0,0 @@
:host {
flex: 1 1 0;
}
.panel {
margin: 0;
padding: 0 24px;
transition: max-height 0.2s ease-out;
}
.active {
border: 5px solid #4d4d4d;
}
.version-button {
height: 50px;
margin: 1px;
}
.version {
position: absolute;
left: 10px;
}

View File

@@ -1,7 +1,15 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
import { PolymorpheusContent } from '@tinkoff/ng-polymorpheus'
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
import { MarketplacePkg } from '../../types'
import { Observable } from 'rxjs'
@Component({
selector: 'release-notes',
@@ -13,27 +21,29 @@ export class ReleaseNotesComponent {
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
) {}
private readonly pkgId = getPkgId(this.route)
@Input({ required: true })
pkg!: MarketplacePkg
private selected: string | null = null
notes$!: Observable<Record<string, string>>
readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId)
isSelected(key: string): boolean {
return this.selected === key
}
setSelected(selected: string) {
this.selected = this.isSelected(selected) ? null : selected
}
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
return this.isSelected(key) ? nativeElement.scrollHeight : 0
ngOnChanges() {
this.notes$ = this.marketplaceService.fetchReleaseNotes$(
this.pkg.manifest.id,
)
}
asIsOrder(a: any, b: any) {
return 0
}
async showReleaseNotes(content: PolymorpheusContent<TuiDialogContext>) {
this.dialogs
.open(content, {
label: 'Previous Release Notes',
})
.subscribe()
}
}

View File

@@ -1,27 +1,26 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import {
EmverPipesModule,
MarkdownPipeModule,
SafeLinksDirective,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import { TuiElementModule } from '@taiga-ui/cdk'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { ReleaseNotesComponent } from './release-notes.component'
} from "@start9labs/shared";
import { NgDompurifyModule } from "@tinkoff/ng-dompurify";
import { ReleaseNotesComponent } from "./release-notes.component";
import { TuiButtonModule } from "@taiga-ui/core";
import { TuiAccordionModule } from "@taiga-ui/kit";
@NgModule({
imports: [
CommonModule,
IonicModule,
TextSpinnerComponentModule,
EmverPipesModule,
MarkdownPipeModule,
TuiElementModule,
NgDompurifyModule,
SafeLinksDirective,
TuiButtonModule,
TuiAccordionModule,
],
declarations: [ReleaseNotesComponent],
exports: [ReleaseNotesComponent],

View File

@@ -1,32 +1,21 @@
<!-- release notes -->
<ion-item-divider>
New in {{ pkg.manifest.version | displayEmver }}
</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div
safeLinks
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
></div>
</ion-label>
</ion-item>
<ion-button routerLink="notes" fill="clear" strong>
Past Release Notes
<ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>
<!-- description -->
<ion-item-divider>Description</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<h2>{{ pkg.manifest.description.long }}</h2>
</ion-label>
</ion-item>
<div
*ngIf="pkg.manifest['marketing-site'] as url"
style="padding: 4px 0 10px 14px"
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10"
>
<ion-button [href]="url" target="_blank" rel="noreferrer" color="tertiary">
View website
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-button>
<div class="lg:col-span-5 xl:col-span-4 bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps pb-3">Description</h2>
<p class="text-base mb-3">
{{ pkg.manifest.description.long }}
</p>
<ng-container *ngIf="pkg.manifest.replaces as replaces">
<div *ngIf="replaces.length">
<h2 class="text-lg font-bold small-caps pb-3">Intended to replace</h2>
<tui-tag
*ngFor="let app; index as i; of: replaces"
size="l"
[class]="i > 0 ? 'ml-1.5 mt-2' : 'mt-2'"
[value]="app"
></tui-tag>
</div>
</ng-container>
</div>
</div>

View File

@@ -1,23 +1,16 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
MarkdownPipeModule,
SafeLinksDirective,
} from '@start9labs/shared'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { AboutComponent } from './about.component'
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { AboutComponent } from "./about.component";
import { TuiTagModule } from "@taiga-ui/kit";
import { NgDompurifyModule } from "@tinkoff/ng-dompurify";
import { SafeLinksDirective } from "@start9labs/shared";
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
MarkdownPipeModule,
EmverPipesModule,
TuiTagModule,
NgDompurifyModule,
SafeLinksDirective,
],

View File

@@ -0,0 +1,12 @@
<a [href]="url" target="_blank" rel="noreferrer">
<div class="flex justify-between items-center">
<label [tuiLabel]="label" class="hover:cursor-pointer">
<tui-line-clamp [content]="url" [linesLimit]="1"></tui-line-clamp>
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/open-outline.svg"
/>
</div>
</a>

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { AdditionalLinkComponent } from './additional-link.component'
import { TuiLabelModule } from '@taiga-ui/core'
import { TuiLineClampModule } from '@taiga-ui/kit'
@NgModule({
imports: [CommonModule, TuiLabelModule, TuiLineClampModule],
declarations: [AdditionalLinkComponent],
exports: [AdditionalLinkComponent],
})
export class AdditionalLinkModule {}

View File

@@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { Url } from '@start9labs/shared'
@Component({
selector: 'marketplace-additional-link',
templateUrl: 'additional-link.component.html',
styleUrls: ['additional-link.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdditionalLinkComponent {
@Input({ required: true })
url!: Url
@Input({ required: true })
label!: string
}

View File

@@ -1,131 +1,128 @@
<ng-container *ngIf="pkg.manifest.replaces as replaces">
<div *ngIf="replaces.length" class="ion-padding-bottom">
<ion-item-divider>Intended to replace</ion-item-divider>
<ul>
<li *ngFor="let app of replaces">
{{ app }}
</li>
</ul>
</div>
</ng-container>
<div
class="rounded-xl bg-gradient-to-bl from-zinc-400/75 to-zinc-600 p-px shadow-lg shadow-zinc-400/10"
>
<div class="bg-zinc-800 rounded-xl p-7">
<h2 class="text-lg font-bold small-caps pb-3">Information</h2>
<div class="grid grid-flow-row divide-y divide-zinc-500">
<!-- git hash -->
<div
*ngIf="pkg.manifest['git-hash'] as gitHash; else noHash"
button
detail="false"
class="py-3 px-1 flex flex-wrap justify-between items-center"
(click)="copyService.copy(gitHash)"
>
<label tuiLabel="Git Hash">{{ gitHash }}</label>
<img
alt="Copy Icon"
class="block opacity-70 invert w-4 hover:cursor-copy"
src="svg/copy-outline.svg"
/>
</div>
<ng-template #noHash>
<div class="py-3 px-1">
<label tuiLabel="Git Hash">Unknown</label>
</div>
</ng-template>
<!-- license -->
<div
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
(click)="presentModalMd('License')"
>
<div class="flex flex-wrap justify-between items-center">
<label tuiLabel="License" class="hover:cursor-pointer">
{{ pkg.manifest.license }}
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/chevron-forward.svg"
/>
</div>
</div>
<!-- instructions -->
<div
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
(click)="presentModalMd('Instructions')"
>
<div class="flex flex-wrap justify-between items-center">
<label tuiLabel="Instructions" class="hover:cursor-pointer">
Click to view instructions
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/chevron-forward.svg"
/>
</div>
</div>
<ion-item-divider>Additional Info</ion-item-divider>
<ion-grid *ngIf="pkg.manifest as manifest">
<ion-row>
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
*ngIf="manifest['git-hash'] as gitHash; else noHash"
button
detail="false"
(click)="copyService.copy(gitHash)"
>
<ion-label>
<h2>Git Hash</h2>
<p>{{ gitHash }}</p>
</ion-label>
<ion-icon slot="end" name="copy-outline"></ion-icon>
</ion-item>
<ng-template #noHash>
<ion-item>
<ion-label>
<h2>Git Hash</h2>
<p>Unknown</p>
</ion-label>
</ion-item>
<!-- versions -->
<div
(click)="presentAlertVersions(version)"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
>
<div class="flex flex-wrap justify-between items-center">
<label tuiLabel="Other versions" class="hover:cursor-pointer">
Click to view other versions
</label>
<img
alt="Open Icon"
class="block opacity-70 invert w-4"
src="svg/chevron-forward.svg"
/>
</div>
<ng-template #version let-data="data" let-completeWith="completeWith">
<tui-radio-list
class="radio"
size="l"
[items]="data.items"
[itemContent]="displayEmver | tuiStringifyContent"
[(ngModel)]="data.value"
></tui-radio-list>
<footer class="buttons">
<button
tuiButton
appearance="secondary"
(click)="completeWith(null)"
>
Cancel
</button>
<button
tuiButton
appearance="secondary"
(click)="completeWith(data.value)"
>
Ok
</button>
</footer>
</ng-template>
<ion-item button detail="false" (click)="presentAlertVersions(version)">
<ion-label>
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
<ng-template #version let-data="data" let-completeWith="completeWith">
<tui-radio-list
class="radio"
size="l"
[items]="data.items"
[itemContent]="displayEmver | tuiStringifyContent"
[(ngModel)]="data.value"
></tui-radio-list>
<footer class="buttons">
<button
tuiButton
appearance="secondary"
(click)="completeWith(null)"
>
Cancel
</button>
<button
tuiButton
appearance="secondary"
(click)="completeWith(data.value)"
>
Ok
</button>
</footer>
</ng-template>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('License')">
<ion-label>
<h2>License</h2>
<p>{{ manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('Instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="manifest['upstream-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ manifest['upstream-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest['wrapper-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ manifest['wrapper-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest['support-site']"
[disabled]="!manifest['support-site']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ manifest['support-site'] || 'Not provided' }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid>
</div>
<!-- links -->
<marketplace-additional-link
[url]="pkg.manifest['marketing-site']"
*ngIf="pkg.manifest['marketing-site']"
label="Marketing Site"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
<marketplace-additional-link
[url]="pkg.manifest['upstream-repo']"
*ngIf="pkg.manifest['upstream-repo']"
label="Source Repository"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
<marketplace-additional-link
[url]="pkg.manifest['wrapper-repo']"
*ngIf="pkg.manifest['wrapper-repo']"
label="Wrapper Repository"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
<marketplace-additional-link
[url]="pkg.manifest['support-site']"
*ngIf="pkg.manifest['support-site']"
label="Support Site"
class="py-3 px-1 hover:bg-zinc-500/10 hover:cursor-pointer"
></marketplace-additional-link>
</div>
</div>
</div>

View File

@@ -1,10 +0,0 @@
.radio {
display: block;
margin: 1rem 0;
}
.buttons {
display: flex;
justify-content: flex-end;
gap: 1rem;
}

View File

@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
EventEmitter,
inject,
Input,
Output,
TemplateRef,
@@ -38,13 +39,13 @@ export class AdditionalComponent {
version = new EventEmitter<string>()
readonly displayEmver = displayEmver
private readonly marketplaceService = inject(AbstractMarketplaceService)
constructor(
readonly copyService: CopyService,
private readonly alerts: TuiAlertService,
private readonly dialogs: TuiDialogService,
private readonly emver: Emver,
private readonly marketplaceService: AbstractMarketplaceService,
private readonly route: ActivatedRoute,
) {}

View File

@@ -1,26 +1,25 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { MarkdownModule, ResponsiveColDirective } from '@start9labs/shared'
import { AdditionalComponent } from './additional.component'
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { AdditionalComponent } from "./additional.component";
import {
TuiRadioListModule,
TuiStringifyContentPipeModule,
} from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms'
import { TuiButtonModule } from '@taiga-ui/core'
} from "@taiga-ui/kit";
import { FormsModule } from "@angular/forms";
import { TuiButtonModule, TuiLabelModule } from "@taiga-ui/core";
import { AdditionalLinkModule } from "./additional-link/additional-link.component.module";
import { ResponsiveColDirective } from "@start9labs/shared";
@NgModule({
imports: [
CommonModule,
IonicModule,
MarkdownModule,
ResponsiveColDirective,
TuiRadioListModule,
FormsModule,
TuiStringifyContentPipeModule,
TuiButtonModule,
TuiLabelModule,
AdditionalLinkModule,
],
declarations: [AdditionalComponent],
exports: [AdditionalComponent],

View File

@@ -1,35 +1,30 @@
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
responsiveCol
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img
alt=""
style="border-radius: 100%"
[src]="getImg(dep.key) | trustUrl"
/>
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
<div
class="bg-zinc-700/40 rounded-xl py-3 px-5 gap-2 drop-shadow-lg hover:bg-zinc-700/70"
>
<div class="flex items-center gap-6">
<tui-avatar
class="w-16 pointer-events-none rounded-full object-cover drop-shadow-lg"
[src]="getImage(dep.key)"
></tui-avatar>
<div class="mt-3">
<div class="flex flex-wrap items-center gap-1 mb-1">
<span class="block text-base font-medium text-zinc-50/90 line-clamp-1">
{{ getTitle(dep.key) }}
</span>
<p>
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</p>
</div>
<span class="text-sm text-zinc-50/70 line-clamp-1">
{{ dep.value.version | displayEmver }}
</span>
<span class="text-sm text-zinc-50/70 h-11 line-clamp-2">
{{ dep.value.description }}
</span>
</div>
</div>
</div>

View File

@@ -1,17 +1,27 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types'
import { Dependency, MarketplacePkg } from '../../../types'
import { KeyValue } from '@angular/common'
@Component({
selector: 'marketplace-dependencies',
templateUrl: 'dependencies.component.html',
styleUrls: ['./dependencies.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DependenciesComponent {
@Input({ required: true })
pkg!: MarketplacePkg
getImg(key: string): string {
@Input({ required: true })
dep!: KeyValue<string, Dependency>
getImage(key: string): string {
const icon = this.pkg['dependency-metadata'][key]?.icon
// @TODO fix when registry api is updated to include mimetype in icon url
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
return icon ? `data:image/png;base64,${icon}` : key.substring(0, 2)
}
getTitle(key: string): string {
return this.pkg['dependency-metadata'][key]?.title || key
}
}

View File

@@ -1,23 +1,16 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
ResponsiveColDirective,
SharedPipesModule,
} from '@start9labs/shared'
import { EmverPipesModule, ResponsiveColDirective } from '@start9labs/shared'
import { DependenciesComponent } from './dependencies.component'
import { TuiAvatarModule } from '@taiga-ui/experimental'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
ResponsiveColDirective,
TuiAvatarModule,
EmverPipesModule,
],
declarations: [DependenciesComponent],
exports: [DependenciesComponent],

View File

@@ -0,0 +1,55 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared'
import { MarketplacePkg } from '../../../types'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@Component({
selector: 'marketplace-package-hero',
template: `
<div class="flex justify-center mt-10 md:mt-0 z-0">
<div
class="flex flex-col w-full h-[32vh] xs:h-[26vh] md:min-h-[14rem] relative rounded-3xl pt-16 px-8 shadow-lg"
>
<!-- icon -->
<img
[src]="pkg | mimeType | trustUrl"
class="w-24 h-24 pointer-events-none rounded-full object-cover shadow-lg absolute -top-9 left-7 z-10"
alt="{{ pkg.manifest.title }} Icon"
/>
<!-- color background -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 rounded-3xl bg-zinc-800"
>
<img
[src]="pkg | mimeType | trustUrl"
class="absolute object-cover pointer-events-none w-[200%] h-[200%] max-w-[200%] blur-[100px] saturate-150 rounded-full"
alt="{{ pkg.manifest.title }} background image"
/>
</div>
<!-- background darkening overlay -->
<div
class="overflow-hidden absolute w-full h-full top-0 left-0 rounded-3xl bg-zinc-700 opacity-70"
></div>
<div class="my-4 text-zinc-50 mix-blend-plus-lighter">
<h2 class="text-2xl font-medium line-clamp-1 mb-1">
{{ pkg.manifest.title }}
</h2>
<p class="block text-base line-clamp-2">
{{ pkg.manifest.description.short }}
</p>
</div>
<!-- control buttons -->
<ng-content></ng-content>
</div>
</div>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, SharedPipesModule, MimeTypePipeModule],
})
export class MarketplacePackageHeroComponent {
@Input({ required: true })
pkg!: MarketplacePkg
}

View File

@@ -1,9 +0,0 @@
<img class="logo" alt="" [src]="pkg | mimeType | trustUrl" />
<div class="text">
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<p *ngIf="pkg['published-at'] as published" class="published">
Released: {{ published | date : 'medium' }}
</p>
<ng-content></ng-content>
</div>

View File

@@ -1,48 +0,0 @@
:host {
display: flex;
align-items: flex-start;
padding: 16px;
line-height: 2;
}
.text {
display: inline-block;
vertical-align: top;
overflow: hidden;
}
.logo {
width: 80px;
margin-right: 16px;
border-radius: 100%;
}
.title {
margin: 0 0 0 -2px;
font-size: 36px;
}
.version {
margin: 0;
font-size: 18px;
}
.published {
margin: 0;
padding: 4px 0 12px 0;
font-style: italic;
}
@media (min-width: 1000px) {
.logo {
width: 140px;
}
.title {
font-size: 64px;
}
.version {
font-size: 32px;
}
}

View File

@@ -1,13 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types'
@Component({
selector: 'marketplace-package',
templateUrl: 'package.component.html',
styleUrls: ['package.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PackageComponent {
@Input({ required: true })
pkg!: MarketplacePkg
}

View File

@@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
SharedPipesModule,
TickerModule,
} from '@start9labs/shared'
import { PackageComponent } from './package.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({
declarations: [PackageComponent],
exports: [PackageComponent],
imports: [
CommonModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
TickerModule,
MimeTypePipeModule,
],
})
export class PackageModule {}

View File

@@ -0,0 +1,97 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
} from '@angular/core'
import {
TuiButtonModule,
TuiDialogContext,
TuiDialogService,
} from '@taiga-ui/core'
import { TuiCarouselModule } from '@taiga-ui/kit'
import { MarketplacePkg } from '../../../types'
import { PolymorpheusContent } from '@tinkoff/ng-polymorpheus'
import { isPlatform } from '@ionic/angular'
@Component({
selector: 'marketplace-package-screenshots',
template: `
<div
*ngIf="pkg.screenshots"
tuiCarouselButtons
class="flex items-center content-center m-0 lg:-ml-14 lg:-mr-14 lg:min-h-80 lg:h-80 2xl:h-full"
>
<button
tuiIconButton
appearance="flat"
icon="tuiIconChevronLeftLarge"
title="Previous"
type="button"
(click)="carousel.prev()"
></button>
<tui-carousel
#carousel
[itemsCount]="isMobile ? 1 : 2"
[(index)]="index"
class="overflow-y-hidden overflow-x-scroll carousel overflow-hidden"
>
<ng-container *ngFor="let item of pkg.screenshots; let i = index">
<div
*tuiItem
draggable="false"
[class.item_active]="i === index + 1"
class="object-cover overflow-hidden rounded-lg md:rounded-xl border border-zinc-400/30 hover:cursor-pointer shadow-lg shadow-zinc-400/10"
>
<img
#template
alt="Service screenshot"
src="assets/img/temp/{{ item }}"
class="w-full h-full rounded-lg md:rounded-xl"
(click)="presentModalImg(dialogTemplate)"
/>
<ng-template #dialogTemplate let-observer>
<img
alt="Service screenshot"
src="assets/img/temp/{{ item }}"
class="rounded-none"
/>
</ng-template>
</div>
</ng-container>
</tui-carousel>
<button
tuiIconButton
appearance="flat"
type="button"
icon="tuiIconChevronRightLarge"
title="Next"
(click)="carousel.next()"
></button>
</div>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, TuiCarouselModule, TuiButtonModule],
})
export class MarketplacePackageScreenshotComponent {
@Input({ required: true })
pkg!: MarketplacePkg
constructor(
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
) {}
index = 0
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
presentModalImg(content: PolymorpheusContent<TuiDialogContext>) {
this.dialogs
.open(content, {
size: 'l',
})
.subscribe()
}
}

View File

@@ -8,8 +8,8 @@ import Fuse from 'fuse.js'
export class FilterPackagesPipe implements PipeTransform {
transform(
packages: MarketplacePkg[],
query: string,
category: string,
query: string | null,
category: string | null,
): MarketplacePkg[] {
// query
if (query) {
@@ -68,13 +68,14 @@ export class FilterPackagesPipe implements PipeTransform {
// category
return packages
.filter(p => category === 'all' || p.categories.includes(category))
.filter(p => category === 'all' || p.categories.includes(category!))
.sort((a, b) => {
return (
new Date(b['published-at']).valueOf() -
new Date(a['published-at']).valueOf()
)
})
.map(a => ({ ...a }))
}
}

View File

@@ -8,8 +8,6 @@ export * from './pages/list/item/item.component'
export * from './pages/list/item/item.module'
export * from './pages/list/search/search.component'
export * from './pages/list/search/search.module'
export * from './pages/list/skeleton/skeleton.component'
export * from './pages/list/skeleton/skeleton.module'
export * from './pages/release-notes/release-notes.component'
export * from './pages/release-notes/release-notes.module'
export * from './pages/show/about/about.component'
@@ -18,8 +16,8 @@ export * from './pages/show/additional/additional.component'
export * from './pages/show/additional/additional.module'
export * from './pages/show/dependencies/dependencies.component'
export * from './pages/show/dependencies/dependencies.module'
export * from './pages/show/package/package.component'
export * from './pages/show/package/package.module'
export * from './pages/show/screenshots/screenshots.component'
export * from './pages/show/hero/hero.component'
export * from './pipes/filter-packages.pipe'
export * from './pipes/mime-type.pipe'
@@ -27,7 +25,10 @@ export * from './pipes/mime-type.pipe'
export * from './components/store-icon/store-icon.component'
export * from './components/store-icon/store-icon.component.module'
export * from './components/store-icon/store-icon.component'
export * from './components/menu/menu.component.module'
export * from './components/menu/menu.component'
export * from './services/marketplace.service'
export * from './services/category.service'
export * from './types'

View File

@@ -0,0 +1,16 @@
import { BehaviorSubject, Observable } from 'rxjs'
export abstract class AbstractCategoryService {
readonly category$ = new BehaviorSubject<string>('all')
readonly query$ = new BehaviorSubject<string>('')
abstract getCategory$(): Observable<string>
abstract changeCategory(category: string): void
abstract setQuery(query: string): void
abstract getQuery$(): Observable<string>
abstract resetQuery(): void
}

View File

@@ -1,5 +1,11 @@
import { Observable } from 'rxjs'
import { MarketplacePkg, Marketplace, StoreData, StoreIdentity } from '../types'
import {
Marketplace,
MarketplacePkg,
StoreData,
StoreIdentity,
StoreIdentityWithData,
} from '../types'
export abstract class AbstractMarketplaceService {
abstract getKnownHosts$(): Observable<StoreIdentity[]>
@@ -10,6 +16,8 @@ export abstract class AbstractMarketplaceService {
abstract getSelectedStore$(): Observable<StoreData>
abstract getSelectedStoreWithCategories$(): Observable<StoreIdentityWithData>
abstract getPackage$(
id: string,
version: string,

View File

@@ -19,9 +19,12 @@ export interface StoreInfo {
categories: string[]
}
export type StoreIdentityWithData = StoreData & StoreIdentity
export interface MarketplacePkg {
icon: Url
license: Url
screenshots?: string[]
instructions: Url
manifest: Manifest
categories: string[]