- Create TbIotHubHomeComponent as default route at /iot-hub - Move browse page to /iot-hub/browse, accept type/search query params - Interactive hero: hoverable keywords cycle through item types with color changes, background gradient transitions, and floating icon animations - Page load animation: hero and categories fade in with slide (down/up) - Category cards: 3x2 grid with exact Figma gradients and separated title/image layout - Popular sections: 5-column grid for widgets/dashboards/solutions, 3-column grid for calculated fields/rule chains - Become a Creator footer section - Export category card images and hero floating icons from Figma - Add plural item type translations and home page locale keyspull/15347/head
@ -0,0 +1,200 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2026 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-iot-hub-home"> |
||||
|
<!-- Hero section --> |
||||
|
<div class="tb-iot-hub-hero"> |
||||
|
<!-- Background gradient overlay --> |
||||
|
<div class="tb-iot-hub-hero-gradient" |
||||
|
[style.background]="'radial-gradient(ellipse at 50% 0%, ' + activeHeroType.gradientColor + ' 0%, transparent 60%)'"></div> |
||||
|
|
||||
|
<!-- Floating icons --> |
||||
|
@for (heroType of heroTypes; track heroType.type) { |
||||
|
@if (heroType.icons.length) { |
||||
|
<div class="tb-iot-hub-hero-icons" |
||||
|
[class.active]="heroIconsReady && activeHeroType === heroType"> |
||||
|
@for (iconUrl of heroType.icons; track iconUrl; let i = $index) { |
||||
|
<img class="tb-iot-hub-hero-float-icon" |
||||
|
[class]="'icon-pos-' + (i + 1)" |
||||
|
[src]="iconUrl" alt=""> |
||||
|
} |
||||
|
</div> |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
<div class="tb-iot-hub-hero-content"> |
||||
|
<h1 class="tb-iot-hub-hero-title">{{ 'iot-hub.home-title' | translate }}</h1> |
||||
|
<div class="tb-iot-hub-hero-subtitle"> |
||||
|
<span class="tb-iot-hub-hero-prefix">{{ 'iot-hub.home-subtitle-prefix' | translate }}</span> |
||||
|
@for (ht of heroTypes; track ht.type; let last = $last) { |
||||
|
<span class="tb-iot-hub-hero-keyword" |
||||
|
[class.active]="activeHeroType === ht" |
||||
|
[style.--keyword-color]="ht.color" |
||||
|
(mouseenter)="onHeroTypeHover(ht)" |
||||
|
(mouseleave)="onHeroTypeLeave()">{{ ht.labelKey | translate }}@if (!last) {,}</span> |
||||
|
} |
||||
|
</div> |
||||
|
<mat-form-field appearance="outline" class="tb-iot-hub-hero-search"> |
||||
|
<input matInput |
||||
|
[placeholder]="'iot-hub.search-placeholder' | translate" |
||||
|
[(ngModel)]="searchText" |
||||
|
(keyup.enter)="onSearch()"> |
||||
|
<button matSuffix mat-icon-button (click)="onSearch()"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Category cards --> |
||||
|
<div class="tb-iot-hub-categories"> |
||||
|
@for (card of categoryCards; track card.type) { |
||||
|
<div class="tb-iot-hub-category-card" [ngClass]="card.cssClass" (click)="navigateToBrowse(card.type)"> |
||||
|
<span class="tb-iot-hub-category-title">{{ card.titleKey | translate }}</span> |
||||
|
<img class="tb-iot-hub-category-img" [src]="card.image" alt=""> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
|
|
||||
|
@if (!isLoading) { |
||||
|
<!-- Popular Widgets --> |
||||
|
@if (popularWidgets.length) { |
||||
|
<div class="tb-iot-hub-section"> |
||||
|
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.WIDGET)"> |
||||
|
{{ 'iot-hub.popular-widgets' | translate }} |
||||
|
<mat-icon>chevron_right</mat-icon> |
||||
|
</a> |
||||
|
<div class="tb-iot-hub-big-cards-row"> |
||||
|
@for (item of popularWidgets; track item.id) { |
||||
|
<tb-iot-hub-item-card |
||||
|
[item]="item" |
||||
|
[showTypeChip]="false" |
||||
|
[showCreator]="true" |
||||
|
(cardClick)="openItemDetail($event)" |
||||
|
(creatorClick)="navigateToCreator($event)" |
||||
|
(installClick)="installItem($event)"> |
||||
|
</tb-iot-hub-item-card> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Popular Dashboards --> |
||||
|
@if (popularDashboards.length) { |
||||
|
<div class="tb-iot-hub-section"> |
||||
|
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.DASHBOARD)"> |
||||
|
{{ 'iot-hub.popular-dashboards' | translate }} |
||||
|
<mat-icon>chevron_right</mat-icon> |
||||
|
</a> |
||||
|
<div class="tb-iot-hub-big-cards-row"> |
||||
|
@for (item of popularDashboards; track item.id) { |
||||
|
<tb-iot-hub-item-card |
||||
|
[item]="item" |
||||
|
[showTypeChip]="false" |
||||
|
[showCreator]="true" |
||||
|
(cardClick)="openItemDetail($event)" |
||||
|
(creatorClick)="navigateToCreator($event)" |
||||
|
(installClick)="installItem($event)"> |
||||
|
</tb-iot-hub-item-card> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Popular Solution Templates --> |
||||
|
@if (popularSolutionTemplates.length) { |
||||
|
<div class="tb-iot-hub-section"> |
||||
|
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.SOLUTION_TEMPLATE)"> |
||||
|
{{ 'iot-hub.popular-solution-templates' | translate }} |
||||
|
<mat-icon>chevron_right</mat-icon> |
||||
|
</a> |
||||
|
<div class="tb-iot-hub-big-cards-row"> |
||||
|
@for (item of popularSolutionTemplates; track item.id) { |
||||
|
<tb-iot-hub-item-card |
||||
|
[item]="item" |
||||
|
[showTypeChip]="false" |
||||
|
[showCreator]="true" |
||||
|
(cardClick)="openItemDetail($event)" |
||||
|
(creatorClick)="navigateToCreator($event)" |
||||
|
(installClick)="installItem($event)"> |
||||
|
</tb-iot-hub-item-card> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Popular Calculated Fields --> |
||||
|
@if (popularCalcFields.length) { |
||||
|
<div class="tb-iot-hub-section"> |
||||
|
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.CALCULATED_FIELD)"> |
||||
|
{{ 'iot-hub.popular-calculated-fields' | translate }} |
||||
|
<mat-icon>chevron_right</mat-icon> |
||||
|
</a> |
||||
|
<div class="tb-iot-hub-compact-cards-grid"> |
||||
|
@for (item of popularCalcFields; track item.id) { |
||||
|
<tb-iot-hub-item-card |
||||
|
[item]="item" |
||||
|
[showTypeChip]="false" |
||||
|
[showCreator]="true" |
||||
|
(cardClick)="openItemDetail($event)" |
||||
|
(creatorClick)="navigateToCreator($event)" |
||||
|
(installClick)="installItem($event)"> |
||||
|
</tb-iot-hub-item-card> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Popular Rule Chains --> |
||||
|
@if (popularRuleChains.length) { |
||||
|
<div class="tb-iot-hub-section"> |
||||
|
<a class="tb-iot-hub-section-header" (click)="navigateToBrowse(ItemType.RULE_CHAIN)"> |
||||
|
{{ 'iot-hub.popular-rule-chains' | translate }} |
||||
|
<mat-icon>chevron_right</mat-icon> |
||||
|
</a> |
||||
|
<div class="tb-iot-hub-compact-cards-grid"> |
||||
|
@for (item of popularRuleChains; track item.id) { |
||||
|
<tb-iot-hub-item-card |
||||
|
[item]="item" |
||||
|
[showTypeChip]="false" |
||||
|
[showCreator]="true" |
||||
|
(cardClick)="openItemDetail($event)" |
||||
|
(creatorClick)="navigateToCreator($event)" |
||||
|
(installClick)="installItem($event)"> |
||||
|
</tb-iot-hub-item-card> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
<!-- Become a Creator --> |
||||
|
<div class="tb-iot-hub-divider"></div> |
||||
|
<div class="tb-iot-hub-become-creator"> |
||||
|
<h2>{{ 'iot-hub.become-a-creator' | translate }}</h2> |
||||
|
<p>{{ 'iot-hub.become-creator-text' | translate }}</p> |
||||
|
<button mat-stroked-button color="primary" (click)="openSignup()"> |
||||
|
{{ 'iot-hub.submit-template' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (isLoading) { |
||||
|
<div class="tb-iot-hub-loading"> |
||||
|
<mat-spinner diameter="40"></mat-spinner> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
@ -0,0 +1,419 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2026 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
// Outer page — 16px padding around the white container |
||||
|
:host { |
||||
|
display: block; |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
// Page container — white card with 8px radius, matches Figma "page-container" |
||||
|
.tb-iot-hub-home { |
||||
|
position: relative; |
||||
|
background: white; |
||||
|
border-radius: 8px; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
// Dot grid pattern — covers top 800px, fades out via gradient mask |
||||
|
&::before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
height: 800px; |
||||
|
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.1) 1px, transparent 1px); |
||||
|
background-size: 20px 20px; |
||||
|
mask-image: linear-gradient(to bottom, black 0%, transparent 100%); |
||||
|
-webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%); |
||||
|
pointer-events: none; |
||||
|
z-index: 0; |
||||
|
} |
||||
|
|
||||
|
> * { |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Hero section |
||||
|
.tb-iot-hub-hero { |
||||
|
text-align: center; |
||||
|
padding: 140px 40px 80px; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
// Animated background gradient |
||||
|
.tb-iot-hub-hero-gradient { |
||||
|
position: absolute; |
||||
|
inset: 0; |
||||
|
transition: background 0.6s ease; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
// Floating icons container per type |
||||
|
.tb-iot-hub-hero-icons { |
||||
|
position: absolute; |
||||
|
inset: 0; |
||||
|
pointer-events: none; |
||||
|
|
||||
|
// Icons start near center (translate toward center, scaled down, invisible) |
||||
|
.tb-iot-hub-hero-float-icon { |
||||
|
opacity: 0; |
||||
|
transform: var(--icon-rotation) translate(var(--start-x), var(--start-y)) scale(0.9); |
||||
|
transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); |
||||
|
} |
||||
|
|
||||
|
// Active: icons move outward to final position, scale to 1, fully visible |
||||
|
&.active .tb-iot-hub-hero-float-icon { |
||||
|
opacity: 1; |
||||
|
transform: var(--icon-rotation) translate(0, 0) scale(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Individual floating icon — final (resting) positions match Figma |
||||
|
.tb-iot-hub-hero-float-icon { |
||||
|
position: absolute; |
||||
|
width: 64px; |
||||
|
height: 64px; |
||||
|
|
||||
|
// Top-left: moves from center-right and center-down to final pos |
||||
|
&.icon-pos-1 { |
||||
|
top: 40px; |
||||
|
left: 23%; |
||||
|
--icon-rotation: rotate(-15deg); |
||||
|
--start-x: 40px; |
||||
|
--start-y: 12px; |
||||
|
} |
||||
|
|
||||
|
// Top-right: moves from center-left and center-down |
||||
|
&.icon-pos-2 { |
||||
|
top: 40px; |
||||
|
right: 23%; |
||||
|
--icon-rotation: rotate(15deg); |
||||
|
--start-x: -40px; |
||||
|
--start-y: 12px; |
||||
|
} |
||||
|
|
||||
|
// Bottom-left: moves from center-right and center-up |
||||
|
&.icon-pos-3 { |
||||
|
bottom: 100px; |
||||
|
left: 18%; |
||||
|
--icon-rotation: rotate(-30deg); |
||||
|
--start-x: 50px; |
||||
|
--start-y: -8px; |
||||
|
} |
||||
|
|
||||
|
// Bottom-right: moves from center-left and center-up |
||||
|
&.icon-pos-4 { |
||||
|
bottom: 100px; |
||||
|
right: 18%; |
||||
|
--icon-rotation: rotate(30deg); |
||||
|
--start-x: -45px; |
||||
|
--start-y: -4px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 40px; |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-title { |
||||
|
font-size: 56px; |
||||
|
font-weight: 500; |
||||
|
line-height: 1.2; |
||||
|
letter-spacing: 0.14px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-subtitle { |
||||
|
font-size: 20px; |
||||
|
line-height: 24px; |
||||
|
letter-spacing: 0.1px; |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
justify-content: center; |
||||
|
gap: 6px; |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-prefix { |
||||
|
font-weight: 400; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-keyword { |
||||
|
font-weight: 600; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
opacity: 0.48; |
||||
|
cursor: pointer; |
||||
|
transition: color 0.3s ease, opacity 0.3s ease; |
||||
|
|
||||
|
&.active { |
||||
|
color: var(--keyword-color); |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
opacity: 0.8; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-search { |
||||
|
width: 600px; |
||||
|
max-width: 100%; |
||||
|
|
||||
|
::ng-deep .mat-mdc-text-field-wrapper { |
||||
|
background: white; |
||||
|
border-radius: 8px; |
||||
|
box-shadow: |
||||
|
0 7px 16px 0 rgba(0, 0, 0, 0.04), |
||||
|
0 29px 29px 0 rgba(0, 0, 0, 0.03), |
||||
|
0 64px 38px 0 rgba(0, 0, 0, 0.02); |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-form-field-subscript-wrapper { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Category cards — Figma: centered 1200px container, flex-col gap=20, pb=48, 3-col rows |
||||
|
.tb-iot-hub-categories { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(3, 1fr); |
||||
|
gap: 20px; |
||||
|
max-width: 1200px; |
||||
|
margin: 0 auto; |
||||
|
padding-bottom: 48px; |
||||
|
} |
||||
|
|
||||
|
// Card — Figma: flex-col, gap=24, pt=24, rounded-8, border, overflow-clip, h=220 |
||||
|
.tb-iot-hub-category-card { |
||||
|
height: 220px; |
||||
|
border-radius: 8px; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
gap: 24px; |
||||
|
padding-top: 24px; |
||||
|
transition: transform 0.2s, box-shadow 0.2s; |
||||
|
border: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
overflow: hidden; |
||||
|
|
||||
|
&:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Title — Figma: 18px Medium, centered, tracking=0.15, flex child |
||||
|
.tb-iot-hub-category-title { |
||||
|
font-size: 18px; |
||||
|
font-weight: 500; |
||||
|
line-height: 24px; |
||||
|
letter-spacing: 0.15px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
text-align: center; |
||||
|
width: 100%; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
// Card image — Figma: h=148, w=full, overflow-clip, fills remaining space |
||||
|
.tb-iot-hub-category-img { |
||||
|
width: 100%; |
||||
|
height: 148px; |
||||
|
object-fit: cover; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
// Exact Figma gradients (104.75deg angle) |
||||
|
.category-widgets { |
||||
|
background: linear-gradient(104.75deg, rgb(240, 255, 245) 0%, rgb(147, 246, 182) 100%); |
||||
|
} |
||||
|
|
||||
|
.category-dashboards { |
||||
|
background: linear-gradient(104.75deg, rgb(245, 246, 255) 0%, rgb(189, 197, 255) 100%); |
||||
|
} |
||||
|
|
||||
|
.category-solutions { |
||||
|
background: linear-gradient(104.75deg, rgb(245, 250, 255) 0%, rgb(149, 200, 255) 100%); |
||||
|
} |
||||
|
|
||||
|
.category-calc-fields { |
||||
|
background: linear-gradient(104.75deg, rgb(245, 252, 255) 0%, rgb(149, 222, 248) 100%); |
||||
|
} |
||||
|
|
||||
|
.category-rule-chains { |
||||
|
background: linear-gradient(104.75deg, rgb(255, 252, 245) 0%, rgb(255, 238, 194) 100%); |
||||
|
} |
||||
|
|
||||
|
.category-devices { |
||||
|
background: linear-gradient(104.75deg, rgb(245, 255, 252) 0%, rgb(147, 240, 213) 100%); |
||||
|
} |
||||
|
|
||||
|
// Sections — matches Figma "items-section" (px=40, gap between sections ~48px) |
||||
|
.tb-iot-hub-section { |
||||
|
padding: 0 40px; |
||||
|
margin-top: 48px; |
||||
|
} |
||||
|
|
||||
|
// Section header — matches Figma "button" (height=40, font-size=20, font-weight=500) |
||||
|
.tb-iot-hub-section-header { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
font-size: 20px; |
||||
|
font-weight: 500; |
||||
|
line-height: 40px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
cursor: pointer; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
mat-icon { |
||||
|
font-size: 20px; |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
color: #00695c; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Big card row — matches Figma "cards-row" (5 cards, gap=20, mt=12 from header) |
||||
|
.tb-iot-hub-big-cards-row { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(5, 1fr); |
||||
|
gap: 20px; |
||||
|
margin-top: 12px; |
||||
|
} |
||||
|
|
||||
|
// Compact card grid — matches Figma "cards" (3 columns, row gap=20, col gap=20) |
||||
|
.tb-iot-hub-compact-cards-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(3, 1fr); |
||||
|
gap: 20px; |
||||
|
margin-top: 12px; |
||||
|
} |
||||
|
|
||||
|
// Divider — matches Figma "divider" (1px, rgba(0,0,0,0.12)) |
||||
|
.tb-iot-hub-divider { |
||||
|
height: 1px; |
||||
|
background: rgba(0, 0, 0, 0.12); |
||||
|
margin: 48px 0 0; |
||||
|
} |
||||
|
|
||||
|
// Become a Creator — matches Figma (pt=64, text centered, button at y=156) |
||||
|
.tb-iot-hub-become-creator { |
||||
|
text-align: center; |
||||
|
padding: 64px 40px; |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 24px; |
||||
|
font-weight: 500; |
||||
|
line-height: 32px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
margin: 0 0 8px; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
margin: 0 0 32px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Loading |
||||
|
.tb-iot-hub-loading { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
padding: 80px 0; |
||||
|
} |
||||
|
|
||||
|
// Page load animation — Figma: Default→appear-first, Smart Animate 1.02s, Gentle easing |
||||
|
// Hero elements fade in + slide DOWN 20px; categories fade in + slide UP 20px |
||||
|
@keyframes iot-hub-fade-slide-down { |
||||
|
from { |
||||
|
opacity: 0; |
||||
|
transform: translateY(-20px); |
||||
|
} |
||||
|
to { |
||||
|
opacity: 1; |
||||
|
transform: translateY(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes iot-hub-fade-slide-up { |
||||
|
from { |
||||
|
opacity: 0; |
||||
|
transform: translateY(20px); |
||||
|
} |
||||
|
to { |
||||
|
opacity: 1; |
||||
|
transform: translateY(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-title, |
||||
|
.tb-iot-hub-hero-subtitle, |
||||
|
.tb-iot-hub-hero-search { |
||||
|
animation: iot-hub-fade-slide-down 1s cubic-bezier(0.19, 1, 0.22, 1) 0.2s both; |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-categories { |
||||
|
animation: iot-hub-fade-slide-up 1s cubic-bezier(0.19, 1, 0.22, 1) 0.2s both; |
||||
|
} |
||||
|
|
||||
|
// Responsive |
||||
|
@media (max-width: 1200px) { |
||||
|
.tb-iot-hub-big-cards-row { |
||||
|
grid-template-columns: repeat(3, 1fr); |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-compact-cards-grid { |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 900px) { |
||||
|
.tb-iot-hub-categories { |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
|
||||
|
.tb-iot-hub-category-card { |
||||
|
height: 180px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-big-cards-row { |
||||
|
grid-template-columns: repeat(2, 1fr); |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero-title { |
||||
|
font-size: 36px; |
||||
|
} |
||||
|
|
||||
|
.tb-iot-hub-hero { |
||||
|
padding: 80px 24px 40px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,222 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2026 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit, OnDestroy } from '@angular/core'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { forkJoin } from 'rxjs'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; |
||||
|
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; |
||||
|
import { IotHubApiService } from '@core/http/iot-hub-api.service'; |
||||
|
import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; |
||||
|
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; |
||||
|
|
||||
|
interface CategoryCard { |
||||
|
type: ItemType; |
||||
|
titleKey: string; |
||||
|
icon: string; |
||||
|
cssClass: string; |
||||
|
image: string; |
||||
|
} |
||||
|
|
||||
|
interface HeroTypeConfig { |
||||
|
type: ItemType; |
||||
|
labelKey: string; |
||||
|
color: string; |
||||
|
gradientColor: string; |
||||
|
icons: string[]; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-iot-hub-home', |
||||
|
standalone: false, |
||||
|
templateUrl: './iot-hub-home.component.html', |
||||
|
styleUrls: ['./iot-hub-home.component.scss'] |
||||
|
}) |
||||
|
export class TbIotHubHomeComponent implements OnInit, OnDestroy { |
||||
|
|
||||
|
readonly ItemType = ItemType; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
heroTypes: HeroTypeConfig[] = [ |
||||
|
{ |
||||
|
type: ItemType.WIDGET, labelKey: 'item.type-widget-plural', color: '#2c9755', |
||||
|
gradientColor: 'rgba(44, 151, 85, 0.1)', |
||||
|
icons: ['assets/iot-hub/hero-widget-icon-1.svg', 'assets/iot-hub/hero-widget-icon-2.svg', 'assets/iot-hub/hero-widget-icon-3.svg', 'assets/iot-hub/hero-widget-icon-4.svg'] |
||||
|
}, |
||||
|
{ |
||||
|
type: ItemType.DASHBOARD, labelKey: 'item.type-dashboard-plural', color: '#4d5fd0', |
||||
|
gradientColor: 'rgba(77, 95, 208, 0.1)', |
||||
|
icons: ['assets/iot-hub/hero-dashboard-icon-1.svg', 'assets/iot-hub/hero-dashboard-icon-2.svg', 'assets/iot-hub/hero-dashboard-icon-3.svg', 'assets/iot-hub/hero-dashboard-icon-4.svg'] |
||||
|
}, |
||||
|
{ |
||||
|
type: ItemType.SOLUTION_TEMPLATE, labelKey: 'item.type-solution-template-plural', color: '#2666a9', |
||||
|
gradientColor: 'rgba(38, 102, 169, 0.1)', icons: [] |
||||
|
}, |
||||
|
{ |
||||
|
type: ItemType.CALCULATED_FIELD, labelKey: 'item.type-calculated-field-plural', color: '#006d92', |
||||
|
gradientColor: 'rgba(0, 109, 146, 0.1)', icons: [] |
||||
|
}, |
||||
|
{ |
||||
|
type: ItemType.RULE_CHAIN, labelKey: 'item.type-rule-chain-plural', color: '#95694b', |
||||
|
gradientColor: 'rgba(149, 105, 75, 0.1)', icons: [] |
||||
|
}, |
||||
|
{ |
||||
|
type: ItemType.DEVICE, labelKey: 'iot-hub.and-devices', color: '#4b8a79', |
||||
|
gradientColor: 'rgba(75, 138, 121, 0.1)', icons: [] |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
activeHeroType: HeroTypeConfig = this.heroTypes[0]; |
||||
|
heroIconsReady = false; |
||||
|
private heroInterval: any; |
||||
|
|
||||
|
categoryCards: CategoryCard[] = [ |
||||
|
{ type: ItemType.WIDGET, titleKey: 'item.type-widget-plural', icon: 'widgets', cssClass: 'category-widgets', image: 'assets/iot-hub/category-widgets-img.svg' }, |
||||
|
{ type: ItemType.DASHBOARD, titleKey: 'item.type-dashboard-plural', icon: 'dashboard', cssClass: 'category-dashboards', image: 'assets/iot-hub/category-dashboards-img.svg' }, |
||||
|
{ type: ItemType.SOLUTION_TEMPLATE, titleKey: 'item.type-solution-template-plural', icon: 'integration_instructions', cssClass: 'category-solutions', image: 'assets/iot-hub/category-solution-templates-img.png' }, |
||||
|
{ type: ItemType.CALCULATED_FIELD, titleKey: 'item.type-calculated-field-plural', icon: 'functions', cssClass: 'category-calc-fields', image: 'assets/iot-hub/category-calculated-fields-img.svg' }, |
||||
|
{ type: ItemType.RULE_CHAIN, titleKey: 'item.type-rule-chain-plural', icon: 'account_tree', cssClass: 'category-rule-chains', image: 'assets/iot-hub/category-rule-chains-img.svg' }, |
||||
|
{ type: ItemType.DEVICE, titleKey: 'iot-hub.device-library', icon: 'memory', cssClass: 'category-devices', image: 'assets/iot-hub/category-device-library-img.svg' } |
||||
|
]; |
||||
|
|
||||
|
popularWidgets: MpItemVersionView[] = []; |
||||
|
popularDashboards: MpItemVersionView[] = []; |
||||
|
popularSolutionTemplates: MpItemVersionView[] = []; |
||||
|
popularCalcFields: MpItemVersionView[] = []; |
||||
|
popularRuleChains: MpItemVersionView[] = []; |
||||
|
|
||||
|
isLoading = true; |
||||
|
|
||||
|
constructor( |
||||
|
private router: Router, |
||||
|
private dialog: MatDialog, |
||||
|
private iotHubApiService: IotHubApiService |
||||
|
) {} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.loadPopularItems(); |
||||
|
// One-tick delay so Angular renders icons in hidden state first, then triggers transition
|
||||
|
requestAnimationFrame(() => { |
||||
|
this.heroIconsReady = true; |
||||
|
this.startHeroCycle(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.stopHeroCycle(); |
||||
|
} |
||||
|
|
||||
|
onHeroTypeHover(config: HeroTypeConfig): void { |
||||
|
this.stopHeroCycle(); |
||||
|
this.activeHeroType = config; |
||||
|
} |
||||
|
|
||||
|
onHeroTypeLeave(): void { |
||||
|
this.startHeroCycle(); |
||||
|
} |
||||
|
|
||||
|
private startHeroCycle(): void { |
||||
|
this.stopHeroCycle(); |
||||
|
this.heroInterval = setInterval(() => { |
||||
|
const idx = this.heroTypes.indexOf(this.activeHeroType); |
||||
|
this.activeHeroType = this.heroTypes[(idx + 1) % this.heroTypes.length]; |
||||
|
}, 3000); |
||||
|
} |
||||
|
|
||||
|
private stopHeroCycle(): void { |
||||
|
if (this.heroInterval) { |
||||
|
clearInterval(this.heroInterval); |
||||
|
this.heroInterval = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onSearch(): void { |
||||
|
if (this.searchText?.trim()) { |
||||
|
this.router.navigate(['/iot-hub/browse'], { queryParams: { search: this.searchText.trim() } }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
navigateToBrowse(type: ItemType): void { |
||||
|
this.router.navigate(['/iot-hub/browse'], { queryParams: { type } }); |
||||
|
} |
||||
|
|
||||
|
navigateToInstalledItems(): void { |
||||
|
this.router.navigate(['/iot-hub/installed']); |
||||
|
} |
||||
|
|
||||
|
openItemDetail(item: MpItemVersionView): void { |
||||
|
this.dialog.open(TbIotHubItemDetailDialogComponent, { |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
autoFocus: false, |
||||
|
data: { |
||||
|
item, |
||||
|
iotHubApiService: this.iotHubApiService |
||||
|
} as IotHubItemDetailDialogData |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
installItem(item: MpItemVersionView): void { |
||||
|
this.dialog.open(TbIotHubInstallDialogComponent, { |
||||
|
panelClass: ['tb-dialog'], |
||||
|
data: { |
||||
|
item, |
||||
|
iotHubApiService: this.iotHubApiService |
||||
|
} as IotHubInstallDialogData |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
navigateToCreator(creatorId: string): void { |
||||
|
this.router.navigate(['/iot-hub/creator', creatorId]); |
||||
|
} |
||||
|
|
||||
|
openSignup(): void { |
||||
|
window.open('https://iothub.thingsboard.io/signup', '_blank'); |
||||
|
} |
||||
|
|
||||
|
private loadPopularItems(): void { |
||||
|
const sortOrder: SortOrder = { property: 'totalInstallCount', direction: Direction.DESC }; |
||||
|
const config = { ignoreLoading: true }; |
||||
|
|
||||
|
const buildQuery = (type: ItemType, size: number): MpItemVersionQuery => { |
||||
|
const pageLink = new PageLink(size, 0, null, sortOrder); |
||||
|
return new MpItemVersionQuery(pageLink, type); |
||||
|
}; |
||||
|
|
||||
|
forkJoin({ |
||||
|
widgets: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.WIDGET, 5), config), |
||||
|
dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, 5), config), |
||||
|
solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, 5), config), |
||||
|
calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, 6), config), |
||||
|
ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, 6), config) |
||||
|
}).subscribe({ |
||||
|
next: (results) => { |
||||
|
this.popularWidgets = results.widgets.data; |
||||
|
this.popularDashboards = results.dashboards.data; |
||||
|
this.popularSolutionTemplates = results.solutionTemplates.data; |
||||
|
this.popularCalcFields = results.calcFields.data; |
||||
|
this.popularRuleChains = results.ruleChains.data; |
||||
|
this.isLoading = false; |
||||
|
}, |
||||
|
error: () => { |
||||
|
this.isLoading = false; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |