...
 
Commits (40)
......@@ -6,7 +6,7 @@ Minds Front
Front-end web application for Minds. Please run inside of [the Minds repo](https://github.com/minds/minds).
## Documentation
Documentation for Minds can be found at [minds.org/docs](https://www.minds.org/docs)
Please see the documentation on [developers.minds.com](https://developers.minds.com) for instructions on how to [build the Minds Front-end](https://developers.minds.com/docs/guides/frontend).
### Building
Please see the documentation on Minds.org for instructions on how to [build the Minds Front-end](https://www.minds.org/docs/install/preparation.html#front-end).
......
import generateRandomId from '../support/utilities';
context('Newsfeed', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
......@@ -14,7 +16,6 @@ context('Newsfeed', () => {
cy.route('POST', '**/api/v1/media').as('mediaPOST');
cy.route('POST', '**/api/v1/newsfeed/**').as('newsfeedEDIT');
cy.route('POST', '**/api/v1/media/**').as('mediaEDIT');
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', '/newsfeed/subscriptions');
......@@ -37,6 +38,19 @@ context('Newsfeed', () => {
cy.get('minds-newsfeed-poster textarea').type(content);
};
const attachRichEmbed = (embedUrl) => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea')
.type(embedUrl);
cy.route('GET', `**/api/v1/newsfeed/preview?url=${embedUrl}**`)
.as('previewGET')
.wait('@previewGET')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
}
const attachImageToActivity = () => {
cy.uploadFile(
'#attachment-input-poster',
......@@ -511,4 +525,140 @@ context('Newsfeed', () => {
deleteActivityFromNewsfeed();
});
it('should show a rich embed post from youtube in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, click it.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.click();
//check modal is open.
cy.get('[data-cy=data-minds-media-modal]')
.contains(content);
// close modal and tidy.
cy.get('.m-overlay-modal--backdrop')
.click({force: true});
deleteActivityFromNewsfeed();
});
});
it('should not open vimeo in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://vimeo.com/8733915';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('iframe')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
// tidy.
deleteActivityFromNewsfeed();
});
});
it('should not open soundcloud in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://soundcloud.com/richarddjames/piano-un10-it-happened';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
it('should not open spotify in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://open.spotify.com/track/2MZSXhq4XDJWu6coGoXX1V?si=nvja0EfwR3q6GMQmYg6gPQ';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
it('should not open spotify in a modal', () => {
const content = generateRandomId() + " ",
url = 'http://giphygifs.s3.amazonaws.com/media/IzVquL965ib4s/giphy.gif';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
});
This diff is collapsed.
......@@ -115,7 +115,14 @@ import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
import { MiniChartComponent } from './components/mini-chart/mini-chart.component';
import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { PageLayoutComponent } from './components/page-layout/page-layout.component';
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
import { ShadowboxHeaderComponent } from './components/shadowbox-header/shadowbox-header.component';
PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [
......@@ -124,6 +131,7 @@ import { MiniChartComponent } from './components/mini-chart/mini-chart.component
RouterModule,
FormsModule,
ReactiveFormsModule,
PlotlyModule,
],
declarations: [
MINDS_PIPES,
......@@ -220,7 +228,10 @@ import { MiniChartComponent } from './components/mini-chart/mini-chart.component
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
MiniChartComponent,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
ShadowboxHeaderComponent,
],
exports: [
MINDS_PIPES,
......@@ -312,6 +323,10 @@ import { MiniChartComponent } from './components/mini-chart/mini-chart.component
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
],
providers: [
SiteService,
......
export * from './text-input-autocomplete.module';
export {
TextInputAutocompleteMenuComponent,
} from './text-input-autocomplete-menu.component';
export { TextInputAutocompleteMenuComponent } from './text-input-autocomplete-menu.component';
import { Component, EventEmitter } from '@angular/core';
import { Client } from '../../../services/api';
import { UserAvatarService } from '../../services/user-avatar.service';
import { of, Observable } from 'rxjs';
@Component({
selector: 'minds-avatar',
......@@ -14,17 +14,28 @@ import { Client } from '../../../services/api';
],
outputs: ['added'],
template: `
<div class="minds-avatar" [style.background-image]="'url(' + src + ')'">
<div
class="minds-avatar"
[ngStyle]="{ 'background-image': 'url(' + (getSrc() | async) + ')' }"
>
<img
*ngIf="!src"
*ngIf="!(userAvatarService.src$ | async)"
src="{{ minds.cdn_assets_url }}assets/avatars/blue/default-large.png"
class="mdl-shadow--4dp"
/>
<div *ngIf="editing" class="overlay">
<i class="material-icons">{{ icon }}</i>
<ng-container *ngIf="showPrompt">
<span *ngIf="src" i18n="@@COMMON__AVATAR__CHANGE">Change avatar</span>
<span *ngIf="!src" i18n="@@COMMON__AVATAR__ADD">Add an avatar</span>
<span
*ngIf="userAvatarService.src$ | async"
i18n="@@COMMON__AVATAR__CHANGE"
>Change avatar</span
>
<span
*ngIf="!(userAvatarService.src$ | async)"
i18n="@@COMMON__AVATAR__ADD"
>Add an avatar</span
>
</ng-container>
</div>
<input *ngIf="editing" type="file" #file (change)="add($event)" />
......@@ -40,18 +51,21 @@ export class MindsAvatar {
index: number = 0;
icon: string = 'camera';
showPrompt: boolean = true;
file: any;
added: EventEmitter<any> = new EventEmitter();
constructor(public userAvatarService: UserAvatarService) {}
set _object(value: any) {
if (!value) return;
value.icontime = value.icontime ? value.icontime : '';
this.object = value;
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
if (this.object.type === 'user')
if (this.object.type !== 'user') {
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
} else if (this.object.guid !== this.minds.user.guid) {
this.src = `${this.minds.cdn_url}icon/${this.object.guid}/large/${this.object.icontime}`;
}
}
set _src(value: any) {
......@@ -63,6 +77,10 @@ export class MindsAvatar {
if (!this.editing && this.file) this.done();
}
/**
* New avatar added.
* @param e - the element.
*/
add(e) {
if (!this.editing) return;
......@@ -78,6 +96,9 @@ export class MindsAvatar {
typeof reader.result === 'string'
? reader.result
: reader.result.toString();
if (this.object.type === 'user' && this.isOwnerAvatar()) {
this.userAvatarService.src$.next(this.src);
}
};
reader.readAsDataURL(this.file);
......@@ -87,9 +108,28 @@ export class MindsAvatar {
if (this.waitForDoneSignal !== true) this.done();
}
/**
* Called upon being done.
*/
done() {
console.log('sending done');
this.added.next(this.file);
this.file = null;
}
/**
* Gets the src of the image
* @returns { Observables<string> } the src for the image.
*/
getSrc(): Observable<string> {
return this.isOwnerAvatar() ? this.userAvatarService.src$ : of(this.src);
}
/**
* Determined whether this is a users avatar.
* @returns true if the object guid matches the currently logged in user guid
*/
isOwnerAvatar(): boolean {
return this.object.guid === this.minds.user.guid;
}
}
......@@ -43,8 +43,8 @@ export class ChannelBadgesComponent {
return true;
} else if (
!this.user.is_admin &&
(this.session.isAdmin() &&
this.user.guid !== this.session.getLoggedInUser().guid)
this.session.isAdmin() &&
this.user.guid !== this.session.getLoggedInUser().guid
) {
return true;
}
......
<!-- <div
<div
#chartContainer
class="m-chartV2__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
[ngClass]="{ isTouchDevice: isTouchDevice, isMini: isMini }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
......@@ -14,48 +12,70 @@
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(plotly_click)="onClick($event)"
id="graphDiv"
>
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="m-chartV2__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
<div #hoverInfoDiv class="m-chartV2__hoverInfoDiv">
<i
*ngIf="isTouchDevice"
class="material-icons m-chartV2__hoverInfo__closeBtn"
(click)="onUnhover($event)"
>close</i
>
<div class="m-chartV2__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-chartV2__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
<div class="m-chartV2__hoverInfo__wrapper">
<div class="m-chartV2__hoverInfo__arrowContainer" *ngIf="isMini">
<i class="material-icons">arrow_upward</i>
</div>
<div class="m-chartV2__hoverInfo__rowsContainer">
<div class="m-chartV2__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="rawData?.unit"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number: '1.0-0' | abbr }}
{{ rawData.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchCase="eth">
{{ hoverInfo.value | number: '1.3-3' }} ETH
</ng-template>
<ng-template ngSwitchCase="tokens">
{{ hoverInfo.value | number: '1.1-3' }} Tokens
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.0-3' }} {{ rawData?.unit }}
</ng-template>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="rawData?.unit"
class="m-chartV2__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number: '1.0-0' | abbr }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchCase="eth">
{{ hoverInfo.value | number: '1.3-3' }} ETH
</ng-template>
<ng-template ngSwitchCase="tokens">
{{ hoverInfo.value | number: '1.1-3' }} Tokens
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
</div>
</div> -->
</div>
m-chartV2 {
display: block;
position: relative;
margin-left: 40px;
}
.js-plotly-plot,
.plot-container {
height: 44vh;
min-height: 44vh;
display: block;
}
#graphDiv {
display: block;
position: relative;
g,
g > * {
cursor: default;
}
> * {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1),
color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.main-svg {
max-width: 100%;
}
}
.m-chartV2__hoverInfoDiv {
width: 160px;
padding: 12px;
position: absolute;
pointer-events: none;
border-radius: 3px;
font-size: 12px;
z-index: 9999999999;
opacity: 0;
transition: opacity 0.2s ease-in;
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0 0 4px rgba(themed($m-black), 0.3);
color: themed($m-grey-300);
}
[class*='m-chartV2__hoverInfo__row'] {
padding-bottom: 4px;
font-weight: 300;
&:last-of-type {
padding-top: 2px;
}
}
.m-chartV2__hoverInfo__row--primary {
font-weight: 400;
font-size: 15px;
@include m-theme() {
color: themed($m-grey-600);
}
}
.m-chartV2__hoverInfo__closeBtn {
display: none;
font-size: 15px;
position: absolute;
cursor: pointer;
top: 10px;
right: 10px;
transition: color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
color: themed($m-grey-300);
}
&:active {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
}
// ----------------------------------------------------
.isTouchDevice .m-chartV2__hoverInfoDiv .m-chartV2__hoverInfo__closeBtn {
display: block;
}
@media screen and (max-width: $min-tablet) {
m-chartV2 {
margin-left: 16px;
}
}
// ----------------------------------------------------
m-chartV2.isMini {
margin-left: 0;
margin-top: 24px;
.js-plotly-plot,
.plot-container {
height: 40px;
min-height: 40px;
}
.m-chartV2__chartContainer {
// margin-right: 24px;
}
.m-chartV2__hoverInfoDiv {
width: 150px;
padding: 0px;
.m-chartV2__hoverInfo__wrapper {
display: flex;
}
.m-chartV2__hoverInfo__rowsContainer {
display: flex;
flex-direction: column;
padding: 14px 14px 14px 0;
}
.m-chartV2__hoverInfo__arrowContainer {
width: 20px;
i {
margin-left: -4px;
transform: rotate(-45deg) scaleX(0.5);
@include m-theme() {
color: themed($m-grey-600);
}
}
}
[class*='m-chartV2__hoverInfo__row'] {
line-height: 1.1;
}
.m-chartV2__hoverInfo__row--primary {
font-size: 12px;
}
}
@media screen and (max-width: $min-tablet) {
margin-left: 0;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { UtcDatePipe } from '../../pipes/utcdate';
import { AbbrPipe } from '../../pipes/abbr';
import { MockService } from '../../../utils/mock';
import { ThemeService } from '../../services/theme.service';
import { ChartV2Component } from './chart-v2.component';
describe('ChartV2Component', () => {
......@@ -8,14 +12,46 @@ describe('ChartV2Component', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ChartV2Component],
declarations: [ChartV2Component, UtcDatePipe, AbbrPipe],
providers: [
{
provide: ThemeService,
useValue: MockService(ThemeService),
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChartV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
component.rawData = {
id: 'views',
label: 'Pageviews',
permissions: ['admin', 'user'],
unit: 'usd',
description: '',
visualisation: {
type: 'chart',
segments: [
{
buckets: [
{
key: 1567296000000,
date: '2019-09-01T00:00:00+00:00',
value: 11,
},
{
key: 1567382400000,
date: '2019-09-02T00:00:00+00:00',
value: 12,
},
],
},
],
},
};
});
it('should create', () => {
......
<div class="m-dashboardLayout__header">
<ng-content select="[m-dashboardLayout__header]"></ng-content>
</div>
<div class="m-dashboardLayout__body">
<ng-content select="[m-dashboardLayout__body]"></ng-content>
</div>
m-dashboardLayout {
display: block;
width: 100%;
max-width: 100%;
}
.m-dashboardLayout__header {
h3 {
font-size: 26px;
font-weight: 500;
margin-top: 0;
margin-right: 24px;
}
}
.m-dashboardLayout__body {
position: relative;
display: block;
width: 100%;
}
@media screen and (max-width: $min-tablet) {
m-dashboardLayout {
display: block;
padding: 0;
max-width: none;
width: 100%;
}
.m-dashboardLayout__header {
padding-left: 24px;
}
}
@media screen and (max-width: $max-mobile) {
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MiniChartComponent } from './mini-chart.component';
import { DashboardLayoutComponent } from './dashboard-layout.component';
describe('MiniChartComponent', () => {
let component: MiniChartComponent;
let fixture: ComponentFixture<MiniChartComponent>;
describe('DashboardLayoutComponent', () => {
let component: DashboardLayoutComponent;
let fixture: ComponentFixture<DashboardLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MiniChartComponent],
declarations: [DashboardLayoutComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MiniChartComponent);
fixture = TestBed.createComponent(DashboardLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
......
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-miniChart',
templateUrl: './mini-chart.component.html',
selector: 'm-dashboardLayout',
templateUrl: './dashboard-layout.component.html',
})
export class MiniChartComponent implements OnInit {
export class DashboardLayoutComponent implements OnInit {
constructor() {}
ngOnInit() {}
......
......@@ -79,9 +79,7 @@ export class FeaturedContentComponent implements OnInit {
component
);
const componentRef: ComponentRef<
any
> = this.dynamicHost.viewContainerRef.createComponent(
const componentRef: ComponentRef<any> = this.dynamicHost.viewContainerRef.createComponent(
componentFactory,
void 0,
this.injector
......
import { Injectable } from '@angular/core';
import {
filter,
first,
map,
switchMap,
mergeMap,
skip,
take,
} from 'rxjs/operators';
import { filter, first, switchMap, mergeMap, skip, take } from 'rxjs/operators';
import { FeedsService } from '../../services/feeds.service';
import { Subscription } from 'rxjs';
@Injectable()
export class FeaturedContentService {
offset: number = -1;
offset = 0;
maximumOffset = 0;
feedLength = 0;
protected feedSubscription: Subscription;
constructor(protected feedsService: FeedsService) {
this.onInit();
}
onInit() {
this.feedSubscription = this.feedsService.feed.subscribe(feed => {
this.feedLength = feed.length;
this.maximumOffset = this.feedLength - 1;
});
this.feedsService
.setLimit(12)
.setOffset(0)
......@@ -23,28 +28,36 @@ export class FeaturedContentService {
}
async fetch() {
if (this.offset >= this.feedsService.rawFeed.getValue().length) {
this.offset = -1;
}
// Refetch every 2 calls, if not loading
if (this.offset % 2 && !this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
return await this.feedsService.feed
.pipe(
filter(feed => feed.length > 0),
first(),
mergeMap(feed => feed),
filter(entities => entities.length > 0),
mergeMap(feed => feed), // Convert feed array to stream
skip(this.offset++),
take(1),
switchMap(async entity => {
if (!entity) {
return false;
} else {
const resolvedEntity = await entity.pipe(first()).toPromise();
this.resetOffsetAtEndOfStream();
return resolvedEntity;
}
return await entity.pipe(first()).toPromise();
})
)
.toPromise();
}
protected resetOffsetAtEndOfStream() {
if (this.offset >= this.maximumOffset) {
this.offset = 0;
this.fetchNextFeed();
}
}
protected fetchNextFeed() {
if (!this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
}
}
<p>
mini-chart works!
</p>
......@@ -22,7 +22,7 @@ export class NSFWSelectorComponent {
@Input('service') serviceRef: string = 'consumer';
@Input('consumer') consumer: false;
@Input('expanded') expanded: false;
@Output('selected') onSelected: EventEmitter<any> = new EventEmitter();
@Output('selectedChange') onSelected: EventEmitter<any> = new EventEmitter();
constructor(
public creatorService: NSFWSelectorCreatorService,
......@@ -33,7 +33,9 @@ export class NSFWSelectorComponent {
ngOnInit() {
if (this.service.reasons) {
this.service.reasons.map(r => this.toggle(r.value));
for (const reason of this.service.reasons) {
this.toggle(reason.value, false);
}
}
}
......@@ -64,14 +66,17 @@ export class NSFWSelectorComponent {
}
}
toggle(reason) {
toggle(reason, triggerChange = true) {
if (reason.locked) {
return;
}
this.service.toggle(reason);
const reasons = this.service.reasons.filter(r => r.selected);
this.onSelected.next(reasons);
if (triggerChange) {
const reasons = this.service.reasons.filter(r => r.selected);
this.onSelected.next(reasons);
}
}
hasSelections(): boolean {
......
<m-sidebarMenu [catId]="navId"></m-sidebarMenu>
<section class="m-pageLayout__main">
<ng-content select="[m-pageLayout__main]"></ng-content>
</section>
m-pageLayout {
display: block;
position: relative;
width: 100%;
padding-top: 56px;
margin-bottom: 48px;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
.m-tooltip {
margin-left: 4px;
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
}
}
}
m-sidebarMenu {
display: block;
box-sizing: border-box;
padding-left: 105px;
width: 245px;
@include m-theme() {
background-color: themed($m-white);
}
}
.m-pageLayout__main {
margin-left: 350px;
margin-right: 24px;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
}
@media screen and (max-width: $min-tablet) {
.m-pageLayout__main {
display: block;
margin: 0;
}
m-sidebarMenu {
margin-left: 0;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { PageLayoutComponent } from './page-layout.component';
@Component({
selector: 'm-sidebarMenu',
template: '',
})
class SidebarMenuComponentMock {
@Input() catId;
}
describe('PageLayoutComponent', () => {
let component: PageLayoutComponent;
let fixture: ComponentFixture<PageLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PageLayoutComponent, SidebarMenuComponentMock],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PageLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-pageLayout',
templateUrl: './page-layout.component.html',
})
export class PageLayoutComponent implements OnInit {
@Input() navId: string;
constructor() {}
ngOnInit() {}
}
......@@ -231,7 +231,7 @@
<m-nsfw-selector
service="editing"
[selected]="entity.nsfw"
(selected)="onNSFWSelected($event)"
(selectedChange)="onNSFWSelected($event)"
>
</m-nsfw-selector>
</li>
......
......@@ -55,7 +55,7 @@
[href]="src.perma_url"
target="_blank"
rel="noopener noreferrer"
class="meta mdl-color-text--blue-grey-900"
class="meta"
[ngClass]="{ 'm-rich-embed-has-thumbnail': src.thumbnail_src, 'm-rich-embed--title--no-padding': hasInlineContentLoaded() }"
>
<h2
......@@ -74,10 +74,7 @@
<a class="thumbnail" *ngIf="preview.thumbnail">
<img src="{{preview.thumbnail}}" />
</a>
<a
class="meta mdl-color-text--blue-grey-900"
[ngClass]="{ 'm-has-thumbnail': preview.thumbnail }"
>
<a class="meta" [ngClass]="{ 'm-has-thumbnail': preview.thumbnail }">
<h2 class="m-rich-embed--title mdl-card__title-text">
{{preview.title | excerpt}}
</h2>
......
......@@ -16,9 +16,6 @@ minds-rich-embed {
left: 0;
width: 100%;
height: 100%;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.2);
}
&:hover {
background: transparent;
......
......@@ -3,11 +3,14 @@ import {
ElementRef,
ChangeDetectorRef,
ChangeDetectionStrategy,
Output,
EventEmitter,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { RichEmbedService } from '../../../services/rich-embed';
import mediaProxyUrl from '../../../helpers/media-proxy-url';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
......@@ -17,18 +20,22 @@ import mediaProxyUrl from '../../../helpers/media-proxy-url';
})
export class MindsRichEmbed {
type: string = '';
mediaSource: string = '';
src: any = {};
preview: any = {};
maxheight: number = 320;
inlineEmbed: any = null;
embeddedInline: boolean = false;
cropImage: boolean = false;
modalRequestSubscribed: boolean = false;
@Output() mediaModalRequested: EventEmitter<any> = new EventEmitter();
private lastInlineEmbedParsed: string;
constructor(
private sanitizer: DomSanitizer,
private service: RichEmbedService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
protected featureService: FeaturesService
) {}
set _src(value: any) {
......@@ -65,6 +72,14 @@ export class MindsRichEmbed {
// Inline Embedding
let inlineEmbed = this.parseInlineEmbed(this.inlineEmbed);
if (
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
this.modalRequestSubscribed =
this.mediaModalRequested.observers.length > 0;
}
if (
inlineEmbed &&
inlineEmbed.id &&
......@@ -80,9 +95,35 @@ export class MindsRichEmbed {
}
this.inlineEmbed = inlineEmbed;
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
if (this.inlineEmbed && this.inlineEmbed.htmlProvisioner) {
this.inlineEmbed.htmlProvisioner().then(html => {
this.inlineEmbed.html = html;
this.detectChanges();
});
// @todo: catch any error here and forcefully window.open to destination
}
}
}
action($event) {
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
$event.preventDefault();
$event.stopPropagation();
this.mediaModalRequested.emit();
return;
}
if (this.inlineEmbed && !this.embeddedInline) {
$event.preventDefault();
$event.stopPropagation();
......@@ -120,6 +161,7 @@ export class MindsRichEmbed {
if ((matches = youtube.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'youtube';
return {
id: `video-youtube-${matches[1]}`,
className:
......@@ -138,12 +180,13 @@ export class MindsRichEmbed {
if ((matches = vimeo.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'vimeo';
return {
id: `video-vimeo-${matches[1]}`,
className:
'm-rich-embed-video m-rich-embed-video-iframe m-rich-embed-video-vimeo',
html: this.sanitizer.bypassSecurityTrustHtml(`<iframe
src="https://player.vimeo.com/video/${matches[1]}?autoplay=1&title=0&byline=0&portrait=0"
src="https://player.vimeo.com/video/${matches[1]}?title=0&byline=0&portrait=0"
frameborder="0"
webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>`),
playable: true,
......@@ -156,6 +199,7 @@ export class MindsRichEmbed {
if ((matches = soundcloud.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'soundcloud';
return {
id: `audio-soundcloud-${matches[1]}`,
className:
......@@ -183,6 +227,7 @@ export class MindsRichEmbed {
if ((matches = spotify.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'spotify';
return {
id: `audio-spotify-${matches[1]}`,
className:
......@@ -207,7 +252,7 @@ export class MindsRichEmbed {
if (!id) {
return null;
}
this.mediaSource = 'giphy';
return {
id: `image-giphy-${matches[1]}`,
className:
......@@ -225,7 +270,11 @@ export class MindsRichEmbed {
}
hasInlineContentLoaded() {
return this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
return this.featureService.has('media-modal')
? !this.modalRequestSubscribed &&
this.inlineEmbed &&
this.inlineEmbed.html
: this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
}
detectChanges() {
......
<section class="m-shadowboxHeader__section">
<div class="m-shadowboxHeader__wrapper">
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-shadowboxHeader__overflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-shadowboxHeader__overflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
</ng-container>
<div
#shadowboxHeaderContainer
class="m-shadowboxHeader__container disable-scrollbars"
(scroll)="onScroll($event)"
>
<ng-content select=".m-shadowboxLayout__header"></ng-content>
</div>
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-shadowboxHeader__overflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-shadowboxHeader__overflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</ng-container>
</div>
</section>
m-shadowboxHeader {
min-height: 116px;
display: block;
}
.m-shadowboxHeader__section {
position: relative;
}
.m-shadowboxHeader__wrapper {
position: relative;
z-index: 1;
height: 124px;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
}
.m-shadowboxHeader__container {
overflow-x: hidden;
overflow-y: hidden;
// display: flex;
// flex-wrap: nowrap;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&.disable-scrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
width: 0px;
background: transparent; /* Chrome/Safari/Webkit */
}
}
.m-tooltip--bubble {
width: 160px;
}
}
[class*='m-shadowboxHeader__overflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 2;
&.m-shadowboxHeader__overflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.m-shadowboxHeader__overflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='m-shadowboxHeader__overflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
cursor: pointer;
&.showButton {
opacity: 1;
}
@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);
}
}
&.m-shadowboxHeader__overflowScrollButton--right {
right: -12;
}
&.m-shadowboxHeader__overflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
@media screen and (max-width: $min-tablet) {
.m-shadowboxHeader__section {
[class*='m-shadowboxHeader__overflowScrollButton--'] {
display: none;
}
.m-shadowboxHeader__container {
overflow-x: scroll;
scroll-snap-type: x mandatory;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
}
}
.m-shadowboxHeader__wrapper {
@include m-theme() {
box-shadow: none;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component,
Input,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
import { ShadowboxHeaderComponent } from './shadowbox-header.component';
describe('ShadowboxHeaderComponent', () => {
let component: ShadowboxHeaderComponent;
let fixture: ComponentFixture<ShadowboxHeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxHeaderComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
Input,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
@Component({
selector: 'm-shadowboxHeader',
templateUrl: './shadowbox-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShadowboxHeaderComponent implements AfterViewInit {
@Input() isScrollable: boolean = true;
@Input() metricActivated;
@ViewChild('shadowboxHeaderContainer', { static: false })
shadowboxHeaderContainerEl: ElementRef;
shadowboxHeaderContainer;
childClientWidth: number;
faderWidth = 24;
isOverflown: boolean = false;
isAtScrollEnd = false;
isAtScrollStart = true;
showButton = { left: false, right: false };
constructor(private cd: ChangeDetectorRef) {}
ngAfterViewInit() {
this.checkOverflow();
// const activeMetric = ;//get the index of the metric with .active
// this.slideToActiveMetric();
}
// updateMetric(metric) {
// // TODO: if clicked metric is not fully visible, slide() until it is
// this.analyticsService.updateMetric(metric.id);
// }
// ----------------------------------------------------
@HostListener('click', ['$event.target'])
onClick(target) {
console.log('***Clicked on: ', target);
// this.slideToActiveMetric(metricIndex);
}
slideToActiveMetric(metricIndex) {
// TODOOJM
}
// ----------------------------------------------------
@HostListener('window:resize')
onResize() {
this.checkOverflow();
}
onScroll($event) {
this.checkOverflow();
}
checkOverflow() {
if (!this.isScrollable) {
return;
}
const firstMetric = <HTMLElement>(
document.querySelector('.m-shadowboxLayout__headerItem')
);
// TODO: figure out how to avoid test failure "Cannot read property 'clientWidth' of null"
this.childClientWidth = firstMetric ? firstMetric.clientWidth : 160;
this.shadowboxHeaderContainer = this.shadowboxHeaderContainerEl.nativeElement;
this.isOverflown =
this.shadowboxHeaderContainer.scrollWidth -
this.shadowboxHeaderContainer.clientWidth >
0;
this.isAtScrollStart =
this.shadowboxHeaderContainer.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.shadowboxHeaderContainer.scrollWidth -
(this.shadowboxHeaderContainer.scrollLeft +
this.shadowboxHeaderContainer.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.shadowboxHeaderContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.shadowboxHeaderContainer.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.shadowboxHeaderContainer.clientWidth % this.childClientWidth;
const completelyVisibleMetricsWidth =
this.shadowboxHeaderContainer.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
currentScrollLeft = this.faderWidth;
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.shadowboxHeaderContainer.scrollWidth -
completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
scrollEndOffset = partiallyVisibleMetricWidth - this.faderWidth;
}
targetScrollLeft = Math.max(
currentScrollLeft - completelyVisibleMetricsWidth + scrollEndOffset,
0
);
}
this.shadowboxHeaderContainer.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<m-shadowboxHeader
*ngIf="hasHeader"
[isScrollable]="scrollableHeader"
[ngClass]="{ isScrollable: scrollableHeader }"
><ng-content
select=".m-shadowboxLayout__header"
ngProjectAs=".m-shadowboxLayout__header"
></ng-content
></m-shadowboxHeader>
<div class="m-shadowboxLayout__bottom">
<ng-content select=".m-shadowboxLayout__body"></ng-content>
<ng-content select=".m-shadowboxLayout__footer"></ng-content>
</div>
m-shadowboxLayout {
display: block;
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
m-shadowboxHeader.isScrollable {
.m-shadowboxLayout__header {
display: flex;
flex-flow: row nowrap;
}
}
.m-shadowboxLayout__bottom {
position: relative;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
background-color: themed($m-white);
}
}
.m-shadowboxLayout__footer {
min-height: 104px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
width: 100%;
@include m-theme() {
background-color: rgba(themed($m-grey-50), 0.25);
border-top: 1px solid rgba(themed($m-grey-50), 0.6);
}
> * {
margin: 30px 68px 30px 0;
}
}
@media screen and (max-width: $min-tablet) {
m-shadowboxLayout {
@include m-theme() {
box-shadow: none;
}
}
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top: 1px solid themed($m-grey-100);
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { ShadowboxLayoutComponent } from './shadowbox-layout.component';
@Component({
selector: 'm-shadowboxHeader',
template: '',
})
class ShadowboxHeaderComponentMock {
@Input() isScrollable;
}
describe('ShadowboxLayoutComponent', () => {
let component: ShadowboxLayoutComponent;
let fixture: ComponentFixture<ShadowboxLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxLayoutComponent, ShadowboxHeaderComponentMock],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-shadowboxLayout',
templateUrl: './shadowbox-layout.component.html',
})
export class ShadowboxLayoutComponent implements OnInit {
@Input() scrollableHeader: boolean = true;
@Input() hasHeader: boolean = true;
constructor() {}
ngOnInit() {}
}
const sidebarMenuCategories = [
{
header: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
links: [
{
id: 'summary',
label: 'Summary',
permissions: ['admin'],
},
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
// path: '/some/path/outside/header/path',
},
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
},
];
export default sidebarMenuCategories;
<section class="m-sidebarMenu">
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarCatLabel" *ngIf="activeCat">
{{ activeCat.category.label }}
</div>
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarHeader">
{{ cat.header.label }}
</div>
<div
class="m-sidebarMenu__overlay"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
(click)="mobileMenuExpanded = false"
></div>
<div
class="m-sidebarMenu__sidebar"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
>
<a class="m-sidebarMenu__userWrapper" [routerLink]="['/', user.username]">
<img
class="m-sidebarMenu__userAvatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/small/' + user.icontime"
/>
<div class="m-sidebarMenu__userDetails">
<div class="m-sidebarMenu__userDetails__name">{{ user.name }}</div>
<div class="m-sidebarMenu__userDetails__username">
@{{ user.username }}
</div>
<!-- TODO: get subscriberCount and remove username -->
<!-- <div class="m-sidebarMenu__userDetails__subscribers">
</div>
<div
class="m-sidebarMenu__overlay"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
(click)="mobileMenuExpanded = false"
></div>
<div
class="m-sidebarMenu__sidebar"
[ngClass]="{ mobileMenuExpanded: mobileMenuExpanded }"
>
<a class="m-sidebarMenu__userWrapper" [routerLink]="['/', user.username]">
<img
class="m-sidebarMenu__userAvatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/medium/' + user.icontime"
/>
<div class="m-sidebarMenu__userDetails">
<div class="m-sidebarMenu__userDetails__name">{{ user.name }}</div>
<div class="m-sidebarMenu__userDetails__username">
@{{ user.username }}
</div>
<!-- TODO: get subscriberCount and remove username -->
<!-- <div class="m-sidebarMenu__userDetails__subscribers">
{{ user.subscribers_count | abbr }} subscribers
</div> -->
</div>
</a>
<ng-container *ngFor="let cat of cats">
<div
class="m-sidebarMenu__catContainer"
*ngIf="cat.category.permissionGranted"
[ngClass]="{ expanded: cat.category.expanded }"
>
<div class="m-sidebarMenu__catLabel">
<h3>{{ cat.category.label }}</h3>
<i
class="material-icons"
*ngIf="cat.category.expanded && cat.subcategories"
(click)="cat.category.expanded = false"
>keyboard_arrow_up</i
>
<i
class="material-icons"
*ngIf="!cat.category.expanded && cat.subcategories"
(click)="cat.category.expanded = true"
>keyboard_arrow_down</i
>
</div>
</div>
</a>
<!-- <ng-container *ngFor="let cat of cats"> -->
<div
class="m-sidebarMenu__catContainer expanded"
*ngIf="cat.header.permissionGranted"
>
<!-- [ngClass]="{ expanded: cat.header.expanded }" -->
<div class="m-sidebarMenu__header">
<h3>{{ cat.header.label }}</h3>
<!-- <i
class="material-icons"
*ngIf="cat.header.expanded && cat.links"
(click)="cat.header.expanded = false"
>keyboard_arrow_up</i
> -->
<!-- <i class="material-icons" *ngIf="!cat.header.expanded && cat.links"
>keyboard_arrow_down</i
> -->
<!-- (click)="cat.header.expanded = true" -->
</div>
<div class="m-sidebarMenu__subcatContainer" *ngIf="cat.subcategories">
<div
class="m-sidebarMenu__subcat"
*ngFor="let subcat of cat.subcategories"
>
<a
*ngIf="subcat.permissionGranted"
class="m-sidebarMenu__subcatLabel"
(click)="mobileMenuExpanded = false"
[routerLink]="'../' + subcat.id"
routerLinkActive="selected"
>{{ subcat.label }}</a
>
</div>
</div>
<nav class="m-sidebarMenu__linksContainer" *ngIf="cat.links">
<div class="m-sidebarMenu__link" *ngFor="let link of cat.links">
<a
*ngIf="link.permissionGranted"
(click)="mobileMenuExpanded = false"
[routerLink]="link.path ? '/' + link.path : '../' + link.id"
routerLinkActive="selected"
>{{ link.label }}</a
>
</div>
</ng-container>
</nav>
</div>
</section>
<!-- </ng-container> -->
</div>
m-sidebarMenu {
display: block;
// min-width: 180px;
// padding: 16px 16px 16px 80px;
// flex: 1 1 0px;
.m-sidebarMenu {
padding: 16px 16px 16px 80px;
}
i {
display: none;
cursor: pointer;
......@@ -15,31 +8,35 @@ m-sidebarMenu {
display: none;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
display: block;
cursor: pointer;
}
}
// .m-sidebarMenu__sidebar {
// position: relative;
// position: -webkit-sticky;
// position: sticky;
// top: 0;
// }
.m-sidebarMenu__catLabel {
.m-sidebarMenu__sidebar {
position: fixed;
top: 157px;
}
.m-sidebarMenu__header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin-top: 0;
font-size: 26px;
font-weight: 500;
}
i {
display: none;
}
}
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
cursor: pointer;
display: none;
.m-sidebarMenu__subcat {
.m-sidebarMenu__link {
a {
display: block;
padding: 8px 0;
......@@ -63,12 +60,9 @@ m-sidebarMenu {
// --------------------------------------------------
@media screen and (max-width: $min-tablet) {
m-sidebarMenu {
margin-right: -32px;
// margin-right: -32px;
flex: 0 1 0px;
padding: 0;
.m-sidebarMenu {
padding: 0 8px;
}
.m-sidebarMenu__topbar {
display: block;
......@@ -94,7 +88,7 @@ m-sidebarMenu {
color: themed($m-grey-300);
}
}
.m-sidebarMenu__topbarCatLabel {
.m-sidebarMenu__topbarHeader {
font-size: 20px;
margin: 0 0 0 -24px;
min-height: 20px;
......@@ -134,13 +128,13 @@ m-sidebarMenu {
left: 0;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
display: none;
}
&.expanded {
.m-sidebarMenu__subcatContainer {
.m-sidebarMenu__linksContainer {
display: block;
.m-sidebarMenu__subcat {
.m-sidebarMenu__link {
a {
padding: 6px 0;
}
......@@ -148,7 +142,7 @@ m-sidebarMenu {
}
}
}
.m-sidebarMenu__catLabel {
.m-sidebarMenu__header {
display: flex;
justify-content: space-between;
align-items: center;
......@@ -176,6 +170,9 @@ m-sidebarMenu {
.m-sidebarMenu__userAvatar {
border-radius: 50%;
margin-right: 16px;
height: 40px;
width: 40px;
object-fit: contain;
}
.m-sidebarMenu__userDetails {
& > {
......@@ -200,3 +197,10 @@ m-sidebarMenu {
}
}
}
@media screen and (min-width: 992px) {
m-sidebarMenu {
.m-sidebarMenu__sidebar {
top: 109px;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { SidebarMenuComponent } from './sidebar-menu.component';
import sidebarMenuCategories from './sidebar-menu-categories.default';
describe('SidebarMenuComponent', () => {
let component: SidebarMenuComponent;
......@@ -19,6 +21,7 @@ describe('SidebarMenuComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SidebarMenuComponent);
component = fixture.componentInstance;
component.catId = 'analytics';
// component.user = sessionMock.user;
fixture.detectChanges();
});
......
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Session } from '../../../services/session';
import menuCategories from './categories.default';
import sidebarMenuCategories from './sidebar-menu-categories.default';
interface MenuCategory {
category: MenuLink;
subcategories?: MenuLink[];
header: MenuLink;
links?: MenuLink[];
expanded?: boolean;
}
export { MenuCategory };
......@@ -24,9 +24,11 @@ export { MenuLink };
templateUrl: './sidebar-menu.component.html',
})
export class SidebarMenuComponent implements OnInit {
cats: MenuCategory[] = menuCategories;
@Input() catId: string;
cat: MenuCategory;
mobileMenuExpanded = false;
activeCat;
// activeCat;
minds: Minds;
user;
userRoles: string[] = ['user'];
......@@ -36,6 +38,7 @@ export class SidebarMenuComponent implements OnInit {
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.cat = sidebarMenuCategories.find(cat => cat.header.id === this.catId);
this.getUserRoles();
this.grantPermissionsAndFindActiveCat();
}
......@@ -48,25 +51,25 @@ export class SidebarMenuComponent implements OnInit {
}
grantPermissionsAndFindActiveCat() {
this.cats.forEach(catObj => {
catObj.category['permissionGranted'] = catObj.category.permissions
? this.checkForRoleMatch(catObj.category.permissions)
: true;
// this.cat.forEach(this.cat => {
this.cat.header['permissionGranted'] = this.cat.header.permissions
? this.checkForRoleMatch(this.cat.header.permissions)
: true;
if (catObj.subcategories) {
catObj.subcategories.forEach(subCat => {
subCat['permissionGranted'] = subCat.permissions
? this.checkForRoleMatch(subCat.permissions)
: true;
});
}
if (location.pathname.indexOf(catObj.category.path) !== -1) {
catObj.category['expanded'] = true;
this.activeCat = catObj;
} else {
catObj.category['expanded'] = false;
}
});
if (this.cat.links) {
this.cat.links.forEach(link => {
link['permissionGranted'] = link.permissions
? this.checkForRoleMatch(link.permissions)
: true;
});
}
// if (location.pathname.indexOf(this.cats.header.path) !== -1) {
// this.cats.header['expanded'] = true;
// this.activeCat = this.cat;
// } else {
// this.cat.header['expanded'] = false;
// }
// });
}
checkForRoleMatch(permissionsArray) {
......
......@@ -4,13 +4,6 @@ import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
import { Storage } from '../../services/storage';
import AsyncLock from '../../helpers/async-lock';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import BlockListSync from '../../lib/minds-sync/services/BlockListSync.js';
import AsyncStatus from '../../helpers/async-status';
@Injectable()
export class BlockListService {
blocked: BehaviorSubject<string[]>;
......
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { first, catchError } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { Client } from '../../services/api';
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import EntitiesSync from '../../lib/minds-sync/services/EntitiesSync.js';
import AsyncStatus from '../../helpers/async-status';
import normalizeUrn from '../../helpers/normalize-urn';
type EntityObservable = BehaviorSubject<Object>;
type EntityObservables = Map<string, EntityObservable>;
......
......@@ -6,40 +6,8 @@ import { Session } from '../../services/session';
import { EntitiesService } from './entities.service';
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import FeedsSync from '../../lib/minds-sync/services/FeedsSync.js';
import hashCode from '../../helpers/hash-code';
import AsyncStatus from '../../helpers/async-status';
import { BehaviorSubject, Observable, of, forkJoin, combineLatest } from 'rxjs';
import {
take,
switchMap,
map,
tap,
skipWhile,
first,
filter,
} from 'rxjs/operators';
export type FeedsServiceGetParameters = {
endpoint: string;
timebased: boolean;
//
limit: number;
offset?: number;
//
syncPageSize?: number;
forceSync?: boolean;
};
export type FeedsServiceGetResponse = {
entities: any[];
next?: number;
};
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
import { switchMap, map, tap, first } from 'rxjs/operators';
/**
* Enables the grabbing of data through observable feeds.
......@@ -69,6 +37,7 @@ export class FeedsService {
this.pageSize = this.offset.pipe(
map(offset => this.limit.getValue() + offset)
);
this.feed = this.rawFeed.pipe(
tap(feed => {
if (feed.length) this.inProgress.next(true);
......@@ -87,6 +56,7 @@ export class FeedsService {
this.inProgress.next(false);
})
);
this.hasMore = combineLatest(
this.rawFeed,
this.inProgress,
......
......@@ -81,9 +81,10 @@ export class UpdateMarkersService {
if (!opts.marker) throw 'marker must be set';
if (!opts.noReply) {
this.http
.post('api/v2/notifications/markers/read', opts)
.subscribe(res => null, err => console.warn(err));
this.http.post('api/v2/notifications/markers/read', opts).subscribe(
res => null,
err => console.warn(err)
);
}
for (let i = 0; i < this.data.length; i++) {
......
/**
* @author Ben Hayward
* @desc Singleton service used to store the current user avatar as a BehaviorSubject.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { Session } from '../../services/session';
import { MindsUser } from '../../interfaces/entities';
@Injectable({
providedIn: 'root',
})
export class UserAvatarService {
private minds = window.Minds;
private user: MindsUser;
public src$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public loggedIn$: Subscription;
constructor(public session: Session) {
this.init();
// Subscribe to loggedIn$ and on login, update src$.
if (this.session.loggedinEmitter) {
this.loggedIn$ = this.session.loggedinEmitter.subscribe(is => {
if (is) {
this.src$.next(this.getSrc());
}
});
}
}
/**
* Sets the current user and avatar src.
*/
public init(): void {
this.user = this.session.getLoggedInUser();
this.src$.next(this.getSrc());
}
/**
* Gets the Src string using the global minds object and the held user object.
*/
public getSrc(): string {
return `${this.minds.cdn_url}icon/${this.user.guid}/large/${this.user.icontime}`;
}
}
......@@ -14,7 +14,8 @@ export let mindsHttpClientMock = new (function() {
}
if (
!res ||
((res.status && res.status === 'error') || res.status === 'failed')
(res.status && res.status === 'error') ||
res.status === 'failed'
)
observer.error(res);
......
......@@ -62,6 +62,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 { AnalyticsBenchmarkComponent } from './v2/components/benchmark/benchmark.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -70,16 +71,7 @@ const routes: Routes = [
path: 'analytics',
component: AnalyticsComponent,
children: [
{ path: '', redirectTo: 'channel', pathMatch: 'full' },
{
path: 'channel',
component: ChannelAnalyticsComponent,
children: [
{ path: '', redirectTo: 'activity', pathMatch: 'full' },
{ path: 'activity', component: ChannelGeneralAnalyticsComponent },
{ path: 'reach', component: ChannelReachAnalyticsComponent },
],
},
{ path: '', redirectTo: 'dashboard/', pathMatch: 'full' },
{
path: 'admin',
component: AdminAnalyticsComponent,
......@@ -169,6 +161,7 @@ const routes: Routes = [
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsBenchmarkComponent,
],
providers: [AnalyticsDashboardService],
})
......
<div class="m-analytics__benchmarkContainer" [ngClass]="{ noChart: noChart }">
<div class="m-analytics__benchmarkLabelWrapper">
<div class="m-analytics__benchmarkLabel">{{ label }}</div>
<m-tooltip icon="help" *ngIf="description">
{{ description }}
</m-tooltip>
</div>
<div class="m-analytics__benchmarkValueWrapper">
<ng-container
[ngSwitch]="unit"
*ngIf="isNumber(value); else placeholderValue"
>
<ng-template ngSwitchCase="usd">
<div class="m-analytics__benchmarkValue">
{{ value / 100 | currency }}
</div>
<div class="m-analytics__benchmarkUnit">USD</div>
</ng-template>
<ng-template ngSwitchCase="eth">
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.3-3' }}
</div>
<div class="m-analytics__benchmarkUnit">ETH</div>
</ng-template>
<ng-template ngSwitchCase="tokens">
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.3-3' }}
</div>
<div class="m-analytics__benchmarkUnit">
{{ value !== 1 ? 'Tokens' : 'Token' }}
</div>
</ng-template>
<ng-template ngSwitchCase="hours">
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.0-0' }}
</div>
<div class="m-analytics__benchmarkUnit">
{{ value !== 1 ? 'hrs' : 'hr' }}
</div>
</ng-template>
<ng-template ngSwitchDefault>
<div class="m-analytics__benchmarkValue">
{{ value | number: '1.0-0' }}
</div>
</ng-template>
</ng-container>
</div>
</div>
<!-- TODO: delete this when active users placeholder is removed -->
<ng-template #placeholderValue>
<div class="m-analytics__benchmarkValue">{{ value }}</div>
</ng-template>
<!-- <div [ngSwitch]="unit" class="m-chartV2__hoverInfo__row--primary">
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number: '1.0-0' | abbr }}
{{ rawData.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ rawData?.unit }}
</ng-template>
</div>
</div> -->
.m-analytics__benchmarkValueWrapper > div {
display: inline-block;
}
.m-analytics__benchmarkLabelWrapper {
padding-bottom: 12px;
min-width: 190px;
& > div {
display: inline-block;
}
}
.m-analytics__benchmarkLabel {
font-size: 18px;
@include m-theme() {
color: themed($m-grey-300);
}
}
.m-analytics__benchmarkValue {
font-size: 24px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-analytics__benchmarkUnit {
font-size: 15px;
padding-left: 6px;
}
.m-analytics__benchmarkContainer {
&.noChart {
.m-analytics__benchmarkValue {
font-size: 42px;
}
.m-analytics__benchmarkLabelWrapper {
padding-bottom: 18px;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TooltipComponentMock } from '../../../../../mocks/common/components/tooltip/tooltip.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { AnalyticsBenchmarkComponent } from './benchmark.component';
describe('AnalyticsBenchmarkComponent', () => {
let component: AnalyticsBenchmarkComponent;
let fixture: ComponentFixture<AnalyticsBenchmarkComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsBenchmarkComponent, TooltipComponentMock],
// schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsBenchmarkComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-analytics__benchmark',
templateUrl: './benchmark.component.html',
})
export class AnalyticsBenchmarkComponent implements OnInit {
@Input() label: string;
@Input() description: string;
@Input() value: string | number;
@Input() unit: string;
@Input() noChart: boolean = false;
isCurrency: boolean = false;
constructor() {}
ngOnInit() {
if (this.unit && (this.unit === 'eth' || this.unit === 'usd')) {
this.isCurrency = true;
}
}
isNumber(val) {
return typeof val === 'number';
}
}
<!-- TODO: Make this into a different component -->
<!-- TODO: then all this becomes m-plotlyChart -->
<div
#chartContainer
class="m-analyticsChart__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice }"
>
<plotly-plot
*ngIf="init"
#graphDiv
id="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[style]="{ position: 'relative' }"
[useResizeHandler]="true"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(plotly_click)="onClick($event)"
>
</plotly-plot>
</div>
<div #hoverInfoDiv id="hoverInfoDiv" class="m-analyticsChart__hoverInfoDiv">
<i *ngIf="isTouchDevice" class="material-icons" (click)="onUnhover($event)"
>close</i
>
<div class="m-analyticsChart__hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="selectedMetric?.unit"
class="m-analyticsChart__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric?.unit }}
</ng-template>
</div>
<div class="m-analyticsChart__hoverInfo__row" *ngIf="isComparison">
vs
<ng-container
[ngSwitch]="selectedMetric?.unit"
class="m-analyticsChart__hoverInfo__row"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }}
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
<m-chartV2
*ngIf="selectedMetric && selectedTimespan"
[rawData]="selectedMetric"
[interval]="selectedTimespan.interval"
></m-chartV2>
m-analytics__chart {
display: block;
position: relative;
margin-left: 40px;
.js-plotly-plot,
.plot-container {
......
......@@ -14,25 +14,14 @@ m-analytics__filter {
@include m-theme() {
color: themed($m-grey-300);
}
m-tooltip {
margin-left: 4px;
}
> * {
display: inline-block;
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
}
}
}
.m-tooltip--bubble {
letter-spacing: 1.2px;
line-height: 16px;
z-index: 9999;
font-size: 11px;
bottom: 110%;
left: 0;
width: 160px;
......
......@@ -2,11 +2,8 @@ import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
} from '@angular/core';
import { Observable } from 'rxjs';
import {
AnalyticsDashboardService,
Filter,
......
......@@ -12,6 +12,6 @@ m-analytics__filters {
@media screen and (max-width: $min-tablet) {
.m-analytics__filtersContainer {
margin-left: 16px;
margin-left: 24px;
}
}
<section class="m-analytics__metricsSection">
<div class="m-analytics__metricsWrapper" #metricsWrapper>
<ng-container *ngIf="metrics$ | async as metrics">
<ng-container *ngFor="let metric of metrics">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-analytics__metricsOverflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-analytics__metricsOverflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
<div
#metricsContainer
*ngIf="metrics$ | async as metrics"
class="m-analytics__metricsContainer disable-scrollbars"
(scroll)="onScroll($event)"
>
<ng-container *ngFor="let metric of metrics">
<div
class="m-analytics__metric"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<div class="m-analytics__metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="m-analytics__metricSummary" *ngIf="metric.summary">
<ng-container *ngIf="metric.unit === 'number'">
{{ metric.summary.current_value | number }}
</ng-container>
<ng-container *ngIf="metric.unit === 'usd'">
<span>$</span
>{{ metric.summary.current_value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<div
*ngIf="metric.summary"
class="m-analytics__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>
</ng-container>
</div>
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-analytics__metricsOverflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-analytics__metricsOverflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</div>
</section>
<!--
<section class="m-analytics__metricsSection">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-analytics__metricsOverflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-analytics__metricsOverflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
<div class="m-analytics__metricsWrapper" #metricsWrapper>
<div
#metricsContainer
*ngIf="metrics$ | async as metrics"
class="m-analytics__metricsContainer disable-scrollbars"
(scroll)="onScroll($event)"
class="m-analytics__metric m-shadowboxLayout__headerItem"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<ng-container *ngFor="let metric of metrics">
<div
class="m-analytics__metric"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<div class="m-analytics__metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="m-analytics__metricSummary" *ngIf="metric.summary">
<ng-container *ngIf="metric.unit === 'number'">
{{ metric.summary.current_value | number }}
</ng-container>
<ng-container *ngIf="metric.unit === 'usd'">
<span>$</span
>{{ metric.summary.current_value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<div class="m-analytics__metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="m-analytics__metricSummary" *ngIf="metric.summary">
<ng-container *ngIf="metric.unit === 'number'">
{{ metric.summary.current_value | number }}
</ng-container>
<ng-container *ngIf="metric.unit === 'usd'">
<span>$</span
>{{ metric.summary.current_value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<div
*ngIf="metric.summary"
class="m-analytics__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>
</ng-container>
<div
*ngIf="metric.summary"
class="m-analytics__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
*ngIf="isOverflown && !isAtScrollEnd"
class="m-analytics__metricsOverflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-analytics__metricsOverflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</section> -->
</ng-container>
</ng-container>
m-analytics__metrics {
display: block;
.m-analytics__metricsSection {
position: relative;
[class*='m-analytics__metricsOverflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 2;
&.m-analytics__metricsOverflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.m-analytics__metricsOverflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='m-analytics__metricsOverflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
&.showButton {
opacity: 1;
}
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);
}
}
&.m-analytics__metricsOverflowScrollButton--right {
right: -12;
}
&.m-analytics__metricsOverflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.m-analytics__metric {
cursor: pointer;
flex: 0 0 auto;
width: 160px;
padding: 24px 20px 20px 20px;
font-size: 14px;
box-sizing: border-box;
overflow: visible;
@include m-theme() {
border-bottom: 8px solid themed($m-white);
}
}
.m-analytics__metricsWrapper {
position: relative;
z-index: 1;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
color: themed($m-grey-300);
}
}
.m-analytics__metricsContainer {
overflow-x: hidden;
overflow-y: hidden;
display: flex;
flex-wrap: nowrap;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&.disable-scrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
width: 0px;
background: transparent; /* Chrome/Safari/Webkit */
&.active {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid themed($m-blue);
}
}
&:first-child {
margin-left: 40px;
}
&:last-child {
margin-right: 40px;
}
.m-analytics__metric {
cursor: pointer;
flex: 0 0 auto;
width: 160px;
padding: 24px 20px 20px 20px;
font-size: 14px;
box-sizing: border-box;
overflow: visible;
&:hover:not(.active) {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
border-bottom: 8px solid themed($m-white);
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid rgba(0, 0, 0, 0);
}
}
.m-analytics__metricLabel {
white-space: nowrap;
}
m-tooltip {
vertical-align: middle;
}
.m-analytics__metricSummary {
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-300);
color: themed($m-grey-800);
}
&.active {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid themed($m-blue);
}
}
&:first-child {
margin-left: 40px;
}
&:last-child {
margin-right: 40px;
}
&:hover:not(.active) {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid rgba(0, 0, 0, 0);
}
}
.m-analytics__metricLabel {
white-space: nowrap;
}
m-tooltip {
vertical-align: middle;
margin-left: 4px;
}
.m-analytics__metricSummary {
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-analytics__metricDelta {
display: flex;
align-items: baseline;
padding-top: 4px;
}
.m-analytics__metricDelta {
display: flex;
align-items: baseline;
padding-top: 4px;
font-size: 11px;
.material-icons {
transform: scaleX(0.7);
font-size: 11px;
.material-icons {
transform: scaleX(0.7);
font-size: 11px;
font-weight: bold;
}
@include m-theme() {
&.goodChange {
color: themed($m-green);
}
&.badChange {
color: themed($m-red);
}
}
font-weight: bold;
}
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
@include m-theme() {
&.goodChange {
color: themed($m-green);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
width: 160px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
&.badChange {
color: themed($m-red);
}
}
}
}
@media screen and (max-width: $min-tablet) {
.m-analytics__metricsSection {
[class*='m-analytics__metricsOverflowScrollButton--'] {
display: none;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
.m-analytics__metricsContainer {
overflow-x: scroll;
position: relative;
scroll-snap-type: x mandatory;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
&:last-child {
margin-right: 16px;
}
}
}
import {
Component,
OnInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
......@@ -28,31 +19,16 @@ export { MetricExtended as Metric };
@Component({
selector: 'm-analytics__metrics',
templateUrl: './metrics.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsMetricsComponent implements OnInit, AfterViewInit {
@ViewChild('metricsContainer', { static: false })
metricsContainerEl: ElementRef;
metricsContainer;
data;
export class AnalyticsMetricsComponent implements OnInit {
subscription: Subscription;
user;
userRoles: string[] = ['user'];
init = false;
metrics$;
metricClientWidth: number;
faderWidth = 24;
isOverflown: boolean = false;
isAtScrollEnd = false;
isAtScrollStart = true;
showButton = { left: false, right: false };
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session,
private cd: ChangeDetectorRef
public session: Session
) {}
ngOnInit() {
......@@ -98,86 +74,10 @@ export class AnalyticsMetricsComponent implements OnInit, AfterViewInit {
return metrics;
})
);
// TODO: if selected metric is not fully visible, slide() until it is
}
ngAfterViewInit() {
this.checkOverflow();
}
updateMetric(metric) {
// TODO: if clicked metric is not fully visible, slide() until it is
this.analyticsService.updateMetric(metric.id);
}
@HostListener('window:resize')
onResize() {
this.checkOverflow();
}
onScroll($event) {
this.checkOverflow();
}
checkOverflow() {
const firstMetric = document.querySelector('.m-analytics__metric');
this.metricClientWidth = firstMetric.clientWidth;
this.metricsContainer = this.metricsContainerEl.nativeElement;
this.isOverflown =
this.metricsContainer.scrollWidth - this.metricsContainer.clientWidth > 0;
this.isAtScrollStart = this.metricsContainer.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.metricsContainer.scrollWidth -
(this.metricsContainer.scrollLeft + this.metricsContainer.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.metricsContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.metricsContainer.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.metricsContainer.clientWidth % this.metricClientWidth;
const completelyVisibleMetricsWidth =
this.metricsContainer.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
currentScrollLeft = this.faderWidth;
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.metricsContainer.scrollWidth - completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
scrollEndOffset = partiallyVisibleMetricWidth - this.faderWidth;
}
targetScrollLeft = Math.max(
currentScrollLeft - completelyVisibleMetricsWidth + scrollEndOffset,
0
);
}
this.metricsContainer.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -167,6 +167,7 @@
.m-analyticsTable__wrapper {
.m-analyticsTable__col--entity {
flex: 3 1 0;
padding-left: 24px;
.m-analyticsTable__entityType,
.m-analyticsTable__entityPublishDate {
display: none;
......
<div class="m-analytics__dashboard">
<m-sidebarMenu></m-sidebarMenu>
<section class="m-analytics__main" *ngIf="ready$ | async">
<div class="m-analytics__mainHeader">
<div class="m-analytics__dashboardTitle">
<h3 class="m-analytics__selectedCatLabel">
{{ category$ | async | titlecase }}
</h3>
</div>
<div class="m-analytics__globalFilters">
<!-- <div *ngIf="session.isAdmin()" class="m-analytics__channelSearch"> -->
<!-- <div class="m-analytics__channelSearch">
<m-analytics__search></m-analytics__search>
</div> -->
<!-- <div class="m-analytics__channelFilter" *ngIf="session.isAdmin()">
<m-analytics__filter [filter]="channelFilter"></m-analytics__filter>
</div> -->
<div class="m-analytics__timespanFilter">
<m-analytics__filter
[filter]="timespanFilter"
[showLabel]="false"
></m-analytics__filter>
<m-pageLayout navId="analytics">
<div class="m-analyticsDashboard" *ngIf="ready$ | async" m-pageLayout__main>
<m-dashboardLayout>
<ng-container m-dashboardLayout__header>
<div>
<h3>
{{ category$ | async | titlecase }}
</h3>
</div>
</div>
</div>
<p
class="m-analytics__selectedCatDescription"
*ngIf="description$ | async as description"
>
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
<div
class="m-analyticsDashboard__headerFilters"
*ngIf="layout === 'chart'"
>
<a
*ngIf="
session.getLoggedInUser().pro &&
!(
session.getLoggedInUser().merchant &&
session.getLoggedInUser().merchant['id']
)
"
routerLink="/wallet/usd"
>Enable payouts</a
>.
<!-- <div *ngIf="session.isAdmin()" class="m-analytics__channelSearch"> -->
<!-- <div class="m-analyticsDashboard__channelSearch">
<m-analytics__search></m-analytics__search>
</div> -->
<!-- <div class="m-analyticsDashboard__channelFilter" *ngIf="session.isAdmin()">
<m-analytics__filter [filter]="channelFilter"></m-analytics__filter>
</div> -->
<div class="m-analyticsDashboard__timespanFilter">
<m-analytics__filter
[filter]="timespanFilter"
[showLabel]="false"
></m-analytics__filter>
</div>
</div>
</ng-container>
</p>
<div class="m-analytics__layoutWrapper">
<p
m-dashboardLayout__body
class="m-analyticsDashboard__description"
*ngIf="description$ | async as description"
>
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
>
<a
*ngIf="
session.getLoggedInUser().pro &&
!(
session.getLoggedInUser().merchant &&
session.getLoggedInUser().merchant['id']
)
"
routerLink="/wallet/usd"
>Enable payouts</a
>
</ng-container>
</p>
<m-analytics__layout--chart
m-dashboardLayout__body
*ngIf="layout === 'chart'"
class="m-analytics__layout"
></m-analytics__layout--chart>
<!-- <m-analytics__layout--summary
class="m-analytics__layout"
*ngIf="(category$ | async).type === 'summary'"
></m-analytics__layout--summary> -->
</div>
</section>
</div>
<m-analytics__layout--summary
m-dashboardLayout__body
*ngIf="layout === 'summary'"
class="m-analytics__layout"
></m-analytics__layout--summary>
</m-dashboardLayout>
</div>
</m-pageLayout>
......@@ -38,6 +38,7 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
options: [],
};
channelFilter: Filter;
layout = 'chart';
constructor(
public client: Client,
......@@ -58,7 +59,13 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.title.setTitle('Analytics');
this.route.paramMap.subscribe((params: ParamMap) => {
this.updateCategory(params.get('category'));
const cat = params.get('category');
this.updateCategory(cat);
if (cat === 'summary') {
this.layout = 'summary';
} else {
this.layout = 'chart';
}
});
this.paramsSubscription = this.route.queryParams.subscribe(params => {
......
......@@ -6,11 +6,7 @@ import {
map,
distinctUntilChanged,
switchMap,
startWith,
tap,
delay,
debounceTime,
throttleTime,
catchError,
} from 'rxjs/operators';
......
This diff is collapsed.
<div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
<ng-container *ngIf="selectedMetric && selectedMetric.visualisation">
<m-shadowboxLayout
*ngIf="selectedMetric && selectedMetric.visualisation"
[hasHeader]="selectedMetric.visualisation.type === 'chart'"
>
<m-analytics__metrics
class="m-shadowboxLayout__header"
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__metrics>
<div
class="m-analytics__filterableChartWrapper"
class="m-shadowboxLayout__body"
[ngClass]="{ isTable: isTable, isMobile: isMobile }"
>
<m-analytics__chart
......@@ -19,4 +23,4 @@
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__filters>
</div>
</ng-container>
</m-shadowboxLayout>
.m-analytics__filterableChartWrapper {
position: relative;
margin-bottom: 48px;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
background-color: themed($m-white);
}
.m-shadowboxLayout__body {
&.isTable {
width: 100%;
min-width: 420px;
}
}
.m-analytics__spinnerContainer {
height: 30%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
@include m-theme() {
background-color: themed($m-white);
}
}
<p>
analytics__layout--summary works!
</p>
<ng-container m-pageLayout__main>
<div
class="m-analytics__spinnerContainer"
*ngIf="loading && this.session.isAdmin()"
>
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
<!-- TILES CONTAINER -->
<div
class="m-analyticsSummary__tilesContainer"
*ngIf="!loading && this.session.isAdmin()"
>
<!-- ACTIVE USERS TILE (TEMP PLACEHOLDER) -->
<div class="m-analyticsSummary__tile">
<m-analytics__benchmark
[label]="'Active Users On Site'"
[description]="
'Coming soon! Realtime count of all users on web and mobile'
"
[value]="'🚧'"
[noChart]="true"
></m-analytics__benchmark>
</div>
<!-- CHART TILES -->
<ng-container *ngFor="let tile of tiles">
<div class="m-analyticsSummary__tile">
<m-analytics__benchmark
[label]="tile.label"
[description]="tile.description"
[value]="tile.value"
[unit]="tile?.unit"
[noChart]="!tile.visualisation"
></m-analytics__benchmark>
<m-chartV2
*ngIf="tile.visualisation"
[rawData]="tile"
[interval]="day"
[isMini]="true"
></m-chartV2>
</div>
</ng-container>
</div>
<!-- BOOST BACKLOG -->
<div class="m-analyticsSummary__boostBacklogWrapper" *ngIf="boosts">
<div class="m-analyticsSummary__boostBacklogTitle">Boost Backlog</div>
<div class="m-analyticsSummary__boostRowsContainer">
<ng-container *ngFor="let boostRow of boostRows">
<div class="m-analyticsSummary__boostRow">
<ng-container *ngFor="let boostType of boostRow">
<div class="m-analyticsSummary__boostType">
<m-analytics__benchmark
[label]="boostType.label"
[value]="boostType.value"
[unit]="'hours'"
></m-analytics__benchmark>
</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
</ng-container>
......@@ -133,7 +133,7 @@
</div>
<minds-button-boost
[object]="blog"
*ngIf="session.isLoggedIn()"
*ngIf="session.isLoggedIn() && !isScheduled(blog.time_created)"
></minds-button-boost>
</div>
</div>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.