...
 
Commits (74)
image: markharding/minds-front-base
services:
- docker:dind
stages:
- test
- build
......@@ -154,6 +151,8 @@ build:production:i18n:
prepare:review:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
......@@ -179,6 +178,8 @@ prepare:review:sentry:
prepare:production:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
......@@ -261,6 +262,8 @@ review:stop:
.deploy: &deploy
image: minds/ci:latest
services:
- docker:dind
script:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
......
......@@ -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).
......
context('Rewards Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const joinRewards = '.m-marketing__mainWrapper .mf-button';
it('should have a join rewards button', () => {
cy.visit('/rewards');
cy.get(joinRewards)
.should('be.visible')
.should('contain', 'Join Rewards')
.click();
cy.location('pathname').should(
'contains',
'/wallet/tokens/contributions'
);
});
});
context('Token Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/token');
});
it('should have the ability to trigger Buy Tokens modal', () => {
const tokensInput = 'm-blockchain--purchase input[name=amount]';
const buyTokensButton =
'm-blockchain--purchase .m-blockchainTokenPurchase__action .mf-button';
const anyBuyTokensModal =
'm-blockchain--purchase m-modal .m-modal-container';
cy.get(tokensInput)
.focus()
.clear()
.type('0');
cy.get(buyTokensButton).should('be.disabled');
cy.get(tokensInput)
.focus()
.clear()
.type('1');
cy.get(buyTokensButton)
.should('not.be.disabled')
.click();
cy.get('.m-get-metamask--cancel-btn.m-btn').click();
cy.get(anyBuyTokensModal).should('be.visible');
});
it('should have the ability to trigger Buy Eth modal', () => {
const buyEthLink =
'm-blockchain--purchase .m-blockchainTokenPurchase__ethRate a';
const buyEthModal = 'm-blockchain__eth-modal .m-modal-container';
cy.get(buyEthLink).click();
cy.get(buyEthModal).should('be.visible');
});
});
context('Boost Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const createBoostButton = '.m-marketing__mainWrapper .mf-button';
it('should have a create boost button', () => {
cy.visit('/boost');
cy.get(createBoostButton)
.should('be.visible')
.should('contain', 'Create Boost')
.click();
cy.location('pathname').should(
'contains',
'/boost/console/newsfeed/create'
);
});
});
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();
});
});
});
context('Pro Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const contactUsButton = '.m-marketing__mainWrapper .mf-button';
it('should have a contact us button', () => {
cy.visit('/nodes', {
onBeforeLoad(_window) {
cy.stub(_window, 'open');
},
});
cy.get(contactUsButton)
.should('be.visible')
.should('contain', 'Contact us for details')
.click();
cy.window()
.its('open')
.should('be.called');
});
});
context('Plus Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const upgradeButton = 'm-plus--subscription .mf-button';
const wirePaymentsComponent = 'm-wire__paymentscreator .m-wire--creator';
it('should open the Wire Payment modal', () => {
cy.visit('/plus');
cy.get(upgradeButton)
.should('be.visible')
.should('contain', 'Upgrade to Plus')
.click();
cy.get(wirePaymentsComponent).should('be.visible');
});
it('should automatically open the Wire Payment modal', () => {
cy.visit('/plus?i=yearly&c=tokens');
cy.get(wirePaymentsComponent).should('be.visible');
});
});
context('Pro Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const upgradeButton = 'm-pro--subscription .mf-button';
const wirePaymentsComponent = 'm-wire__paymentscreator .m-wire--creator';
it('should show a coming soon button', () => {
cy.visit('/pro');
cy.get(upgradeButton)
.should('be.visible')
.should('contain', 'Coming soon')
.click();
});
// it('should open the Wire Payment modal', () => {
//
// cy.visit('/pro');
//
// cy.get(upgradeButton)
// .should('be.visible')
// .should('contain', 'Upgrade to Pro')
// .click();
//
// cy.get(wirePaymentsComponent).should('be.visible');
// });
//
// it('should automatically open the Wire Payment modal', () => {
// cy.visit('/pro?i=yearly&c=tokens');
//
// cy.get(wirePaymentsComponent).should('be.visible');
// });
});
......@@ -21,7 +21,6 @@ context('Pro Settings', () => {
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
},
logoGuid: '#logo_guid',
}
const hashtags = {
......
context('Upgrades page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/upgrades');
});
it('should scroll to upgrades table', () => {
cy.viewport(1200, 600); // Only on desktop
const scrollButton = '[data-cy="m-upgrades__upgrade-now-button"]';
const heading = '.m-upgradesUpgradeOptions__header h2';
cy.get(scrollButton)
.should('contain', 'Upgrade now')
.click();
cy.wait(1500);
cy.isInViewport(heading);
});
// TODO: Toggles tests (make them testable)
it('should have the ability to trigger Buy Tokens modal', () => {
const tokensInput = 'm-blockchain--purchase input[name=amount]';
const buyTokensButton =
'm-blockchain--purchase .m-blockchainTokenPurchase__action .mf-button';
const anyBuyTokensModal =
'm-blockchain--purchase m-modal .m-modal-container';
cy.get(tokensInput)
.focus()
.clear()
.type('0');
cy.get(buyTokensButton).should('be.disabled');
cy.get(tokensInput)
.focus()
.clear()
.type('1');
cy.get(buyTokensButton)
.should('not.be.disabled')
.click();
cy.get('.m-get-metamask--cancel-btn.m-btn').click();
cy.get(anyBuyTokensModal).should('be.visible');
});
it('should have the ability to trigger Buy Eth modal', () => {
const buyEthLink =
'm-blockchain--purchase .m-blockchainTokenPurchase__ethRate a';
const buyEthModal = 'm-blockchain__eth-modal .m-modal-container';
cy.get(buyEthLink).click();
cy.get(buyEthModal).should('be.visible');
});
it('should navigate to Plus and trigger a Wire', () => {
const upgradeButton = cy.get(
'[data-cy="m-upgradeOptions__upgrade-to-plus-button"]'
);
upgradeButton.click();
cy.location('pathname').should('contain', '/plus');
});
it('should navigate to Pro and trigger a Wire', () => {
const upgradeButton = cy.get(
'[data-cy="m-upgradeOptions__upgrade-to-pro-button"]'
);
upgradeButton.click();
cy.location('pathname').should('contain', '/pro');
});
it('should navigate to Nodes', () => {
const upgradeButton = cy.get(
'[data-cy="m-upgradeOptions__contact-us-nodes-button"]'
);
upgradeButton.click();
cy.location('pathname').should('contain', '/nodes');
});
});
......@@ -5,10 +5,10 @@
* @desc Spec tests for Wire transactions.
*/
import generateRandomId from "../support/utilities";
import generateRandomId from "../../support/utilities";
// Issue to re-enable https://gitlab.com/minds/front/issues/1846
context.skip('Wire', () => {
context.skip('Wire Creator', () => {
const receiver = {
username: generateRandomId(),
......
context('Pay Product Page', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
if (!sessionCookie) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
});
const monetizeChannelButton = '.m-marketing__mainWrapper .mf-button';
it('should have a monetize channel button', () => {
cy.visit('/pay');
cy.get(monetizeChannelButton)
.should('be.visible')
.should('contain', 'Monetize your channel')
.click();
cy.location('pathname').should(
'contains',
'/wallet/tokens/contributions'
);
});
});
......@@ -255,3 +255,36 @@ function b64toBlob(b64Data, contentType, sliceSize = 512) {
blob.lastModifiedDate = new Date();
return blob;
}
/**
* Check if certain element is on viewport
* @param {*} element
*/
Cypress.Commands.add('isInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.$(cy.state('window')).height();
const rect = $el[0].getBoundingClientRect();
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
})
});
/**
* Check if certain element is on viewport
* @param {*} element
*/
Cypress.Commands.add('isNotInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.$(cy.state('window')).height();
const rect = $el[0].getBoundingClientRect();
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
})
});
This diff is collapsed.
......@@ -113,6 +113,16 @@ import { MarketingComponent } from './components/marketing/marketing.component';
import { MarketingFooterComponent } from './components/marketing/footer.component';
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 * 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: [
......@@ -121,6 +131,7 @@ import { MarketingAsFeaturedInComponent } from './components/marketing/as-featur
RouterModule,
FormsModule,
ReactiveFormsModule,
PlotlyModule,
],
declarations: [
MINDS_PIPES,
......@@ -215,6 +226,12 @@ import { MarketingAsFeaturedInComponent } from './components/marketing/as-featur
MarketingComponent,
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
ShadowboxHeaderComponent,
],
exports: [
MINDS_PIPES,
......@@ -305,6 +322,11 @@ import { MarketingAsFeaturedInComponent } from './components/marketing/as-featur
ToggleComponent,
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
],
providers: [
SiteService,
......
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;
}
......
const chartPalette = {
segmentColorIds: [
// Colors for up to 6 segments
'm-blue',
'm-grey-160',
'm-amber-dark',
'm-green-dark',
'm-red-dark',
'm-blue-grey-500',
],
themeMaps: [
{
id: 'm-white',
themeMap: ['#fff', '#232323'],
},
{
id: 'm-grey-50',
themeMap: ['rgba(232,232,232,1)', 'rgba(47,47,47,1)'], // 222
},
{
id: 'm-grey-70',
themeMap: ['#eee', '#404040'], // 333 before 5% lighten
},
{
id: 'm-grey-130',
themeMap: ['#ccc', '#515151'], // 444
},
{
id: 'm-grey-160',
themeMap: ['#bbb', '#626262'], // 555
},
{
id: 'm-grey-300',
themeMap: ['#999', '#737373'], // 666
},
{
id: 'm-blue',
themeMap: ['#4690df', '#5db6ff'], // 44aaff
},
{
id: 'm-red-dark',
themeMap: ['#c62828', '#e98989'], // e57373
},
{
id: 'm-amber-dark',
themeMap: ['#ffa000', '#fff2cc'], // ffecb3
},
{
id: 'm-green-dark',
themeMap: ['#388e3c', '#97c95d'], // 8bc34a
},
{
id: 'm-blue-grey-500',
themeMap: ['#607d8b', '#6b8a99'], // 607d8b
},
],
};
export default chartPalette;
<div
#chartContainer
class="m-chartV2__chartContainer"
[ngClass]="{ isTouchDevice: isTouchDevice, isMini: isMini }"
>
<plotly-plot
*ngIf="init"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
id="graphDiv"
>
</plotly-plot>
</div>
<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__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>
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', () => {
let component: ChartV2Component;
let fixture: ComponentFixture<ChartV2Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ChartV2Component, UtcDatePipe, AbbrPipe],
providers: [
{
provide: ThemeService,
useValue: MockService(ThemeService),
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChartV2Component);
component = fixture.componentInstance;
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', () => {
expect(component).toBeTruthy();
});
});
This diff is collapsed.
<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 { AnalyticsMenuComponent } from './menu.component';
import { DashboardLayoutComponent } from './dashboard-layout.component';
describe('AnalyticsMenuComponent', () => {
let component: AnalyticsMenuComponent;
let fixture: ComponentFixture<AnalyticsMenuComponent>;
describe('DashboardLayoutComponent', () => {
let component: DashboardLayoutComponent;
let fixture: ComponentFixture<DashboardLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMenuComponent],
declarations: [DashboardLayoutComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMenuComponent);
fixture = TestBed.createComponent(DashboardLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-analytics__layout--table',
templateUrl: './layout-table.component.html',
selector: 'm-dashboardLayout',
templateUrl: './dashboard-layout.component.html',
})
export class AnalyticsLayoutTableComponent implements OnInit {
export class DashboardLayoutComponent implements OnInit {
constructor() {}
ngOnInit() {}
......
......@@ -443,7 +443,9 @@ export class ButtonsPlugin {
const $input = this.$element.querySelector('.medium-media-buttons');
if ($input) {
$input.parentNode.removeChild($input);
try {
$input.parentNode.removeChild($input);
} catch (e) {}
}
}
......
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();
}
}
}
......@@ -6,29 +6,25 @@
<ul class="m-grid__column-10">
<li>
<a
href="https://www.independent.co.uk/news/business/indyventure/minds-facebook-alternative-deletefacebook-social-network-data-a8475841.html"
href="https://www.wsj.com/articles/facebook-on-notice-as-vietnam-tightens-grip-on-social-media-11547036594"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/press-logos/independent.png'"
alt="Independent"
[src]="cdnAssetsUrl + 'assets/marketing/wsj.png'"
alt="The Wall Street Journal"
/>
</a>
</li>
<li>
<a
href="https://www.foxnews.com/tech/alternate-social-media-squash-extremist-content-without-violating-first-amendment"
href="https://www.wired.com/story/minds-anti-facebook/"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/foxnews.png'"
alt="Fox News"
/>
<img [src]="cdnAssetsUrl + 'assets/marketing/wired.png'" alt="Wired" />
</a>
</li>
<li *ngIf="false">
<img [src]="cdnAssetsUrl + 'assets/marketing/forbes.png'" alt="Forbes" />
</li>
<li>
<a
href="https://techcrunch.com/2018/04/16/minds-aims-to-decentralize-the-social-network/"
......@@ -40,35 +36,62 @@
/>
</a>
</li>
<li>
<a
href="https://mobile.reuters.com/article/amp/idUSKBN1K7147"
href="http://podcasts.joerogan.net/podcasts/bill-ottman"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/reuters.png'"
alt="Reuters"
[src]="cdnAssetsUrl + 'assets/marketing/tjre.png'"
alt="The Joe Rogan Experience"
/>
</a>
</li>
<li>
<a
href="https://www.wired.com/story/minds-anti-facebook/"
href="https://www.foxnews.com/tech/alternate-social-media-squash-extremist-content-without-violating-first-amendment"
target="_blank"
>
<img [src]="cdnAssetsUrl + 'assets/marketing/wired.png'" alt="Wired" />
<img
[src]="cdnAssetsUrl + 'assets/marketing/foxnews.png'"
alt="Fox News"
/>
</a>
</li>
<li>
<a
href="http://podcasts.joerogan.net/podcasts/bill-ottman"
href="https://www.independent.co.uk/news/business/indyventure/minds-facebook-alternative-deletefacebook-social-network-data-a8475841.html"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/tjre.png'"
alt="The Joe Rogan Experience"
[src]="cdnAssetsUrl + 'assets/marketing/press-logos/independent.png'"
alt="Independent"
/>
</a>
</li>
<li>
<a
href="https://www.reuters.com/article/us-vietnam-cyber-usa/u-s-lawmakers-urge-google-facebook-to-resist-vietnam-cybersecurity-law-idUSKBN1K7147"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/reuters.png'"
alt="Reuters"
/>
</a>
</li>
<li>
<a
href="https://www.npr.org/2019/08/06/748810962/debate-over-policing-free-speech-intensifies-as-8chan-struggles-to-stay-online"
target="_blank"
>
<img [src]="cdnAssetsUrl + 'assets/marketing/npr.png'" alt="npr" />
</a>
</li>
</ul>
</div>
......@@ -50,20 +50,8 @@
object-fit: contain;
@media screen and (max-width: $m-grid-min-vp) {
width: 40px;
height: 40px;
}
}
&.m-marketingAsFeaturedIn__item--bigger {
img {
width: 96px;
height: 96px;
@media screen and (max-width: $m-grid-min-vp) {
width: 40px;
height: 40px;
}
width: 32px;
height: 32px;
}
}
}
......
......@@ -3,9 +3,7 @@
<div
class="m-grid__column-4 m-grid__column-12--mobile m-marketingFooter__column m-marketingFooter__brandColumn"
>
<div class="m-marketingFooter__mindsLogo">
<img [src]="cdnAssetsUrl + 'assets/logos/bulb.svg'" />
</div>
<div class="m-marketingFooter__mindsLogo"></div>
<h4 class="m-marketingFooter__sloganText" i18n>
Take back control of your social media
......@@ -20,12 +18,6 @@
<h4 i18n>About</h4>
<ul>
<li hidden>
<a href="#" i18n>
Mission
</a>
</li>
<li>
<a routerLink="/mobile" i18n>
Mobile
......@@ -99,20 +91,20 @@
</li>
<li>
<a routerLink="/nodes" i18n>
Nodes
<a routerLink="/pay" i18n>
Pay
</a>
</li>
<li>
<a routerLink="/boost" i18n>
Boost
<a routerLink="/nodes" i18n>
Nodes
</a>
</li>
<li>
<a routerLink="/pay" i18n>
Pay
<a routerLink="/boost" i18n>
Boost
</a>
</li>
......@@ -197,18 +189,6 @@
Status
</a>
</li>
<li>
<a href="#" i18n>
Contact
</a>
</li>
<li>
<a href="#" i18n>
Donate
</a>
</li>
</ul>
</div>
</div>
......
......@@ -80,10 +80,17 @@ m-marketing__footer {
}
.m-marketingFooter__mindsLogo {
margin: 0 0 20px;
> img {
height: 45px;
width: 116px;
height: 43px;
margin: 0 0 35px;
background: url('<%= APP_CDN %>/assets/logos/logo.svg') no-repeat center
left;
background-size: contain;
@include m-on-theme(dark) {
background: url('<%= APP_CDN %>/assets/logos/logo-white.svg') no-repeat
center left;
background-size: contain;
}
}
......@@ -119,7 +126,7 @@ m-marketing__footer {
a {
color: inherit;
font-weight: 300;
font-weight: normal;
text-decoration: none;
}
}
......
......@@ -2,6 +2,16 @@
.m-marketing__main,
.m-marketing__section {
a {
font: inherit;
text-decoration: none;
cursor: pointer;
@include m-theme() {
color: themed($m-blue);
}
}
.m-marketing--hideMobile {
@media screen and (max-width: $m-grid-min-vp) {
display: none;
......@@ -116,6 +126,57 @@
}
}
.m-marketing__links {
@media screen and (max-width: $m-grid-min-vp) {
text-align: center;
}
h3 {
margin: 0;
font-size: 15px;
line-height: 20px;
opacity: 0.5;
font-weight: normal;
@include m-theme() {
color: themed($m-grey-800);
}
}
ul {
margin: 13px 0 0 0;
padding: 0;
list-style: none;
> li {
margin: 0 0 10px;
font-size: 15px;
line-height: 20px;
&:last-child {
margin: 0;
}
}
}
a {
> * {
vertical-align: middle;
}
i.material-icons {
margin-left: 0.35em;
font-size: 16px;
line-height: 16px;
opacity: 0.4;
@include m-theme() {
color: themed($m-grey-800);
}
}
}
}
span.m-marketing__imageUX {
span.m-marketing__imageTick {
border-radius: 50%;
......
......@@ -56,6 +56,7 @@
@media screen and (max-width: $m-grid-min-vp) {
margin: 15px 0 15px;
text-align: center;
}
}
......@@ -68,6 +69,7 @@
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
......@@ -89,6 +91,7 @@
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
......
......@@ -21,10 +21,13 @@
.m-marketing__body {
position: relative;
padding: 95px 0 0;
margin: auto 0;
padding: 0;
min-height: 330px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px 0;
min-height: 0;
}
&::before {
......@@ -62,6 +65,7 @@
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
text-align: center;
}
}
}
......@@ -83,6 +87,7 @@
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
......
......@@ -20,10 +20,13 @@
.m-marketing__body {
position: relative;
padding: 95px 0 0;
margin: auto 0;
padding: 0;
min-height: 360px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px 0;
min-height: 0;
}
h2 {
......@@ -35,11 +38,13 @@
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
margin-bottom: 42px;
......@@ -70,8 +75,10 @@
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
......
......@@ -31,8 +31,6 @@
@media screen and (max-width: $m-grid-min-vp) {
margin: 0 0 12px;
font-size: 19px;
line-height: 25px;
}
}
......
......@@ -31,6 +31,12 @@ export class NSFWSelectorComponent {
private storage: Storage
) {}
ngOnInit() {
if (this.service.reasons) {
this.service.reasons.map(r => this.toggle(r.value));
}
}
get service() {
switch (this.serviceRef) {
case 'editing':
......
<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';
import { AnalyticsLayoutTableComponent } from './layout-table.component';
@Component({
selector: 'm-sidebarMenu',
template: '',
})
class SidebarMenuComponentMock {
@Input() catId;
}
describe('AnalyticsLayoutTableComponent', () => {
let component: AnalyticsLayoutTableComponent;
let fixture: ComponentFixture<AnalyticsLayoutTableComponent>;
describe('PageLayoutComponent', () => {
let component: PageLayoutComponent;
let fixture: ComponentFixture<PageLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsLayoutTableComponent],
declarations: [PageLayoutComponent, SidebarMenuComponentMock],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsLayoutTableComponent);
fixture = TestBed.createComponent(PageLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
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() {}
}
......@@ -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 = [
{
category: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
subcategories: [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
},
{
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'],
// },
],
},
// {
// category: {
// id: 'test1',
// label: 'Test1',
// permissions: ['admin', 'user'],
// path: '/somepath/bork',
// },
// subcategories: [
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
// {
// id: 'nodes2',
// label: 'Nodes2',
// permissions: ['admin'],
// },
// ],
// },
// {
// category: {
// id: 'test2',
// label: 'Test2 no subcats',
// path: '/anotherpath/test2',
// },
// },
];
export default sidebarMenuCategories;
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;
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarHeader">
{{ cat.header.label }}
</div>
</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 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>
<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>
</nav>
</div>
<!-- </ng-container> -->
</div>
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
m-sidebarMenu {
i {
display: none;
cursor: pointer;
}
.m-sidebarMenu__topbar,
.m-sidebarMenu__userWrapper {
display: none;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__linksContainer {
display: block;
cursor: pointer;
}
}
.page.isMobile m-analytics__menu {
margin-right: -32px;
flex: 0 1 0px;
}
.m-sidebarMenu__sidebar {
position: fixed;
top: 157px;
}
m-analytics__menu {
display: block;
max-width: 160px;
// ----------------------------------------
// MOBILE
.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__linksContainer {
cursor: pointer;
display: none;
.m-sidebarMenu__link {
a {
display: block;
padding: 8px 0;
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-300);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
// --------------------------------------------------
// TABLET & MOBILE
// --------------------------------------------------
@media screen and (max-width: $min-tablet) {
m-sidebarMenu {
// margin-right: -32px;
flex: 0 1 0px;
padding: 0;
.isMobile {
.topbar {
.m-sidebarMenu__topbar {
display: block;
z-index: 99999;
position: fixed;
top: 0;
......@@ -24,7 +74,7 @@ m-analytics__menu {
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
background-color: themed($m-grey-50);
color: themed($m-grey-800);
}
......@@ -35,34 +85,32 @@ m-analytics__menu {
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
color: themed($m-grey-300);
}
}
.pageTitle {
.m-sidebarMenu__topbarHeader {
font-size: 20px;
margin: 0;
margin: 0 0 0 -24px;
min-height: 20px;
}
}
.overlay {
.m-sidebarMenu__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;
&.mobileMenuExpanded {
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
.m-sidebarMenu__sidebar {
z-index: 999999;
position: fixed;
top: 0;
......@@ -76,50 +124,69 @@ m-analytics__menu {
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
&.mobileMenuExpanded {
left: 0;
}
.sidebarTitle {
.m-sidebarMenu__catContainer {
.m-sidebarMenu__linksContainer {
display: none;
}
&.expanded {
.m-sidebarMenu__linksContainer {
display: block;
.m-sidebarMenu__link {
a {
padding: 6px 0;
}
}
}
}
}
.m-sidebarMenu__header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
margin: 16px 0;
}
i {
font-size: 20px;
display: inline-block;
font-size: 26px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
.m-sidebarMenu__userWrapper {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
.m-sidebarMenu__userAvatar {
border-radius: 50%;
margin-right: 16px;
height: 40px;
width: 40px;
object-fit: contain;
}
.details {
.m-sidebarMenu__userDetails {
& > {
padding: 8px 0;
}
.name {
.m-sidebarMenu__userDetails__name {
font-weight: bold;
}
.username {
.m-sidebarMenu__userDetails__username {
@include m-theme() {
color: themed($m-grey-200);
}
}
.subscribers {
.m-sidebarMenu__userDetails__subscribers {
font-size: 11px;
@include m-theme() {
color: themed($m-grey-200);
......@@ -129,32 +196,11 @@ m-analytics__menu {
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
a {
display: block;
padding: 6px 0;
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);
}
}
}
@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;
let fixture: ComponentFixture<SidebarMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SidebarMenuComponent],
imports: [RouterTestingModule],
providers: [{ provide: Session, useValue: sessionMock }],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SidebarMenuComponent);
component = fixture.componentInstance;
component.catId = 'analytics';
// component.user = sessionMock.user;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Session } from '../../../services/session';
import sidebarMenuCategories from './sidebar-menu-categories.default';
interface MenuCategory {
header: MenuLink;
links?: MenuLink[];
expanded?: boolean;
}
export { MenuCategory };
interface MenuLink {
id: string;
label: string;
permissions?: string[];
permissionGranted?: boolean;
path?: string;
}
export { MenuLink };
@Component({
selector: 'm-sidebarMenu',
templateUrl: './sidebar-menu.component.html',
})
export class SidebarMenuComponent implements OnInit {
@Input() catId: string;
cat: MenuCategory;
mobileMenuExpanded = false;
// activeCat;
minds: Minds;
user;
userRoles: string[] = ['user'];
constructor(public route: ActivatedRoute, public session: Session) {}
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.cat = sidebarMenuCategories.find(cat => cat.header.id === this.catId);
this.getUserRoles();
this.grantPermissionsAndFindActiveCat();
}
getUserRoles() {
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
// TODO: define & handle other userRole options, e.g. pro, loggedIn
}
grantPermissionsAndFindActiveCat() {
// this.cat.forEach(this.cat => {
this.cat.header['permissionGranted'] = this.cat.header.permissions
? this.checkForRoleMatch(this.cat.header.permissions)
: true;
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) {
return permissionsArray.some(role => this.userRoles.includes(role));
}
}
import { TestBed } from '@angular/core/testing';
import { TagsPipe } from './tags';
import { FeaturesService } from '../../services/features.service';
import { MockService } from '../../utils/mock';
import { SiteService } from '../services/site.service';
describe('TagPipe', () => {
let featuresServiceMock: any = MockService(FeaturesService, {
const featuresServiceMock: any = MockService(FeaturesService, {
has: feature => {
return true;
},
});
const siteServiceMock: any = MockService(SiteService, {
props: {
isProDomain: { get: () => false },
pro: { get: () => false },
},
});
let pipe;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TagsPipe],
providers: [
{
provide: FeaturesService,
useValue: featuresServiceMock,
},
],
});
pipe = new TagsPipe(featuresServiceMock, siteServiceMock);
});
it('should transform when # in the middle ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring#name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -32,7 +32,6 @@ describe('TagPipe', () => {
});
it('should transform when # preceded by space ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring #name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -41,7 +40,6 @@ describe('TagPipe', () => {
});
it('should transform when # preceded by [] ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring [#name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -50,7 +48,6 @@ describe('TagPipe', () => {
});
it('should transform when # preceded by () ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring (#name)';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -59,7 +56,6 @@ describe('TagPipe', () => {
});
it('should transform uppercase text following # to lower case ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textString #NaMe';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -68,7 +64,6 @@ describe('TagPipe', () => {
});
it('should correctly parse when duplicates substrings present', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = '#hash #hashlonger';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -80,28 +75,24 @@ describe('TagPipe', () => {
});
it('should transform when @ preceded by () ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring (@name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a class="tag"');
});
it('should transform when @ preceded by [] ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring [@name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a class="tag"');
});
it('should transform when @ preceded by space', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring @name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a class="tag"');
});
it('should transform when @ followed by `.com`', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring @name.com';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a class="tag"');
......@@ -109,7 +100,6 @@ describe('TagPipe', () => {
});
it('should transform two adjacent tags', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = '@test1 @test2';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toEqual(
......@@ -118,7 +108,6 @@ describe('TagPipe', () => {
});
it('should transform many adjacent tags', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string =
'@test1 @test2 @test3 @test4 @test5 @test6 @test7 @test8 @test9 @test10 @test11 @test12 @test13 @test14 @test15';
const transformedString = pipe.transform(<any>string);
......@@ -135,14 +124,12 @@ describe('TagPipe', () => {
});
it('should transform to an email', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring@name.com';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a href="mailto:textstring@name.com"');
});
it('should not transform when @ not present', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toEqual(string);
......@@ -150,35 +137,30 @@ describe('TagPipe', () => {
});
it('should transform url http', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring http://minds.com/';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a href="http://minds.com/');
});
it('should transform url with https', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring https://minds.com/';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a href="https://minds.com/');
});
it('should transform url with ftp', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring ftp://minds.com/';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a href="ftp://minds.com/');
});
it('should transform url with file', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textstring file://minds.com/';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain('<a href="file://minds.com/');
});
it('should transform url with a hashtag', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'text http://minds.com/#position';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -187,7 +169,6 @@ describe('TagPipe', () => {
});
it('should transform url with a hashtag and @', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'text http://minds.com/#position@some';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
......@@ -196,7 +177,6 @@ describe('TagPipe', () => {
});
it('should transform many tags', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = `text http://minds.com/#position@some @name
@name1 #hash1#hash2 #hash3 ftp://s.com name@mail.com
`;
......
import { Pipe, Inject, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { FeaturesService } from '../../services/features.service';
import { SiteService } from '../services/site.service';
@Pipe({
name: 'tags',
......@@ -31,7 +32,11 @@ export class TagsPipe implements PipeTransform {
hash: {
rule: /(^|\s||)#(\w+)/gim,
replace: m => {
if (this.featureService.has('top-feeds')) {
if (this.siteService.isProDomain) {
return `${
m.match[1]
}<a href="/all;query=${m.match[2].toLowerCase()}">#${m.match[2]}</a>`;
} else if (this.featureService.has('top-feeds')) {
return `${
m.match[1]
}<a href="/newsfeed/global/top;hashtag=${m.match[2].toLowerCase()};period=24h">#${
......@@ -49,7 +54,10 @@ export class TagsPipe implements PipeTransform {
},
};
constructor(private featureService: FeaturesService) {}
constructor(
private featureService: FeaturesService,
private siteService: SiteService
) {}
/**
* Push a match to results array
......
......@@ -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,
......
/**
* @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}`;
}
}
......@@ -184,36 +184,14 @@ export class AdminBoosts {
}
onKeyPress(e: KeyboardEvent) {
if (this.reasonModalOpened || e.ctrlKey || e.altKey || e.shiftKey) {
//If an input is focused, disregard.
if (document.activeElement.tagName === 'INPUT') {
return;
}
e.stopPropagation();
// numbers
switch (e.key.toLowerCase()) {
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0':
const keyValue = Number.parseInt(e.key);
this.boosts[0].quality = keyValue > 0 ? keyValue * 10 : 100;
break;
case 'arrowleft':
return this.accept();
case 'arrowright':
return this.openReasonsModal();
case 'e':
//mark as nsfw and reject
this.eTag(this.boosts[0]);
break;
case 'n':
//mark as nsfw and accept
this.accept(this.boosts[0], true);
......@@ -221,9 +199,6 @@ export class AdminBoosts {
case 'a':
this.accept();
break;
case 'r':
this.openReasonsModal();
break;
}
}
......
......@@ -85,7 +85,6 @@ export interface MindsUser {
pro_published?: boolean;
pro_settings?: {
logo_image: string;
logo_guid: string;
tag_list?: Tag[];
background_image: string;
title: string;
......@@ -97,6 +96,9 @@ export interface MindsUser {
featured_content?: Array<string>;
tile_ratio?: string;
styles?: { [key: string]: string };
domain: string;
has_custom_logo?: boolean;
has_custom_background?: boolean;
};
mode: ChannelMode;
}
......
......@@ -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);
......
......@@ -51,7 +51,6 @@ import { PageviewsCardComponent } from './components/cards/pageviews/pageviews.c
import { PageviewsChartComponent } from './components/charts/pageviews/pageviews.component';
import { AnalyticsDashboardComponent } from './v2/dashboard.component';
import { AnalyticsLayoutChartComponent } from './v2/layouts/layout-chart/layout-chart.component';
import { AnalyticsLayoutTableComponent } from './v2/layouts/layout-table/layout-table.component';
import { AnalyticsLayoutSummaryComponent } from './v2/layouts/layout-summary/layout-summary.component';
import { AnalyticsMetricsComponent } from './v2/components/metrics/metrics.component';
import { AnalyticsFiltersComponent } from './v2/components/filters/filters.component';
......@@ -63,7 +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 { AnalyticsMenuComponent } from './v2/components/menu/menu.component';
import { AnalyticsBenchmarkComponent } from './v2/components/benchmark/benchmark.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -163,7 +162,6 @@ const routes: Routes = [
Graph,
AnalyticsDashboardComponent,
AnalyticsLayoutChartComponent,
AnalyticsLayoutTableComponent,
AnalyticsLayoutSummaryComponent,
AnalyticsMetricsComponent,
AnalyticsFiltersComponent,
......@@ -172,7 +170,7 @@ const routes: Routes = [
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsMenuComponent,
AnalyticsBenchmarkComponent,
],
providers: [AnalyticsDashboardService],
})
......
const categories: Array<any> = [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// metrics: [],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
metrics: [
'active_users',
'signups',
'unique_visitors',
'pageviews',
'impressions',
'retention',
],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
metrics: ['total', 'pageviews', 'active_referrals', 'customers'],
},
// {
// id: 'engagement',
// label: 'Engagement',
// permissions: ['admin', 'user'],
// metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
// },
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
metrics: ['top_content', 'top_channels'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// metrics: ['top_referrers'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_tokens'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
];
export default categories;
const chartPalette: Array<any> = [
{
id: 'm-white',
themeMap: ['#fff', '#161616'],
},
{
id: 'm-transparent',
themeMap: ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'],
},
{
id: 'm-grey-50',
themeMap: ['rgba(232,232,232,1)', 'rgba(53,53,53,1)'],
},
{
id: 'm-grey-70',
themeMap: ['#eee', '#333'],
},
{
id: 'm-grey-130',
themeMap: ['#ccc', '#555'],
},
{
id: 'm-grey-160',
themeMap: ['#bbb', '#555'],
},
{
id: 'm-grey-300',
themeMap: ['#999', '#666'],
},
{
id: 'm-blue',
themeMap: ['#4690df', '#44aaff'],
},
{
id: 'm-red-dark',
themeMap: ['#c62828', '#e57373'],
},
{
id: 'm-amber-dark',
themeMap: ['#ffa000', '#ffecb3'],
},
{
id: 'm-green-dark',
themeMap: ['#388e3c', '#8bc34a'],
},
{
id: 'm-blue-grey-500',
themeMap: ['#607d8b', '#607d8b'],
},
];
export default chartPalette;
<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 -->
<!-- <m-chart [buckets]="(vm$.thecurrentvisualisation" | async)></m-chart> -->
<!-- TODO: then all this becomes m-plotlyChart -->
<!-- <div *ngIf="vm$ | async as vm"> -->
<div>
<div #graphDiv id="graphDiv"></div>
<!-- <plotly-plot
id="graphDiv"
[divId]="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(afterPlot)="afterPlot()"
>
</plotly-plot> -->
<!-- <div class="hoverInfo__row">
{{ hoverInfo.date | date: selectedTimespan.datePipe }}
</div> -->
<div #hoverInfoDiv id="hoverInfoDiv" class="hoverInfoDiv">
<div class="hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div [ngSwitch]="selectedMetric.unit" class="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="hoverInfo__row" *ngIf="isComparison">
vs
<ng-container [ngSwitch]="selectedMetric.unit" class="hoverInfo__row">
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
{{ selectedMetric.unit }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
</div>
<m-chartV2
*ngIf="selectedMetric && selectedTimespan"
[rawData]="selectedMetric"
[interval]="selectedTimespan.interval"
></m-chartV2>
m-analytics__chart {
display: block;
position: relative;
.js-plotly-plot,
.plot-container {
height: 44vh;
min-height: 44vh;
display: block;
}
}
#graphDiv {
display: block;
position: relative;
g,
g > * {
......@@ -24,7 +27,7 @@ m-analytics__chart {
}
}
.hoverInfoDiv {
.m-analyticsChart__hoverInfoDiv {
width: 160px;
padding: 12px;
position: absolute;
......@@ -37,20 +40,49 @@ m-analytics__chart {
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0 0 4px rgba(themed($m-black), 0.3);
color: themed($m-grey-200);
color: themed($m-grey-300);
}
[class*='hoverInfo__row'] {
[class*='m-analyticsChart__hoverInfo__row'] {
padding-bottom: 4px;
font-weight: 300;
&:last-of-type {
padding-top: 2px;
}
}
.hoverInfo__row--primary {
.m-analyticsChart__hoverInfo__row--primary {
font-weight: 400;
font-size: 15px;
// font-weight: bold;
@include m-theme() {
color: themed($m-grey-600);
}
}
i {
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);
}
&:hover {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
}
.isTouchDevice .m-analyticsChart__hoverInfoDiv i {
display: block;
}
@media screen and (max-width: $min-tablet) {
m-analytics__chart {
margin-left: 16px;
}
}
<div class="filterLabelWrapper" *ngIf="filter.id !== 'timespan'">
<div class="m-analyticsFilter__labelWrapper" *ngIf="showLabel">
<span>{{ filter.label }}</span>
<m-tooltip icon="help">
<div class="filterDesc">{{ filter?.description }}</div>
<ul class="filterOptions__descContainer">
<div>{{ filter?.description }}</div>
<ul>
<ng-container *ngFor="let option of filter.options">
<li class="filterOption__desc">
<span class="filterOption__descLabel">{{ option.label }}</span
><span class="filterOption__desc" *ngIf="option.description"
>: {{ option.description }}</span
>
<li>
<span>{{ option.label }}</span
><span *ngIf="option.description">: {{ option.description }}</span>
</li>
</ng-container>
</ul>
</m-tooltip>
</div>
<div
class="filterWrapper"
class="m-analyticsFilter__wrapper"
[ngClass]="{
expanded: expanded,
isMobile: isMobile,
dropUp: dropUp
}"
(focus)="expanded = true"
(blur)="expanded = false"
tabindex="0"
>
<div class="filterHeader" (click)="expanded = !expanded">
<div class="row">
<span class="option option--selected">
{{ selectedOption.label }}
</span>
<i class="material-icons" *ngIf="!expanded">keyboard_arrow_down</i>
<i class="material-icons" *ngIf="expanded">keyboard_arrow_up</i>
</div>
<div
class="m-analyticsFilter__header m-analyticsFilter__row"
(click)="expanded = !expanded"
>
<span class="m-analyticsFilter__option m-analyticsFilter__option--selected">
{{ selectedOption.label }}
</span>
<i class="material-icons" *ngIf="!expanded">keyboard_arrow_down</i>
<i class="material-icons" *ngIf="expanded">keyboard_arrow_up</i>
</div>
<div class="unselectedOptionsContainer">
<div class="m-analyticsFilter__optionsContainer">
<ng-container *ngFor="let option of filter.options">
<div
class="option row"
class="m-analyticsFilter__option m-analyticsFilter__row"
(click)="updateFilter(option)"
[ngClass]="{
unavailable: option.available === false
}"
>
<span>{{ option.label }}</span>
{{ option.label }}
<!-- <span>{{ option.label }}</span> -->
</div>
</ng-container>
</div>
......
......@@ -3,95 +3,94 @@ $rounded-bottom: 0 0 3px 3px;
m-analytics__filter {
position: relative;
margin: 0 36px 0 0;
margin: 0 24px 36px 0;
z-index: 2;
display: block;
}
.filterLabelWrapper {
.m-analyticsFilter__labelWrapper {
position: absolute;
bottom: 110%;
bottom: 115%;
white-space: nowrap;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.9);
}
m-tooltip {
margin-right: 4px;
color: themed($m-grey-300);
}
> * {
display: inline-block;
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.7);
}
.m-tooltip--bubble {
letter-spacing: 1.2px;
line-height: 16px;
bottom: 110%;
left: 0;
width: 160px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
.m-tooltip--bubble {
letter-spacing: 1.2px;
line-height: 16px;
z-index: 9999;
> * {
font-size: 11px;
bottom: 85%;
left: 100%;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
> * {
font-size: 11px;
font-weight: 300;
line-height: inherit;
letter-spacing: inherit;
}
ul {
padding-inline-start: 16px;
margin-block-end: 4px;
li {
padding-bottom: 8px;
.filterOption__descLabel {
// font-weight: bold;
}
}
font-weight: 300;
line-height: inherit;
letter-spacing: inherit;
}
ul {
padding-inline-start: 16px;
margin-block-end: 4px;
li {
padding-bottom: 8px;
}
}
}
}
.filterWrapper {
.m-analyticsFilter__wrapper {
cursor: pointer;
&:focus {
outline: 0;
}
> * {
width: 180px;
box-sizing: border-box;
}
.m-analyticsFilter__optionsContainer {
padding: 8px 0;
.m-analyticsFilter__option {
transform: translateY(25%);
}
}
&.expanded {
@include m-theme() {
box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
}
.filterHeader {
.m-analyticsFilter__header {
@include m-theme() {
border-color: themed($m-blue);
}
}
.unselectedOptionsContainer {
visibility: visible;
// @include m-theme() {
// box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
// }
.m-analyticsFilter__optionsContainer {
display: block;
}
&:not(.dropUp) {
.filterHeader {
.m-analyticsFilter__header {
@include m-theme() {
border-radius: $rounded-top;
}
}
.unselectedOptionsContainer {
.m-analyticsFilter__optionsContainer {
border-top: none;
border-radius: $rounded-bottom;
}
}
&.dropUp {
.filterHeader {
.m-analyticsFilter__header {
border-radius: $rounded-bottom;
}
.unselectedOptionsContainer {
.m-analyticsFilter__optionsContainer {
bottom: 100%;
border-radius: $rounded-top;
border-bottom: none;
......@@ -99,40 +98,36 @@ m-analytics__filter {
}
}
.filterHeader {
.m-analyticsFilter__header {
position: relative;
width: 100%;
padding: 8px 6px 6px 10px;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
background-color: themed($m-white);
color: rgba(themed($m-grey-200), 0.9);
color: themed($m-grey-300);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
.filterLabel {
.m-analyticsFilter__label {
margin-right: 10px;
}
i {
flex-grow: 0;
width: 24px;
height: 24px;
}
.option--selected {
margin-right: 8px;
.m-analyticsFilter__option--selected {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
.unselectedOptionsContainer {
.m-analyticsFilter__optionsContainer {
position: absolute;
// display: none;
visibility: hidden;
width: 100%;
padding: 8px 6px 6px 10px;
display: none;
border-radius: 3px;
left: 0px;
transition: box-shadow 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
......@@ -141,21 +136,29 @@ m-analytics__filter {
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
}
.option {
padding: 5px 0;
}
}
.row {
.m-analyticsFilter__row {
display: flex;
justify-content: space-between;
align-items: center;
height: 46px;
padding: 0 20px;
&.m-analyticsFilter__header {
padding-right: 10px;
}
}
.option {
.m-analyticsFilter__option {
display: inline-block;
box-sizing: border-box;
width: inherit;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@include m-theme() {
background-color: themed($m-white);
color: rgba(themed($m-grey-200), 0.9);
color: themed($m-grey-300);
}
&.unavailable {
......@@ -166,19 +169,45 @@ m-analytics__filter {
}
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-500);
color: themed($m-grey-600);
}
}
}
}
.filterWrapper.isMobile {
.filterHeader {
i {
display: none;
@media screen and (max-width: $min-tablet) {
m-analytics__filter {
.m-analyticsFilter__labelWrapper {
.m-tooltip--bubble {
width: 120px;
}
}
.m-analyticsFilter__wrapper {
> * {
width: 140px;
}
}
}
.option--selected {
margin-right: 0;
}
@media screen and (max-width: $max-mobile) {
m-analytics__filter {
.m-analyticsFilter__wrapper {
.m-analyticsFilter__header {
i {
display: none;
}
}
.m-analyticsFilter__row {
padding: 0 18px;
height: 40px;
&.m-analyticsFilter__header {
padding-right: 10px;
}
}
.m-analyticsFilter__option--selected {
margin-right: 0;
}
}
}
}
<div class="filtersContainer">
<div class="m-analytics__filtersContainer">
<!-- <ng-container *ngFor="let filter of filters$ | async"> -->
<ng-container *ngFor="let filter of filters">
<m-analytics__filter
......
.filtersContainer {
m-analytics__filters {
display: block;
}
.m-analytics__filtersContainer {
display: flex;
flex-wrap: wrap;
padding: 16px;
padding: 16px 16px 16px 0;
position: relative;
margin-top: 36px;
margin: 36px 0 0 40px;
}
@media screen and (max-width: $min-tablet) {
.m-analytics__filtersContainer {
margin-left: 24px;
}
}
......@@ -27,8 +27,9 @@ export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
ngOnInit() {
// TODO: remove all of this once channel search is ready
// Temporarily remove channel search from channel filter options
this.analyticsService.filters$.subscribe(filters => {
this.subscription = this.analyticsService.filters$.subscribe(filters => {
this.filters = filters;
const channelFilter = filters.find(filter => filter.id === 'channel');
channelFilter.options = channelFilter.options.filter(option => {
......@@ -48,5 +49,7 @@ export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
this.cd.detectChanges();
}
ngOnDestroy() {}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
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.
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.
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.
This diff is collapsed.
This diff is collapsed.