...
 
Commits (90)
......@@ -39,3 +39,4 @@ cypress/videos
!/.gitlab
!/.githooks
!/.prettierrc
!.gitattributes
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,
......
export * from './text-input-autocomplete.module';
export {
TextInputAutocompleteMenuComponent,
} from './text-input-autocomplete-menu.component';
export { TextInputAutocompleteMenuComponent } from './text-input-autocomplete-menu.component';
import { Component, EventEmitter } from '@angular/core';
import { Client } from '../../../services/api';
import { UserAvatarService } from '../../services/user-avatar.service';
import { of, Observable } from 'rxjs';
@Component({
selector: 'minds-avatar',
......@@ -14,17 +14,28 @@ import { Client } from '../../../services/api';
],
outputs: ['added'],
template: `
<div class="minds-avatar" [style.background-image]="'url(' + src + ')'">
<div
class="minds-avatar"
[ngStyle]="{ 'background-image': 'url(' + (getSrc() | async) + ')' }"
>
<img
*ngIf="!src"
*ngIf="!(userAvatarService.src$ | async)"
src="{{ minds.cdn_assets_url }}assets/avatars/blue/default-large.png"
class="mdl-shadow--4dp"
/>
<div *ngIf="editing" class="overlay">
<i class="material-icons">{{ icon }}</i>
<ng-container *ngIf="showPrompt">
<span *ngIf="src" i18n="@@COMMON__AVATAR__CHANGE">Change avatar</span>
<span *ngIf="!src" i18n="@@COMMON__AVATAR__ADD">Add an avatar</span>
<span
*ngIf="userAvatarService.src$ | async"
i18n="@@COMMON__AVATAR__CHANGE"
>Change avatar</span
>
<span
*ngIf="!(userAvatarService.src$ | async)"
i18n="@@COMMON__AVATAR__ADD"
>Add an avatar</span
>
</ng-container>
</div>
<input *ngIf="editing" type="file" #file (change)="add($event)" />
......@@ -40,18 +51,21 @@ export class MindsAvatar {
index: number = 0;
icon: string = 'camera';
showPrompt: boolean = true;
file: any;
added: EventEmitter<any> = new EventEmitter();
constructor(public userAvatarService: UserAvatarService) {}
set _object(value: any) {
if (!value) return;
value.icontime = value.icontime ? value.icontime : '';
this.object = value;
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
if (this.object.type === 'user')
if (this.object.type !== 'user') {
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
} else if (!this.minds.user || 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.minds.user && 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) {}
}
}
......
......@@ -79,9 +79,7 @@ export class FeaturedContentComponent implements OnInit {
component
);
const componentRef: ComponentRef<
any
> = this.dynamicHost.viewContainerRef.createComponent(
const componentRef: ComponentRef<any> = this.dynamicHost.viewContainerRef.createComponent(
componentFactory,
void 0,
this.injector
......
import { Injectable } from '@angular/core';
import {
filter,
first,
map,
switchMap,
mergeMap,
skip,
take,
} from 'rxjs/operators';
import { filter, first, switchMap, mergeMap, skip, take } from 'rxjs/operators';
import { FeedsService } from '../../services/feeds.service';
import { Subscription } from 'rxjs';
@Injectable()
export class FeaturedContentService {
offset: number = -1;
offset = 0;
maximumOffset = 0;
feedLength = 0;
protected feedSubscription: Subscription;
constructor(protected feedsService: FeedsService) {
this.onInit();
}
onInit() {
this.feedSubscription = this.feedsService.feed.subscribe(feed => {
this.feedLength = feed.length;
this.maximumOffset = this.feedLength - 1;
});
this.feedsService
.setLimit(12)
.setOffset(0)
......@@ -23,28 +28,36 @@ export class FeaturedContentService {
}
async fetch() {
if (this.offset >= this.feedsService.rawFeed.getValue().length) {
this.offset = -1;
}
// Refetch every 2 calls, if not loading
if (this.offset % 2 && !this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
return await this.feedsService.feed
.pipe(
filter(feed => feed.length > 0),
first(),
mergeMap(feed => feed),
filter(entities => entities.length > 0),
mergeMap(feed => feed), // Convert feed array to stream
skip(this.offset++),
take(1),
switchMap(async entity => {
if (!entity) {
return false;
} else {
const resolvedEntity = await entity.pipe(first()).toPromise();
this.resetOffsetAtEndOfStream();
return resolvedEntity;
}
return await entity.pipe(first()).toPromise();
})
)
.toPromise();
}
protected resetOffsetAtEndOfStream() {
if (this.offset >= this.maximumOffset) {
this.offset = 0;
this.fetchNextFeed();
}
}
protected fetchNextFeed() {
if (!this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
}
}
......@@ -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>
......@@ -2,14 +2,19 @@
.m-marketing__asFeaturedIn {
max-width: 1084px;
margin: 40px auto 0;
margin: 45px auto 80px;
.m-marketing__asFeaturedIn--noMargin & {
margin-bottom: 0;
}
@media screen and (max-width: $m-grid-min-vp) {
&.m-grid {
display: block;
margin-bottom: 45px;
}
margin: 20px 0 45px;
margin: 20px 0 50px;
padding: 0 12px;
}
......@@ -45,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;
}
}
}
......
<div class="m-marketing__footer">
<div class="m-grid m-marketingFooter__columns">
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
class="m-grid__column-4 m-grid__column-12--mobile m-marketingFooter__column m-marketingFooter__brandColumn"
>
<h4 i18n>About</h4>
<div class="m-marketingFooter__mindsLogo"></div>
<ul>
<li hidden>
<a href="#" i18n>
Company
</a>
</li>
<h4 class="m-marketingFooter__sloganText" i18n>
Take back control of your social media
</h4>
<li hidden>
<a href="#" i18n>
Mission
</a>
</li>
<div class="m-marketingFooter__text">&copy; {{ year }} Minds, Inc.</div>
</div>
<li hidden>
<a href="#" i18n>
Features
</a>
</li>
<div
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4 i18n>About</h4>
<ul>
<li>
<a routerLink="/mobile" i18n>
Mobile
......@@ -32,7 +26,7 @@
<li>
<a routerLink="/jobs" i18n>
Jobs
Careers
</a>
</li>
......@@ -67,7 +61,7 @@
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4 i18n>Business</h4>
......@@ -96,6 +90,12 @@
</a>
</li>
<li>
<a routerLink="/pay" i18n>
Pay
</a>
</li>
<li>
<a routerLink="/nodes" i18n>
Nodes
......@@ -109,8 +109,8 @@
</li>
<li>
<a routerLink="/wire" i18n>
Pay
<a routerLink="/rewards" i18n>
Rewards
</a>
</li>
......@@ -123,7 +123,7 @@
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4>Developers</h4>
......@@ -161,7 +161,7 @@
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4>Support</h4>
......@@ -189,30 +189,19 @@
Status
</a>
</li>
<li hidden>
<a routerLink="/p/contact" i18n>
Contact
</a>
</li>
</ul>
</div>
</div>
<div class="m-marketing__sep m-marketing__sep--big"></div>
<div
class="m-marketing__sep m-marketing__sep--dashed m-marketing__sep--big"
></div>
<div class="m-grid m-marketingFooter__columns">
<div
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column m-marketingFooter__column--noMobileSpacing"
i18n
>
<div class="m-marketingFooter__text">&copy; {{ year }} Minds, Inc.</div>
</div>
<div
class="m-grid__column-10 m-grid__column-12--mobile m-marketingFooter__column"
class="m-grid__column-12 m-grid__column-12--mobile m-marketingFooter__column"
>
<ul class="m-marketingFooter__inlineList">
<ul class="m-marketingFooter__inlineList m-marketingFooter__legalLinks">
<li>
<a routerLink="/p/terms" i18n>
Terms of Service
......
......@@ -2,10 +2,10 @@
m-marketing__footer {
display: block;
margin-top: 105px;
margin: 105px 0 95px;
@media screen and (max-width: $m-grid-min-vp) {
margin-top: 80px;
margin: 80px 0 0;
}
@include m-theme() {
......@@ -19,12 +19,14 @@ m-marketing__footer {
.m-marketing__footer {
padding: 60px 0 48px;
border-top: 1px dashed;
@media screen and (max-width: $m-grid-min-vp) {
padding: 32px 0;
}
@include m-theme() {
border-color: themed($m-grey-50);
color: themed($m-grey-800);
}
......@@ -47,6 +49,17 @@ m-marketing__footer {
}
}
&.m-marketingFooter__brandColumn {
width: 60%;
margin: 0 auto;
@media screen and (max-width: $m-grid-min-vp) {
width: 60%;
grid-row: 999;
margin: 32px 0 0;
}
}
h4 {
font-weight: 500;
font-size: 16px;
......@@ -60,11 +73,34 @@ m-marketing__footer {
@include m-theme() {
color: themed($m-grey-800);
}
&.m-marketingFooter__sloganText {
margin: 0 0 21px;
}
}
.m-marketingFooter__mindsLogo {
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;
}
}
.m-marketingFooter__text {
font-size: 14px;
line-height: 26px;
@include m-theme() {
color: themed($m-grey-300);
}
}
ul {
......@@ -90,7 +126,7 @@ m-marketing__footer {
a {
color: inherit;
font-weight: 300;
font-weight: normal;
text-decoration: none;
}
}
......@@ -108,6 +144,16 @@ m-marketing__footer {
margin-right: 0;
}
}
&.m-marketingFooter__legalLinks {
text-align: right;
padding-right: 92px;
@media screen and (max-width: $m-grid-min-vp) {
text-align: inherit;
padding-right: initial;
}
}
}
}
}
......
......@@ -7,4 +7,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
})
export class MarketingFooterComponent {
readonly year: number = new Date().getFullYear();
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
}
......@@ -20,6 +20,10 @@ m-marketing {
width: 100%;
margin: 40px 0;
&.m-marketing__sep--dashed {
border-top-style: dashed;
}
&.m-marketing__sep--big {
margin: 60px 0;
}
......
@import '../../../../foundation/grid-values';
.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;
}
}
.m-marketing__title,
h1 {
font-size: 22px;
line-height: 44px;
font-weight: bold;
opacity: 0.7;
margin: 12px 0 4px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-marketing__subtitle,
h2 {
font-weight: 900;
font-size: 42px;
line-height: 44px;
margin: 0 0 23px;
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 32px;
line-height: 34px;
margin: 0 0 18px;
}
&.m-marketing__subtitle--asTitle {
font-size: 48px;
line-height: 53px;
margin: 0 0 26px;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 38px;
line-height: 46px;
margin: 0 0 21px;
}
}
em {
font-style: inherit;
text-decoration: inherit;
white-space: nowrap;
position: relative;
&::after {
content: '';
display: inline-block;
position: absolute;
background: rgba(80, 226, 195, 0.3);
top: 0.36em;
left: -0.03em;
right: -0.03em;
bottom: 0.22em;
pointer-events: none;
z-index: -1;
}
}
em + em::after {
left: -0.25em;
}
}
p.m-marketing__description {
font-size: 18px;
line-height: 27px;
margin: 0 0 36px;
}
ul.m-marketing__points {
list-style: disc;
margin: 0 0 45px;
padding: 0 0 0 1em;
font-size: 16px;
line-height: 21px;
> li {
margin-bottom: 19px;
&:last-child {
margin-bottom: 0;
}
}
}
.m-marketing__actionButtons {
> * {
margin: 0 25px 0 0;
@media screen and (max-width: $m-grid-min-vp) {
margin: 0 0 20px;
}
&:last-child {
margin-right: initial;
}
}
}
.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);
}
}
}
}
}
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
// Common
overflow-x: hidden;
.m-marketing--hideMobile {
@media screen and (max-width: $m-grid-min-vp) {
display: none;
}
}
.m-marketing__title,
h1 {
font-size: 22px;
line-height: 44px;
font-weight: bold;
opacity: 0.7;
margin: 12px 0 4px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-marketing__subtitle,
h2 {
font-weight: 900;
font-size: 42px;
line-height: 44px;
margin: 0 0 23px;
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 32px;
line-height: 34px;
margin: 0 0 18px;
}
&.m-marketing__subtitle--asTitle {
font-size: 48px;
line-height: 53px;
margin: 0 0 26px;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 38px;
line-height: 46px;
margin: 0 0 21px;
}
}
em {
font-style: inherit;
text-decoration: inherit;
white-space: nowrap;
position: relative;
&::after {
content: '';
display: inline-block;
position: absolute;
background: rgba(80, 226, 195, 0.3);
top: 0.36em;
left: -0.03em;
right: -0.03em;
bottom: 0.22em;
pointer-events: none;
z-index: -1;
}
}
em + em::after {
left: -0.25em;
}
}
p.m-marketing__description {
font-size: 18px;
line-height: 27px;
margin: 0 0 36px;
}
ul.m-marketing__points {
list-style: disc;
margin: 0 0 45px;
padding: 0 0 0 1em;
font-size: 16px;
line-height: 21px;
> li {
margin-bottom: 19px;
&:last-child {
margin-bottom: 0;
}
}
}
// Style 1
&.m-marketing__section--style-1 {
@include m-theme() {
background: linear-gradient(
......@@ -272,160 +176,4 @@
}
}
}
// Style 2
&.m-marketing__section--style-2 {
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 72px 0 32px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px 30px;
}
&::before {
content: '';
position: absolute;
top: 0;
right: -290px;
bottom: -56px;
left: 0;
transform: translate(-86px, -56px);
clip-path: polygon(0% 0%, 0% 100%, 100% 92%, 100% 0%);
z-index: -1;
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-colored-gradient-start) 0%,
themed($m-marketing-bg-colored-gradient-end) 99.99%
);
}
@media screen and (max-width: $m-grid-min-vp) {
right: 0;
bottom: -3vw;
transform: none;
clip-path: none;
}
}
h1 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 15px 0 15px;
text-align: center;
}
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
.m-marketing__image {
position: relative;
z-index: 0;
img {
object-fit: contain;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
width: 100vw;
height: 100vw;
object-fit: cover;
clip-path: polygon(0% 2%, 0% 97%, 100% 100%, 100% 0%);
}
}
span {
display: inline-block;
position: relative;
// Deco
&::before {
content: '';
display: block;
position: absolute;
width: 393px;
height: 193px;
bottom: 0;
right: 0;
transform: translate(45px, 32px);
background: url('<%= APP_CDN %>/assets/marketing/deco_1.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
&::after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 284px;
height: 262px;
transform: translate(35px, -35px);
background: url('<%= APP_CDN %>/assets/marketing/deco_2-straight.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
}
}
}
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
&.m-marketing__section--style-2 {
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 72px 0 0;
margin-bottom: 95px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0;
margin-bottom: 15px;
}
}
.m-marketing__body {
position: relative;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px 30px;
}
&::before {
content: '';
position: absolute;
top: 0;
right: -290px;
bottom: -56px;
left: 0;
transform: translate(-86px, -56px);
clip-path: polygon(0% 0%, 0% 100%, 100% 92%, 100% 0%);
z-index: -1;
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-colored-gradient-start) 0%,
themed($m-marketing-bg-colored-gradient-end) 99.99%
);
}
@media screen and (max-width: $m-grid-min-vp) {
right: 0;
bottom: -3vw;
transform: none;
clip-path: none;
}
}
h1 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 15px 0 15px;
text-align: center;
}
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
.m-marketing__image {
position: relative;
z-index: 0;
img {
object-fit: cover;
width: 438px;
height: 547px;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 2%, 0% 97%, 100% 100%, 100% 0%);
}
}
span {
display: inline-block;
position: relative;
// Deco
&::before {
content: '';
display: block;
position: absolute;
width: 393px;
height: 193px;
bottom: 0;
right: 0;
transform: translate(45px, 32px);
background: url('<%= APP_CDN %>/assets/marketing/deco_1.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
&::after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 284px;
height: 262px;
transform: translate(35px, -35px);
background: url('<%= APP_CDN %>/assets/marketing/deco_2-straight.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
}
}
}
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
&.m-marketing__section--style-3 {
margin-bottom: 100px;
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 80px;
}
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 80px 0 80px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
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 {
content: '';
position: absolute;
top: 0;
right: -360px;
left: 0;
bottom: auto;
height: calc(100% + 160px);
transform: translate(-230px, -80px);
clip-path: polygon(0% 6%, 0% 95%, 100% 100%, 100% 0%);
z-index: -1;
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-colored-gradient-start) 0%,
themed($m-marketing-bg-colored-gradient-end) 99.99%
);
}
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
ul.m-marketing__points {
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
> li em {
font-style: normal;
@include m-theme() {
color: themed($m-black);
}
@include m-on-theme(dark) {
color: #ffffff;
font-weight: bold;
}
}
}
.m-marketing__image {
position: relative;
z-index: 0;
grid-column-start: 1;
grid-row: 1;
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 518px;
clip-path: polygon(0% 1%, 0% 100%, 100% 96%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 100%, 100% 97%, 100% 0%);
}
}
img.m-marketing__image--2 {
object-fit: contain;
width: 358px;
height: 191px;
position: absolute;
right: 0;
bottom: 35px;
transform: translate(15px, 0);
@media screen and (max-width: $m-grid-min-vp) {
right: auto;
left: 50%;
bottom: 0;
transform: translate(-50%, 50%);
width: 85vw;
height: 45.35vw;
}
}
span {
display: inline-block;
position: relative;
width: 100%;
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
&.m-marketing__image--noUxSample {
margin-bottom: 40px;
}
}
// Deco
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 191px;
height: 191px;
transform: translate(-60px, -58px);
background: url('<%= APP_CDN %>/assets/marketing/deco_3.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
}
}
}
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
&.m-marketing__section--style-4 {
margin-bottom: 100px;
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 80px;
}
.m-marketing__wrapper {
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
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 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
ul.m-marketing__points {
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
> li em {
font-style: normal;
@include m-theme() {
color: themed($m-black);
}
@include m-on-theme(dark) {
color: #ffffff;
font-weight: bold;
}
}
}
.m-marketing__image {
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
grid-row: 1;
}
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 547px;
clip-path: polygon(0% 1%, 0% 96%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
}
}
img.m-marketing__image--2 {
object-fit: contain;
width: 358px;
height: 191px;
position: absolute;
left: 0;
bottom: 35px;
transform: translate(-15px, 0);
@media screen and (max-width: $m-grid-min-vp) {
right: auto;
left: 50%;
bottom: 0;
transform: translate(-50%, 50%);
width: 85vw;
height: 45.35vw;
}
}
span {
display: inline-block;
position: relative;
width: 100%;
text-align: right;
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
&.m-marketing__image--noUxSample {
margin-bottom: 40px;
}
}
// Deco
&::after {
content: '';
display: block;
position: absolute;
right: 0;
bottom: 0;
width: 169px;
height: 169px;
transform: translate(50px, 50px);
background: url('<%= APP_CDN %>/assets/marketing/deco_4.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
}
}
}
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
&.m-marketing__section--tail {
h2 {
font-size: 24px;
line-height: 32px;
font-weight: 700;
margin: 0 0 70px;
@include m-theme() {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 0 0 35px;
}
}
.m-marketing__body {
position: relative;
text-align: center;
padding: 0 36px;
h3 {
margin: 0 0 24px;
font-size: 24px;
line-height: 32px;
font-weight: 900;
@media screen and (max-width: $m-grid-min-vp) {
margin: 0 0 12px;
}
}
p.m-marketing__description {
font-size: 16px;
line-height: 21px;
@include m-theme() {
color: themed($m-grey-300);
}
em {
font-style: normal;
@include m-theme() {
color: themed($m-black);
}
@include m-on-theme(dark) {
color: #ffffff;
font-weight: bold;
}
}
}
&.m-marketing__body--extra {
margin-top: 135px;
@media screen and (max-width: $m-grid-min-vp) {
margin-top: 0;
}
}
}
.m-marketingSectionTail__table {
.m-marketing__body {
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 45px;
&:last-child {
margin-bottom: 55px;
}
}
}
> *::after {
content: '';
display: block;
position: absolute;
width: 1px;
height: 100%;
top: 0;
right: 0;
transform: translateX(($m-grid-gap / 2) + 1px);
@include m-theme() {
background: linear-gradient(
to bottom,
rgba(themed($m-grey-100), 0) 0%,
rgba(themed($m-grey-100), 0) 25%,
rgba(themed($m-grey-100), 0.9) 50%,
rgba(themed($m-grey-100), 0) 75%,
rgba(themed($m-grey-100), 0) 100%
);
}
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: initial;
}
}
> *:last-child::after {
content: initial;
display: initial;
}
}
}
}
......@@ -22,7 +22,7 @@ export class NSFWSelectorComponent {
@Input('service') serviceRef: string = 'consumer';
@Input('consumer') consumer: false;
@Input('expanded') expanded: false;
@Output('selected') onSelected: EventEmitter<any> = new EventEmitter();
@Output('selectedChange') onSelected: EventEmitter<any> = new EventEmitter();
constructor(
public creatorService: NSFWSelectorCreatorService,
......@@ -31,6 +31,14 @@ export class NSFWSelectorComponent {
private storage: Storage
) {}
ngOnInit() {
if (this.service.reasons) {
for (const reason of this.service.reasons) {
this.toggle(reason.value, false);
}
}
}
get service() {
switch (this.serviceRef) {
case 'editing':
......@@ -58,14 +66,17 @@ export class NSFWSelectorComponent {
}
}
toggle(reason) {
toggle(reason, triggerChange = true) {
if (reason.locked) {
return;
}
this.service.toggle(reason);
const reasons = this.service.reasons.filter(r => r.selected);
this.onSelected.next(reasons);
if (triggerChange) {
const reasons = this.service.reasons.filter(r => r.selected);
this.onSelected.next(reasons);
}
}
hasSelections(): boolean {
......
<m-sidebarMenu [catId]="navId"></m-sidebarMenu>
<section class="m-pageLayout__main">
<ng-content select="[m-pageLayout__main]"></ng-content>
</section>
m-pageLayout {
display: block;
position: relative;
width: 100%;
padding-top: 56px;
margin-bottom: 48px;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
.m-tooltip {
margin-left: 4px;
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-300), 0.7);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
}
}
}
m-sidebarMenu {
display: block;
box-sizing: border-box;
padding-left: 105px;
width: 245px;
@include m-theme() {
background-color: themed($m-white);
}
}
.m-pageLayout__main {
margin-left: 350px;
margin-right: 24px;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
}
@media screen and (max-width: $min-tablet) {
.m-pageLayout__main {
display: block;
margin: 0;
}
m-sidebarMenu {
margin-left: 0;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { PageLayoutComponent } from './page-layout.component';
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() {}
}
......@@ -231,7 +231,7 @@
<m-nsfw-selector
service="editing"
[selected]="entity.nsfw"
(selected)="onNSFWSelected($event)"
(selectedChange)="onNSFWSelected($event)"
>
</m-nsfw-selector>
</li>
......
......@@ -55,7 +55,7 @@
[href]="src.perma_url"
target="_blank"
rel="noopener noreferrer"
class="meta mdl-color-text--blue-grey-900"
class="meta"
[ngClass]="{ 'm-rich-embed-has-thumbnail': src.thumbnail_src, 'm-rich-embed--title--no-padding': hasInlineContentLoaded() }"
>
<h2
......@@ -74,10 +74,7 @@
<a class="thumbnail" *ngIf="preview.thumbnail">
<img src="{{preview.thumbnail}}" />
</a>
<a
class="meta mdl-color-text--blue-grey-900"
[ngClass]="{ 'm-has-thumbnail': preview.thumbnail }"
>
<a class="meta" [ngClass]="{ 'm-has-thumbnail': preview.thumbnail }">
<h2 class="m-rich-embed--title mdl-card__title-text">
{{preview.title | excerpt}}
</h2>
......
......@@ -16,9 +16,6 @@ minds-rich-embed {
left: 0;
width: 100%;
height: 100%;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.2);
}
&:hover {
background: transparent;
......
......@@ -3,11 +3,14 @@ import {
ElementRef,
ChangeDetectorRef,
ChangeDetectionStrategy,
Output,
EventEmitter,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { RichEmbedService } from '../../../services/rich-embed';
import mediaProxyUrl from '../../../helpers/media-proxy-url';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
......@@ -17,18 +20,22 @@ import mediaProxyUrl from '../../../helpers/media-proxy-url';
})
export class MindsRichEmbed {
type: string = '';
mediaSource: string = '';
src: any = {};
preview: any = {};
maxheight: number = 320;
inlineEmbed: any = null;
embeddedInline: boolean = false;
cropImage: boolean = false;
modalRequestSubscribed: boolean = false;
@Output() mediaModalRequested: EventEmitter<any> = new EventEmitter();
private lastInlineEmbedParsed: string;
constructor(
private sanitizer: DomSanitizer,
private service: RichEmbedService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
protected featureService: FeaturesService
) {}
set _src(value: any) {
......@@ -65,6 +72,14 @@ export class MindsRichEmbed {
// Inline Embedding
let inlineEmbed = this.parseInlineEmbed(this.inlineEmbed);
if (
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
this.modalRequestSubscribed =
this.mediaModalRequested.observers.length > 0;
}
if (
inlineEmbed &&
inlineEmbed.id &&
......@@ -80,9 +95,35 @@ export class MindsRichEmbed {
}
this.inlineEmbed = inlineEmbed;
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
if (this.inlineEmbed && this.inlineEmbed.htmlProvisioner) {
this.inlineEmbed.htmlProvisioner().then(html => {
this.inlineEmbed.html = html;
this.detectChanges();
});
// @todo: catch any error here and forcefully window.open to destination
}
}
}
action($event) {
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
$event.preventDefault();
$event.stopPropagation();
this.mediaModalRequested.emit();
return;
}
if (this.inlineEmbed && !this.embeddedInline) {
$event.preventDefault();
$event.stopPropagation();
......@@ -120,6 +161,7 @@ export class MindsRichEmbed {
if ((matches = youtube.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'youtube';
return {
id: `video-youtube-${matches[1]}`,
className:
......@@ -138,12 +180,13 @@ export class MindsRichEmbed {
if ((matches = vimeo.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'vimeo';
return {
id: `video-vimeo-${matches[1]}`,
className:
'm-rich-embed-video m-rich-embed-video-iframe m-rich-embed-video-vimeo',
html: this.sanitizer.bypassSecurityTrustHtml(`<iframe
src="https://player.vimeo.com/video/${matches[1]}?autoplay=1&title=0&byline=0&portrait=0"
src="https://player.vimeo.com/video/${matches[1]}?title=0&byline=0&portrait=0"
frameborder="0"
webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>`),
playable: true,
......@@ -156,6 +199,7 @@ export class MindsRichEmbed {
if ((matches = soundcloud.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'soundcloud';
return {
id: `audio-soundcloud-${matches[1]}`,
className:
......@@ -183,6 +227,7 @@ export class MindsRichEmbed {
if ((matches = spotify.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'spotify';
return {
id: `audio-spotify-${matches[1]}`,
className:
......@@ -207,7 +252,7 @@ export class MindsRichEmbed {
if (!id) {
return null;
}
this.mediaSource = 'giphy';
return {
id: `image-giphy-${matches[1]}`,
className:
......@@ -225,7 +270,11 @@ export class MindsRichEmbed {
}
hasInlineContentLoaded() {
return this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
return this.featureService.has('media-modal')
? !this.modalRequestSubscribed &&
this.inlineEmbed &&
this.inlineEmbed.html
: this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
}
detectChanges() {
......
<section class="m-shadowboxHeader__section">
<div class="m-shadowboxHeader__wrapper">
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollStart"
class="m-shadowboxHeader__overflowFade--left"
></div>
<div
[ngClass]="{ showButton: showButton.left }"
class="m-shadowboxHeader__overflowScrollButton--left"
(click)="slide('left')"
>
<i class="material-icons">chevron_left</i>
</div>
</ng-container>
<div
#shadowboxHeaderContainer
class="m-shadowboxHeader__container disable-scrollbars"
(scroll)="onScroll($event)"
>
<ng-content select=".m-shadowboxLayout__header"></ng-content>
</div>
<ng-container *ngIf="isScrollable">
<div
*ngIf="isOverflown && !isAtScrollEnd"
class="m-shadowboxHeader__overflowFade--right"
></div>
<div
[ngClass]="{ showButton: showButton.right }"
class="m-shadowboxHeader__overflowScrollButton--right"
(click)="slide('right')"
>
<i class="material-icons">chevron_right</i>
</div>
</ng-container>
</div>
</section>
m-shadowboxHeader {
min-height: 116px;
display: block;
}
.m-shadowboxHeader__section {
position: relative;
}
.m-shadowboxHeader__wrapper {
position: relative;
z-index: 1;
height: 124px;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
}
.m-shadowboxHeader__container {
overflow-x: hidden;
overflow-y: hidden;
// display: flex;
// flex-wrap: nowrap;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&.disable-scrollbars {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
width: 0px;
background: transparent; /* Chrome/Safari/Webkit */
}
}
.m-tooltip--bubble {
width: 160px;
}
}
[class*='m-shadowboxHeader__overflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 2;
&.m-shadowboxHeader__overflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.m-shadowboxHeader__overflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='m-shadowboxHeader__overflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
cursor: pointer;
&.showButton {
opacity: 1;
}
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
border: 1px solid themed($m-white);
}
&:hover {
@include m-theme() {
border: 1px solid themed($m-blue);
}
}
&.m-shadowboxHeader__overflowScrollButton--right {
right: -12;
}
&.m-shadowboxHeader__overflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
@media screen and (max-width: $min-tablet) {
.m-shadowboxHeader__section {
[class*='m-shadowboxHeader__overflowScrollButton--'] {
display: none;
}
.m-shadowboxHeader__container {
overflow-x: scroll;
scroll-snap-type: x mandatory;
.m-analytics__metric {
scroll-snap-align: start;
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
}
}
}
.m-shadowboxHeader__wrapper {
@include m-theme() {
box-shadow: none;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component,
Input,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
import { ShadowboxHeaderComponent } from './shadowbox-header.component';
describe('ShadowboxHeaderComponent', () => {
let component: ShadowboxHeaderComponent;
let fixture: ComponentFixture<ShadowboxHeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxHeaderComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
Input,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
ViewChild,
ElementRef,
HostListener,
} from '@angular/core';
@Component({
selector: 'm-shadowboxHeader',
templateUrl: './shadowbox-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShadowboxHeaderComponent implements AfterViewInit {
@Input() isScrollable: boolean = true;
@Input() metricActivated;
@ViewChild('shadowboxHeaderContainer', { static: false })
shadowboxHeaderContainerEl: ElementRef;
shadowboxHeaderContainer;
childClientWidth: number;
faderWidth = 24;
isOverflown: boolean = false;
isAtScrollEnd = false;
isAtScrollStart = true;
showButton = { left: false, right: false };
constructor(private cd: ChangeDetectorRef) {}
ngAfterViewInit() {
this.checkOverflow();
// const activeMetric = ;//get the index of the metric with .active
// this.slideToActiveMetric();
}
// updateMetric(metric) {
// // TODO: if clicked metric is not fully visible, slide() until it is
// this.analyticsService.updateMetric(metric.id);
// }
// ----------------------------------------------------
@HostListener('click', ['$event.target'])
onClick(target) {
console.log('***Clicked on: ', target);
// this.slideToActiveMetric(metricIndex);
}
slideToActiveMetric(metricIndex) {
// TODOOJM
}
// ----------------------------------------------------
@HostListener('window:resize')
onResize() {
this.checkOverflow();
}
onScroll($event) {
this.checkOverflow();
}
checkOverflow() {
if (!this.isScrollable) {
return;
}
const firstMetric = <HTMLElement>(
document.querySelector('.m-shadowboxLayout__headerItem')
);
// TODO: figure out how to avoid test failure "Cannot read property 'clientWidth' of null"
this.childClientWidth = firstMetric ? firstMetric.clientWidth : 160;
this.shadowboxHeaderContainer = this.shadowboxHeaderContainerEl.nativeElement;
this.isOverflown =
this.shadowboxHeaderContainer.scrollWidth -
this.shadowboxHeaderContainer.clientWidth >
0;
this.isAtScrollStart =
this.shadowboxHeaderContainer.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.shadowboxHeaderContainer.scrollWidth -
(this.shadowboxHeaderContainer.scrollLeft +
this.shadowboxHeaderContainer.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.shadowboxHeaderContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.shadowboxHeaderContainer.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.shadowboxHeaderContainer.clientWidth % this.childClientWidth;
const completelyVisibleMetricsWidth =
this.shadowboxHeaderContainer.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
currentScrollLeft = this.faderWidth;
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.shadowboxHeaderContainer.scrollWidth -
completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
scrollEndOffset = partiallyVisibleMetricWidth - this.faderWidth;
}
targetScrollLeft = Math.max(
currentScrollLeft - completelyVisibleMetricsWidth + scrollEndOffset,
0
);
}
this.shadowboxHeaderContainer.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<m-shadowboxHeader
*ngIf="hasHeader"
[isScrollable]="scrollableHeader"
[ngClass]="{ isScrollable: scrollableHeader }"
><ng-content
select=".m-shadowboxLayout__header"
ngProjectAs=".m-shadowboxLayout__header"
></ng-content
></m-shadowboxHeader>
<div class="m-shadowboxLayout__bottom">
<ng-content select=".m-shadowboxLayout__body"></ng-content>
<ng-content select=".m-shadowboxLayout__footer"></ng-content>
</div>
m-shadowboxLayout {
display: block;
@include m-theme() {
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
}
}
m-shadowboxHeader.isScrollable {
.m-shadowboxLayout__header {
display: flex;
flex-flow: row nowrap;
}
}
.m-shadowboxLayout__bottom {
position: relative;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
background-color: themed($m-white);
}
}
.m-shadowboxLayout__footer {
min-height: 104px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
width: 100%;
@include m-theme() {
background-color: rgba(themed($m-grey-50), 0.25);
border-top: 1px solid rgba(themed($m-grey-50), 0.6);
}
> * {
margin: 30px 68px 30px 0;
}
}
@media screen and (max-width: $min-tablet) {
m-shadowboxLayout {
@include m-theme() {
box-shadow: none;
}
}
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top: 1px solid themed($m-grey-100);
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { ShadowboxLayoutComponent } from './shadowbox-layout.component';
@Component({
selector: 'm-shadowboxHeader',
template: '',
})
class ShadowboxHeaderComponentMock {
@Input() isScrollable;
}
describe('ShadowboxLayoutComponent', () => {
let component: ShadowboxLayoutComponent;
let fixture: ComponentFixture<ShadowboxLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxLayoutComponent, ShadowboxHeaderComponentMock],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-shadowboxLayout',
templateUrl: './shadowbox-layout.component.html',
})
export class ShadowboxLayoutComponent implements OnInit {
@Input() scrollableHeader: boolean = true;
@Input() hasHeader: boolean = true;
constructor() {}
ngOnInit() {}
}
const sidebarMenuCategories = [
{
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));
}
}
This diff is collapsed.
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,
......
......@@ -81,9 +81,10 @@ export class UpdateMarkersService {
if (!opts.marker) throw 'marker must be set';
if (!opts.noReply) {
this.http
.post('api/v2/notifications/markers/read', opts)
.subscribe(res => null, err => console.warn(err));
this.http.post('api/v2/notifications/markers/read', opts).subscribe(
res => null,
err => console.warn(err)
);
}
for (let i = 0; i < this.data.length; i++) {
......
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.
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.
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.
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.