Commit f1b39a30 authored by Olivia Madrid's avatar Olivia Madrid

(feat): mobile style, metrics scroller

1 merge request!579WIP: Entity centric metrics (analytics v2)
Pipeline #87715576 failed with stages
in 5 minutes and 43 seconds
......@@ -63,6 +63,7 @@ import { SearchModule } from '../search/search.module';
import { AnalyticsSearchComponent } from './v2/components/search/search.component';
import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsMenuComponent } from './v2/components/menu/menu.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -171,6 +172,7 @@ const routes: Routes = [
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsMenuComponent,
],
providers: [AnalyticsDashboardService],
})
......
......@@ -2,13 +2,13 @@ const categories: Array<any> = [
{
id: 'summary',
label: 'Summary',
type: 'summary',
permissions: ['admin', 'user'],
metrics: [],
},
{
id: 'traffic',
label: 'Traffic',
type: 'chart',
permissions: ['admin', 'user'],
metrics: [
'active_users',
'signups',
......@@ -18,46 +18,52 @@ const categories: Array<any> = [
'retention',
],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
metrics: ['total', 'pageviews', 'active_referrals', 'customers'],
},
{
id: 'engagement',
label: 'Engagement',
type: 'chart',
permissions: ['admin', 'user'],
metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
},
{
id: 'trending',
label: 'Trending',
type: 'table',
permissions: ['admin', 'user'],
metrics: ['top_content', 'top_channels'],
},
{
id: 'referrers',
label: 'Referrers',
type: 'table',
permissions: ['admin', 'user'],
metrics: ['top_referrers'],
},
{
id: 'plus',
label: 'Plus',
type: 'chart',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
},
{
id: 'pro',
label: 'Pro',
type: 'chart',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
},
{
id: 'boost',
label: 'Boost',
type: 'chart',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_tokens'],
},
{
id: 'nodes',
label: 'Nodes',
type: 'chart',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
},
];
......
<div
class="filterWrapper"
[ngClass]="{ expanded: expanded }"
[ngClass]="{ expanded: expanded, isMobile: isMobile }"
(blur)="expanded = false"
>
<div class="filterHeader" (click)="expanded = !expanded">
......
m-analytics__filter {
position: relative;
margin: 16px 16px 0 0;
z-index: 1;
z-index: 2;
}
.filterWrapper {
......@@ -74,7 +74,7 @@ m-analytics__filter {
left: 0px;
@include m-theme() {
border: 1px solid themed($m-blue);
border-top: 1px solid themed($m-grey-100);
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
}
......@@ -83,3 +83,11 @@ m-analytics__filter {
border-bottom-right-radius: 3px;
}
}
.isMobile {
.filterHeader {
i {
display: none;
}
}
}
......@@ -22,6 +22,7 @@ import {
Timespan,
UserState,
} from '../../dashboard.service';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-analytics__filter',
......@@ -32,6 +33,7 @@ export class AnalyticsFilterComponent implements OnInit, OnDestroy {
// TODO: extend Filter interface to allow additional fields (like for timespans?)
@Input() filter: Filter;
isMobile: boolean;
expanded = false;
options: Array<any> = [];
selectedOption: Option;
......@@ -52,6 +54,7 @@ export class AnalyticsFilterComponent implements OnInit, OnDestroy {
this.filter.options[0];
}
});
this.isMobile = isMobileOrTablet();
}
updateFilter(option: Option) {
......
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
m-analytics__menu {
// ----------------------------------------
// MOBILE
.isMobile {
.topbar {
z-index: 99999;
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-800);
}
i {
display: block;
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
}
}
.pageTitle {
font-size: 20px;
margin: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
// display: none;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.expanded {
// display: block;
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
z-index: 999999;
position: fixed;
top: 0;
bottom: 0;
left: -340px;
transition: left 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
padding: 0 24px;
width: 300px;
max-width: 70%;
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
left: 0;
}
.sidebarTitle {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
}
i {
font-size: 20px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
border-radius: 50%;
margin-right: 16px;
}
.details {
& > {
padding: 8px 0;
}
.name {
font-weight: bold;
}
.subscribers {
font-size: 16px;
@include m-theme() {
color: themed($m-grey-300);
}
}
}
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
padding: 6px 0;
a {
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
<section class="menu" [ngClass]="{ isMobile: isMobile }">
<div class="topbar" *ngIf="isMobile">
<i class="material-icons" (click)="expanded = true">menu</i>
<div class="pageTitle">
Analytics
</div>
</div>
<div
class="overlay"
[ngClass]="{ expanded: expanded }"
(click)="expanded = false"
></div>
<div class="sidebar" [ngClass]="{ expanded: expanded }">
<a class="profile" *ngIf="isMobile" [routerLink]="['/', user.username]">
<img
class="avatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/small/' + user.icontime"
/>
<div class="details">
<div class="name">{{ user.name }}</div>
<!-- TODO: get subscriberCount -->
<div class="subscribers">
{{ user.subscribers_count | abbr }} subscribers
</div>
</div>
</a>
<div class="sidebarTitle">
<h3>Analytics</h3>
<i class="material-icons" *ngIf="isMobile" (click)="expanded = false"
>keyboard_arrow_up</i
>
</div>
<div class="catContainer">
<!-- TODO: apply permissions from categories.default to cats sidebar -->
<div class="cat" *ngFor="let cat of cats">
<a
(click)="expanded = false"
[routerLink]="'../' + cat.id"
routerLinkActive="selected"
>{{ cat?.label }}</a
>
</div>
</div>
</div>
</section>
.m-sidebarMarkers__container,
m-v2-topbar {
display: none;
}
m-analytics__menu {
// ----------------------------------------
// MOBILE
.isMobile {
.topbar {
z-index: 99999;
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-800);
}
i {
display: block;
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
}
}
.pageTitle {
font-size: 20px;
margin: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
// display: none;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.expanded {
// display: block;
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
z-index: 999999;
position: fixed;
top: 0;
bottom: 0;
left: -340px;
transition: left 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
padding: 0 24px;
width: 300px;
max-width: 70%;
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
left: 0;
}
.sidebarTitle {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
}
i {
font-size: 20px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
border-radius: 50%;
margin-right: 16px;
}
.details {
& > {
padding: 8px 0;
}
.name {
font-weight: bold;
}
.subscribers {
font-size: 11px;
@include m-theme() {
color: themed($m-grey-300);
}
}
}
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
padding: 6px 0;
a {
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsMenuComponent } from './menu.component';
describe('AnalyticsMenuComponent', () => {
let component: AnalyticsMenuComponent;
let fixture: ComponentFixture<AnalyticsMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMenuComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
ParamMap,
RoutesRecognized,
} from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import {
AnalyticsDashboardService,
Category,
UserState,
} from '../../dashboard.service';
import categories from '../../categories.default';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-analytics__menu',
templateUrl: './menu.component.html',
})
export class AnalyticsMenuComponent implements OnInit {
isMobile: boolean;
expanded: boolean = false;
minds;
user;
cats = categories;
// subscription: Subscription;
// paramsSubscription: Subscription;
// category$ = this.analyticsService.category$;
selectedCat: string;
constructor(
// public client: Client,
public route: ActivatedRoute,
// private router: Router,
// public analyticsService: AnalyticsDashboardService,
// private cd: ChangeDetectorRef
public session: Session
) {}
ngOnInit() {
this.minds = window.Minds;
this.isMobile = isMobileOrTablet();
this.user = this.session.getLoggedInUser();
}
// updateCategory(categoryId) {
// this.analyticsService.updateCategory(categoryId);
// }
// detectChanges() {
// this.cd.markForCheck();
// this.cd.detectChanges();
// }
}
<div class="metricsContainer" *ngIf="metrics$ | async as metrics">
<div
class="metric"
*ngFor="let metric of metrics"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
>
<div class="metricLabel">{{ metric.label }}</div>
<!-- TODO the "number" pipe should be from backend so it can dynamically handle diff decimals/currency formats -->
<div class="metricSummary">
{{ metric.summary.current_value | number }}
</div>
<section class="metricsSection" [ngClass]="{ isMobile: isMobile }">
<div class="overflowFade--left"></div>
<div class="overflowScrollButton--left">
<i class="material-icons">chevron_left</i>
</div>
<div class="metricsWrapper">
<div class="metricsContainer" *ngIf="metrics$ | async as metrics">
<div
class="metric"
*ngFor="let metric of metrics"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
>
<div class="metricLabel">{{ metric.label }}</div>
<!-- TODO the "number" pipe should be from backend so it can dynamically handle diff decimals/currency formats -->
<div class="metricSummary">
{{ metric.summary.current_value | number }}
</div>
<div
class="metricDelta"
[ngClass]="{
goodChange: metric.hasChanged && metric.positiveTrend,
badChange: metric.hasChanged && !metric.positiveTrend
}"
>
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0">arrow_downward</i>
<span>{{ metric.delta | percent: '1.0-1' }}</span>
<div
class="metricDelta"
[ngClass]="{
goodChange: metric.hasChanged && metric.positiveTrend,
badChange: metric.hasChanged && !metric.positiveTrend
}"
>
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0">arrow_downward</i>
<span>{{ metric.delta | percent: '1.0-1' }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="overflowFade--right"></div>
<div class="overflowScrollButton--right">
<i class="material-icons">chevron_right</i>
</div>
</section>
m-analytics__metrics {
.metricsSection {
position: relative;
[class*='overflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 1;
&.overflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.overflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='overflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
transition: all 0.2s ease-in;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
border: 1px solid themed($m-white);
}
&:hover {
@include m-theme() {
border: 1px solid themed($m-blue);
// box-shadow: 0px 0px 5px -3px rgba(themed($m-black-always), 0.5);
}
}
&.overflowScrollButton--right {
right: -12;
}
&.overflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
}
}
.metricsWrapper {
position: relative;
overflow: hidden;
}
.metricsContainer {
scroll-snap-type: x mandatory;
position: relative;
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
width: 95%;
padding: 0 16px;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
// &.metricsContainer::-webkit-scrollbar {
// display: none;
// }
.metric {
scroll-snap-align: start;
flex: 0 0 auto;
width: 20%;
padding: 24px 20px 20px 20px;
font-size: 12px;
......@@ -19,7 +95,7 @@ m-analytics__metrics {
}
&.active {
@include m-theme() {
border-bottom: 5px solid themed($m-blue);
border-bottom: 8px solid themed($m-blue);
}
}
......
......@@ -21,6 +21,7 @@ import {
Timespan,
UserState,
} from '../../dashboard.service';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
interface MetricExtended extends MetricBase {
delta: number;
......@@ -37,9 +38,10 @@ export { MetricExtended as Metric };
export class AnalyticsMetricsComponent implements OnInit, OnDestroy {
data;
subscription: Subscription;
isMobile: boolean;
//TODO: (maybe) interface ViewMetric implements Metric {}
metrics$;
isOverflown = { left: false, right: false };
constructor(private analyticsService: AnalyticsDashboardService) {}
......@@ -69,6 +71,7 @@ export class AnalyticsMetricsComponent implements OnInit, OnDestroy {
return metrics;
})
);
this.isMobile = isMobileOrTablet();
}
updateMetric(metric) {
......@@ -78,4 +81,8 @@ export class AnalyticsMetricsComponent implements OnInit, OnDestroy {
ngOnDestroy() {
// this.subscription.unsubscribe();
}
checkOverflow() {
// element.scrollWidth - element.clientWidth
}
}
......@@ -15,10 +15,11 @@ m-analytics__search {
background-color: themed($m-white);
border: 1px solid rgba(themed($m-black), 0.12);
}
&:placeholder {
&::placeholder {
@include m-theme() {
color: themed($m-grey-200);
font-weight: 200;
font-weight: 400;
letter-spacing: normal;
}
}
}
......@@ -29,10 +30,6 @@ m-analytics__search {
}
}
.m-search-bar--context {
display: none;
}
.mdl-textfield .mdl-textfield__input {
height: 32px;
border-radius: 18px;
......
<div class="page" [ngClass]="{ isMobile: isMobile }">
<section class="sidebar">
<h3>Analytics</h3>
<div *ngIf="isMobile">{{ category$ | async | titlecase }}</div>
<div class="catContainer">
<i class="material-icons">menu</i>
<div class="cat" *ngFor="let cat of cats">
<a [routerLink]="'../' + cat.id" routerLinkActive="selected">{{
cat?.label
}}</a>
</div>
</div>
</section>
<m-analytics__menu></m-analytics__menu>
<!-- TODO: uncomment this -->
<!-- <section class="main" *ngIf="ready$ | async"> -->
<section class="main">
<section class="main" [ngClass]="{ isMobile: isMobile }">
<div class="mainHeader">
<h3 class="selectedCatLabel">
{{ category$ | async | titlecase }}
</h3>
<div class="globalFilters">
<!-- TODO: ngIf for global_filter bool -->
<!-- TODO: *ngIf for global_filter bool -->
<!-- TODO: enable only show to admins -->
<!-- <div *ngIf="session.isAdmin()" class="channelSearch"> -->
<div class="channelSearch">
......
// ----------------------------------------
// MOBILE
.isMobile {
&.page {
padding: 0;
}
.layoutWrapper {
box-shadow: none;
padding: 0;
}
.main {
padding: 0;
}
.m-analytics__menu {
padding: 0 8px;
}
}
// ----------------------------------------
.page {
padding: 16px;
display: flex;
......@@ -13,32 +32,6 @@
}
}
.sidebar {
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
.cat {
padding: 6px 0;
a {
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover {
cursor: pointer;
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
.main {
flex: 4 1 0px;
padding: 16px;
......
......@@ -45,7 +45,7 @@ import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
isMobile: boolean;
cats = categories;
// cats = categories;
subscription: Subscription;
paramsSubscription: Subscription;
......@@ -76,6 +76,7 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
return;
}
this.isMobile = isMobileOrTablet();
this.title.setTitle('Analytics');
this.route.paramMap.subscribe((params: ParamMap) => {
......
......@@ -17,13 +17,12 @@ import { Client } from '../../../services/api/client';
// TEMPORARY
import { HttpClient, HttpHeaders } from '@angular/common/http';
import fakeData from './fake';
export interface Category {
id: string;
label: string;
type?: string; // TODO: probably remove this because it's in visualisations
metrics?: string[]; // TODO: probably remove this too
metrics?: string[]; // TODO: remove this
permissions?: string[];
}
export interface Response {
......@@ -267,6 +266,66 @@ let _state: UserState = {
],
},
},
{
id: 'active_users',
label: 'Active Users2',
permissions: ['admin'],
summary: {
current_value: 120962,
comparison_value: 120962,
comparison_interval: 28,
comparison_positive_inclination: true,
},
visualisation: null,
},
{
id: 'active_users',
label: 'Active Users3',
permissions: ['admin'],
summary: {
current_value: 120962,
comparison_value: 120962,
comparison_interval: 28,
comparison_positive_inclination: true,
},
visualisation: null,
},
{
id: 'active_users',
label: 'Active Users4',
permissions: ['admin'],
summary: {
current_value: 120962,
comparison_value: 120962,
comparison_interval: 28,
comparison_positive_inclination: true,
},
visualisation: null,
},
{
id: 'active_users',
label: 'Active Users5',
permissions: ['admin'],
summary: {
current_value: 120962,
comparison_value: 120962,
comparison_interval: 28,
comparison_positive_inclination: true,
},
visualisation: null,
},
{
id: 'active_users',
label: 'Active Users6',
permissions: ['admin'],
summary: {
current_value: 120962,
comparison_value: 120962,
comparison_interval: 28,
comparison_positive_inclination: true,
},
visualisation: null,
},
],
// filter: ['platform::browser'],
// filters: [],
......
......@@ -2,6 +2,7 @@
position: relative;
padding: 16px;
width: 95%;
max-width: 100%;
@include m-theme() {
border-top: 1px solid themed($m-grey-50);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment