...
 
Commits (46)
### Summary
(What is the Merge request intending to do, in plain language)
(Be sure to associate any related issues or merge requests)
### Steps to test
(Steps to demonstrate merge achieves goal)
(Include any platform specific directions)
### Estimated Regression Scope
(What features do these changes effect in your estimation?)
......@@ -3,32 +3,38 @@
* @desc E2E testing for Minds Pro's pages.
*/
context('Pro Page', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const topBar = '.m-proChannel__topbar';
let categories = [
{ label: 'Technology', tag: '#technology' },
{ label: 'Food', tag: '#food' },
{ label: 'News', tag: '#news' }
{ label: 'News', tag: '#news' },
];
let footerLinks = [
{ label: 'Minds', link: 'https://www.minds.com/' },
{ label: 'Careers', link: 'https://www.minds.com/careers' },
];
const proButton = 'data-minds-sidebar-admin-pro-button';
function resetSettings() {
cy.visit(`/pro/settings`);
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.route('POST', '**/api/v2/pro/settings').as('settings');
cy.get('#title').focus().clear().type('Title');
cy.get('#headline').focus().clear().type('This is a headline');
cy.get('#title')
.focus()
.clear()
.type('Title');
cy.get('#headline')
.focus()
.clear()
.type('This is a headline');
cy.contains('Hashtags')
.click();
cy.contains('Hashtags').click();
// remove all hashtags
removeInputs();
......@@ -37,8 +43,7 @@ context('Pro Page', () => {
let cat = categories[i];
addTag(cat.label, cat.tag, i);
}
cy.contains('Footer')
.click();
cy.contains('Footer').click();
cy.get('#footer_text')
.clear()
......@@ -54,30 +59,36 @@ context('Pro Page', () => {
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
.wait('@settings')
.then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
}
);
});
}
function removeInputs() {
cy.get('.m-draggableList__list .m-proSettings__field .m-proSettings__flexInputs').should('be.visible').within($el => {
for (let i = $el.length - 1; i >= 0; i--) { // flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) { // inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') { // if it's the X button, click on it
cy.wrap(cc).click();
cy.get(
'.m-draggableList__list .m-proSettings__field .m-proSettings__dragDropRow--input'
)
.should('be.visible')
.within($el => {
for (let i = $el.length - 1; i >= 0; i--) {
// flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) {
// inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') {
// if it's the X button, click on it
cy.wrap(cc).click();
}
}
}
}
});
});
}
function addTag(label, tag, index) {
cy.contains('+ Add Tag')
.click();
cy.contains('+ Add Tag').click();
cy.get(`#tag-label-${index}`)
.clear()
......@@ -89,8 +100,7 @@ context('Pro Page', () => {
}
function addFooterLink(label, link, index) {
cy.contains('Add Link')
.click();
cy.contains('Add Link').click();
cy.get(`#footer_link-title-${index}`)
.clear()
......@@ -116,42 +126,51 @@ context('Pro Page', () => {
});
it('should load the feed tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/activities/top**").as("activities");
cy.route('GET', '**/api/v2/pro/content/*/activities/top**').as(
'activities'
);
cy.contains('Feed')
.click()
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@activities')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the videos tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/videos/top**").as("videos");
cy.route('GET', '**/api/v2/pro/content/*/videos/top**').as('videos');
cy.contains('Videos')
.click()
.wait('@videos').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@videos')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the images tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/images/top**").as("images");
cy.route('GET', '**/api/v2/pro/content/*/images/top**').as('images');
cy.contains('Images')
.click()
.wait('@images').then((xhr) => {
expect(xhr.status).to.equal(200);
});
.wait('@images')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
// should have sub-categories
cy.get('m-pro--channel--categories > .m-proChannel__category').each(($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
});
cy.get('m-pro--channel--categories > .m-proChannel__category').each(
($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
}
);
cy.get('m-pro--channel .m-overlay-modal').should('not.be.visible');
// click on tile
cy.get('.m-proChannelListContent__list li:first-child m-pro--channel-tile').click();
cy.get(
'.m-proChannelListContent__list li:first-child m-pro--channel-tile'
).click();
cy.wait(200);
// media modal should appear
......@@ -159,35 +178,41 @@ context('Pro Page', () => {
// close media modal
cy.get('m-pro--channel .m-overlay-modal--close').click();
})
});
it('should load the articles tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/blogs/top**").as("blogs");
cy.route('GET', '**/api/v2/pro/content/*/blogs/top**').as('blogs');
cy.contains('Articles')
.click()
.wait('@blogs').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@blogs')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the groups tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/groups/top**").as("groups");
cy.route('GET', '**/api/v2/pro/content/*/groups/top**').as('groups');
cy.contains('Groups')
.click()
.wait('@groups').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@groups')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should have a footer', () => {
// should have a footer text
cy.get('.m-proChannelFooter__text').contains('This is the footer text');
// should have footer links
cy.get('.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link').should('be.visible').each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
})
cy.get(
'.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link'
)
.should('be.visible')
.each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
});
}
})
});
......@@ -3,30 +3,31 @@
* @desc E2E testing for Minds Pro's settings.
*/
context('Pro Settings', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const title = '#title';
const headline = '#headline';
const previewButton = '.m-proSettings__previewBtn';
const activityContainer = 'minds-activity';
const footerText = '#footer_text';
const theme = {
primaryColor: '#primary_color',
plainBackgroundColor: '#plain_background_color',
textColor: '#textColor',
primaryColor: '#primaryColor',
plainBackgroundColor: '#plainBgColor',
schemeLight: '#scheme_light',
schemeDark: '#scheme_dark',
aspectRatio: {
169: '#tile_ratio_16\:9', // 16:9
1610: '#tile_ratio_16\:10', // 16:10
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
169: '#tile_ratio_16:9', // 16:9
1610: '#tile_ratio_16:10', // 16:10
43: '#tile_ratio_4:3', // 4:3
11: '#tile_ratio_1:1', // 1:1
},
}
};
const hashtags = {
labelInput0: '#tag-label-0',
labelInput0: '#tag-label-0',
hashtagInput0: '#tag-tag-0',
labelInput1: '#tag-label-1',
labelInput1: '#tag-label-1',
hashtagInput1: '#tag-tag-1',
label1: 'label1',
label2: 'label2',
......@@ -34,40 +35,47 @@ context('Pro Settings', () => {
hashtag1: '#hashtag1',
hashtag2: '#hashtag2',
hashtag3: '#hashtag3',
}
};
const footer = {
hrefInput: `#footer_link-href-0`,
titleInput: `#footer_link-title-0`,
}
};
const strings = {
title: "Minds Pro E2E",
headline: "This headline is a test",
footer: "This is a footer",
footerTitle: "Minds",
title: 'Minds Pro E2E',
headline: 'This headline is a test',
footer: 'This is a footer',
footerTitle: 'Minds',
footerHref: 'https://www.minds.com/',
}
};
before(() => {
cy.login(true, Cypress.env().pro_username, Cypress.env().pro_password);
});
after(() => {
cy.visit("/pro/settings")
// cy.visit(`/${Cypress.env().username}`);
cy.visit('/pro/' + Cypress.env().pro_username + '/settings/hashtags')
.location('pathname')
.should('eq', '/pro/settings');
.should(
'eq',
'/pro/' + Cypress.env().pro_username + '/settings/hashtags'
);
clearHashtags();
});
beforeEach(()=> {
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.route('POST', '**/api/v2/pro/settings').as('settings');
cy.visit("/pro/settings")
cy.visit('/pro/' + Cypress.env().pro_username + '/settings/general')
.location('pathname')
.should('eq', '/pro/settings');
.should(
'eq',
'/pro/' + Cypress.env().pro_username + '/settings/general'
);
});
it('should update the title and headline', () => {
......@@ -84,40 +92,36 @@ context('Pro Settings', () => {
saveAndPreview();
//check tab title.
cy.title()
.should('eq', strings.title+' - '+strings.headline+" | Minds");
cy.title().should(
'eq',
strings.title + ' - ' + strings.headline + ' | Minds'
);
});
// Need to find a way around the color input in Cypress.
it('should allow the user to set a dark theme for posts', () => {
cy.contains('Theme')
.click();
cy.contains('Theme').click();
cy.get(theme.schemeDark)
.click();
cy.get(theme.schemeDark).click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.contains('Feed').click();
cy.get(activityContainer)
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
});
it('should allow the user to set a light theme for posts', () => {
cy.contains('Theme')
.click();
cy.contains('Theme').click();
cy.get(theme.schemeLight)
.click();
cy.get(theme.schemeLight).click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.contains('Feed').click();
cy.get(activityContainer)
.should('have.css', 'background-color')
......@@ -125,12 +129,10 @@ context('Pro Settings', () => {
});
it('should allow the user to set category hashtags', () => {
cy.contains('Hashtags')
.click();
cy.contains('Hashtags').click();
cy.contains('Add').click();
cy.contains('+ Add Tag')
.click();
cy.get(hashtags.labelInput0)
.clear()
.type(hashtags.label1);
......@@ -138,10 +140,9 @@ context('Pro Settings', () => {
cy.get(hashtags.hashtagInput0)
.clear()
.type(hashtags.hashtag1);
cy.contains('+ Add Tag')
.click();
cy.contains('Add').click();
cy.get(hashtags.labelInput1)
.first()
.clear()
......@@ -151,7 +152,7 @@ context('Pro Settings', () => {
.first()
.clear()
.type(hashtags.hashtag2);
saveAndPreview();
//check the labels are present and clickable.
......@@ -160,20 +161,18 @@ context('Pro Settings', () => {
});
it('should allow the user to set footer', () => {
cy.contains('Footer')
.click();
cy.contains('Footer').click();
cy.get(footerText)
.clear()
.type(strings.footer);
cy.contains('Add Link')
.click();
cy.contains('Add Link').click();
cy.get(footer.hrefInput)
.clear()
.type(strings.footerHref);
cy.get(footer.titleInput)
.clear()
.type(strings.footerTitle);
......@@ -189,50 +188,45 @@ context('Pro Settings', () => {
function saveAndPreview() {
//save and await response
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
.click()
.wait('@settings')
.then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
//go to pro page
cy.get(previewButton)
.click();
cy.contains('View Pro Channel').click();
}
function clearHashtags() {
cy.contains('Hashtags')
.click();
cy.contains('+ Add Tag')
.click();
cy.contains('clear')
.click({multiple: true});
saveAndPreview();
cy.contains('Hashtags').click();
cy.contains('Add').click();
cy.contains('clear').click({ multiple: true });
saveAndPreview();
}
//
//
// it.only('should update the theme', () => {
// // nav to theme tab
// cy.contains('Theme')
// .click();
// cy.get(theme.plainBackgroundColor).then(elem => {
// elem.val('#00dd00');
// //save and await response
// cy.contains('Save')
// .click()
// .click()
// .wait('@settings').then((xhr) => {
// expect(xhr.status).to.equal(200);
// expect(xhr.response.body).to.deep.equal({ status: 'success' });
// });
// //go to pro page
// cy.get(previewButton)
// .click();
// });
// cy.contains('View Pro Channel').click();
// })
}
})
});
......@@ -1985,6 +1985,12 @@
"integrity": "sha512-4GbNCDs98uHCT/OMv40qQC/OpoPbYn9XdXeTiFwHBBFO6eJhYEPUu2zDKirXSbHlvDV8oZ9l8EQ+HrEx/YS9DQ==",
"dev": true
},
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
......@@ -5308,13 +5314,14 @@
"dev": true
},
"cypress": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-3.4.1.tgz",
"integrity": "sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-3.6.1.tgz",
"integrity": "sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A==",
"dev": true,
"requires": {
"@cypress/listr-verbose-renderer": "0.4.1",
"@cypress/xvfb": "1.2.4",
"@types/sizzle": "2.3.2",
"arch": "2.1.1",
"bluebird": "3.5.0",
"cachedir": "1.3.0",
......@@ -5341,6 +5348,7 @@
"request-progress": "3.0.0",
"supports-color": "5.5.0",
"tmp": "0.1.0",
"untildify": "3.0.3",
"url": "0.11.0",
"yauzl": "2.10.0"
},
......@@ -19068,6 +19076,12 @@
}
}
},
"untildify": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz",
"integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==",
"dev": true
},
"upath": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
......
......@@ -64,7 +64,7 @@
"@types/jasminewd2": "~2.0.4",
"@types/node": "~10.12.18",
"codelyzer": "^4.5.0",
"cypress": "^3.4.1",
"cypress": "^3.6.1",
"cypress-file-upload": "^3.3.3",
"gulp": "~4.0.0",
"gulp-autoprefixer": "^6.0.0",
......
<ng-container *ngIf="!isProDomain">
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></m-notifications--topbar-toggle>
</ng-container>
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container *ngIf="ready">
<ng-container *ngIf="!isProDomain">
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar></m-search--bar>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></m-notifications--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container search>
<m-search--bar></m-search--bar>
</ng-container>
<m-sidebar--markers
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
<m-sidebar--markers
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
</ng-container>
<m-body
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
</ng-container>
[class.is-pro-domain]="isProDomain"
>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<span
class="m-blockchain--wallet-address-notice--action"
routerLink="/tokens"
i18n="@@BLOCKCHAIN__SALE__NOTICE"
>
The MINDS token is now live. Learn more here.
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
<router-outlet></router-outlet>
</m-body>
<m-messenger *ngIf="minds.LoggedIn && !isProDomain"></m-messenger>
<m-hovercard-popup></m-hovercard-popup>
<m-body
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.is-pro-domain]="isProDomain"
>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<span
class="m-blockchain--wallet-address-notice--action"
routerLink="/tokens"
i18n="@@BLOCKCHAIN__SALE__NOTICE"
>
The MINDS token is now live. Learn more here.
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
<router-outlet></router-outlet>
</m-body>
<m-messenger *ngIf="minds.LoggedIn && !isProDomain"></m-messenger>
<m-hovercard-popup></m-hovercard-popup>
<m-overlay-modal></m-overlay-modal>
<m--blockchain--transaction-overlay></m--blockchain--transaction-overlay>
<m-modal--tos-updated *ngIf="session.isLoggedIn()"></m-modal--tos-updated>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<m-modal-signup-on-scroll *ngIf="!isProDomain"></m-modal-signup-on-scroll>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"> </m-cookies-notice>
<m-overlay-modal></m-overlay-modal>
<m--blockchain--transaction-overlay></m--blockchain--transaction-overlay>
<m-modal--tos-updated *ngIf="session.isLoggedIn()"></m-modal--tos-updated>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<m-modal-signup-on-scroll *ngIf="!isProDomain"></m-modal-signup-on-scroll>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"> </m-cookies-notice>
</ng-container>
import { Component, HostBinding } from '@angular/core';
import { ChangeDetectorRef, Component, HostBinding } from '@angular/core';
import { NotificationService } from './modules/notifications/notification.service';
import { AnalyticsService } from './services/analytics';
......@@ -18,6 +18,7 @@ import { ThemeService } from './common/services/theme.service';
import { BannedService } from './modules/report/banned/banned.service';
import { DiagnosticsService } from './services/diagnostics.service';
import { SiteService } from './common/services/site.service';
import { SsoService } from './common/services/sso.service';
import { Subscription } from 'rxjs';
import { RouterHistoryService } from './common/services/router-history.service';
import { PRO_DOMAIN_ROUTES } from './modules/pro/pro.module';
......@@ -29,8 +30,11 @@ import { PRO_DOMAIN_ROUTES } from './modules/pro/pro.module';
})
export class Minds {
name: string;
minds = window.Minds;
ready: boolean = false;
showOnboarding: boolean = false;
showTOSModal: boolean = false;
......@@ -57,7 +61,9 @@ export class Minds {
private bannedService: BannedService,
private diagnostics: DiagnosticsService,
private routerHistoryService: RouterHistoryService,
private site: SiteService
private site: SiteService,
private sso: SsoService,
private cd: ChangeDetectorRef
) {
this.name = 'Minds';
......@@ -67,8 +73,29 @@ export class Minds {
}
async ngOnInit() {
this.diagnostics.setUser(this.minds.user);
this.diagnostics.listen(); // Listen for user changes
try {
this.diagnostics.setUser(this.minds.user);
this.diagnostics.listen(); // Listen for user changes
if (this.sso.isRequired()) {
await this.sso.connect();
}
} catch (e) {
console.error('ngOnInit()', e);
}
this.ready = true;
this.detectChanges();
try {
await this.initialize();
} catch (e) {
console.error('initialize()', e);
}
}
async initialize() {
this.blockListService.fetch();
if (!this.site.isProDomain) {
this.notificationService.getNotifications();
......@@ -136,4 +163,9 @@ export class Minds {
get isProDomain() {
return this.site.isProDomain;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
import { Cookie } from '../../services/cookie';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { Location } from '@angular/common';
import { SiteService } from '../services/site.service';
/**
* API Class
*/
export class MindsHttpClient {
base: string = '/';
origin: string = '';
cookie: Cookie = new Cookie();
static _(http: HttpClient, site: SiteService) {
return new MindsHttpClient(http, site);
static _(http: HttpClient) {
return new MindsHttpClient(http);
}
constructor(public http: HttpClient, protected site: SiteService) {
if (this.site.isProDomain) {
this.base = window.Minds.site_url;
this.origin = document.location.host;
}
}
constructor(public http: HttpClient) {}
/**
* Return a GET request
......@@ -81,22 +73,11 @@ export class MindsHttpClient {
'X-VERSION': environment.version,
};
if (this.origin) {
const PRO_XSRF_JWT = this.cookie.get('PRO-XSRF-JWT') || '';
headers['X-MINDS-ORIGIN'] = this.origin;
headers['X-PRO-XSRF-JWT'] = PRO_XSRF_JWT;
}
const builtOptions = {
headers: new HttpHeaders(headers),
cache: true,
};
if (this.origin) {
builtOptions['withCredentials'] = true;
}
return Object.assign(options, builtOptions);
}
}
......
......@@ -122,6 +122,11 @@ import { DashboardLayoutComponent } from './components/dashboard-layout/dashboar
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
import { ShadowboxHeaderComponent } from './components/shadowbox-header/shadowbox-header.component';
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime';
import { DropdownSelectorComponent } from './components/dropdown-selector/dropdown-selector.component';
import { ShadowboxSubmitButtonComponent } from './components/shadowbox-submit-button/shadowbox-submit-button.component';
import { FormDescriptorComponent } from './components/form-descriptor/form-descriptor.component';
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { SsoService } from './services/sso.service';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -235,6 +240,10 @@ PlotlyModule.plotlyjs = PlotlyJS;
DashboardLayoutComponent,
ShadowboxLayoutComponent,
ShadowboxHeaderComponent,
DropdownSelectorComponent,
FormDescriptorComponent,
FormToastComponent,
ShadowboxSubmitButtonComponent,
],
exports: [
MINDS_PIPES,
......@@ -330,9 +339,14 @@ PlotlyModule.plotlyjs = PlotlyJS;
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
DropdownSelectorComponent,
FormDescriptorComponent,
FormToastComponent,
ShadowboxSubmitButtonComponent,
],
providers: [
SiteService,
SsoService,
{
provide: AttachmentService,
useFactory: AttachmentService._,
......@@ -348,7 +362,7 @@ PlotlyModule.plotlyjs = PlotlyJS;
{
provide: MindsHttpClient,
useFactory: MindsHttpClient._,
deps: [HttpClient, SiteService],
deps: [HttpClient],
},
{
provide: NSFWSelectorCreatorService,
......
......@@ -63,7 +63,7 @@ export class MindsAvatar {
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) {
} 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}`;
}
}
......@@ -130,6 +130,6 @@ export class MindsAvatar {
* @returns true if the object guid matches the currently logged in user guid
*/
isOwnerAvatar(): boolean {
return this.object.guid === this.minds.user.guid;
return this.minds.user && this.object.guid === this.minds.user.guid;
}
}
......@@ -241,7 +241,7 @@ export class ChartV2Component implements OnInit, OnDestroy {
margin: {
t: this.isMini ? 0 : 16,
b: this.isMini ? 0 : 80,
l: this.isMini ? 0 : 0,
l: 0,
r: this.isMini ? 0 : 80,
pad: 16,
},
......@@ -377,7 +377,7 @@ export class ChartV2Component implements OnInit, OnDestroy {
return rows.map(row => {
if (key === 'date') {
return row[key].slice(0, 10);
} else if (this.segments[0].unit === 'usd') {
} else if (this.rawData.unit && this.rawData.unit === 'usd') {
return row[key] / 100;
} else {
return row[key];
......
......@@ -15,6 +15,13 @@ m-dashboardLayout {
position: relative;
display: block;
width: 100%;
a {
font-weight: 400;
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
}
@media screen and (max-width: $min-tablet) {
......
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'm-dashboardLayout',
templateUrl: './dashboard-layout.component.html',
})
export class DashboardLayoutComponent implements OnInit {
export class DashboardLayoutComponent {
constructor() {}
ngOnInit() {}
}
......@@ -38,3 +38,11 @@
}
}
}
.m-tooltip--hidden {
.m-tooltip--bubble {
position: absolute;
right: 0;
bottom: 0;
}
}
import { Component, EventEmitter, Input, Output } from '@angular/core';
/**
* Date picker / selector - can be adapted to add time.
* Date picker / selector.
*/
@Component({
moduleId: module.id,
selector: 'm-date-selector',
template: `
<label class="m-dateSelector__label" *ngIf="label">{{ label }}</label>
<label class="m-dateSelector__label" *ngIf="label">{{ label }} </label>
<input
class="m-dateSelector__input"
[ngClass]="{ 'm-dateSelector__input--hidden': hideInput }"
[owlDateTimeTrigger]="dt"
[owlDateTime]="dt"
[min]="min"
......@@ -17,7 +18,15 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
[(ngModel)]="date"
(ngModelChange)="onDateChange($event)"
/>
<owl-date-time [pickerType]="'calendar'" #dt></owl-date-time>
<owl-date-time [pickerType]="calendarType" #dt></owl-date-time>
<m-tooltip
*ngIf="tooltipIcon"
icon="{{ tooltipIcon }}"
[owlDateTimeTrigger]="dt"
i18n-label="{{ i18n }}"
>
{{ tooltipText }}
</m-tooltip>
`,
})
export class DateSelectorComponent {
......@@ -26,6 +35,13 @@ export class DateSelectorComponent {
@Input() dateFormat: string = 'short'; // legacy. TODO: implement localization.
@Input() label: string; // label for input.
@Input() hideInput = false; // text input showing the date.
@Input() calendarType = 'calendar'; // timer/calendar/both.
@Input() i18n?: string; // i18n string to accompany tooltip text.
@Input() tooltipIcon?: string; // tooltip icon.
@Input() tooltipText?: string; // tooltip text.
protected _date: Date;
@Input('date') // parse input into Date object.
......
<div
class="m-draggableList__listItem m-draggableList__listHeader"
*ngIf="headers"
(click)="clickedHeaderRow($event)"
>
<ng-container *ngFor="let header of headers">
<div class="m-draggableList__cell">{{ header | titlecase }}</div>
</ng-container>
<div class="m-draggableList__cell"></div>
</div>
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndStart)="dragging = true"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
[ngClass]="{ dragging: dragging }"
>
<div class="dndPlaceholder" dndPlaceholderRef></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
[dndDragImageOffsetFunction]="dragImageOffsetRight"
[dndDisableIf]="disabled"
class="m-draggableList__listItem"
>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
<div class="m-draggableList__cell">
<i class="handle material-icons" dndHandle>open_with</i>
<i class="material-icons" (click)="removeItem(i)">
clear
</i>
</div>
</li>
</ul>
@import 'themes';
m-draggable-list {
m-draggableList {
width: 100%;
@include m-theme() {
box-shadow: 0 1px 4px 0 rgba(themed($m-black), 0.1);
}
ul.m-draggableList__list {
width: 100%;
list-style: none;
padding: 0;
padding-inline-start: 0;
margin: 0;
display: flex;
flex-direction: column;
transition: all ease 300ms;
&.dndDragover {
padding-top: 16px;
padding-bottom: 16px;
// padding-top: 16px;
// padding-bottom: 16px;
@include m-theme() {
background-color: rgba(themed($m-black), 0.05);
box-shadow: 0 1px 4px 0 rgba(themed($m-black), 0.1);
}
}
&.dragging {
li.m-draggableList__listItem {
&:first-child {
@include m-theme() {
border-top: 1px solid themed($m-grey-50);
}
}
}
}
}
li.m-draggableList__listItem {
padding: 8px;
border: 1px solid #ddd;
.m-draggableList__listItem {
display: flex;
align-items: center;
list-style-type: none;
padding: 0;
margin: 0;
@include m-theme() {
border: 1px solid themed($m-grey-50);
color: themed($m-grey-800);
}
// &:first-child {
&:not(.m-draggableList__listHeader) {
@include m-theme() {
border-top: none;
}
}
// }
&.m-draggableList__listHeader {
@include m-theme() {
// border-bottom: none;
color: themed($m-grey-300);
}
}
}
input.m-draggableList__cell {
width: 0;
min-width: 0;
}
.m-draggableList__cell {
padding: 10px 20px;
flex: 1 1 0px;
box-sizing: border-box;
@include m-theme() {
border: none;
border-right: 1px solid themed($m-grey-50);
background-color: themed($m-white);
}
&input {
width: 0;
min-width: 0;
}
&:last-child {
//icon cell
padding: 10px 15px;
flex: 0 0 80px;
max-width: 80px;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
.handle {
@include m-theme() {
color: themed($grey-600);
}
@include m-theme() {
border-right: none;
}
}
}
i {
cursor: pointer;
width: auto;
height: auto;
transition: all 0.3s ease;
@include m-theme() {
color: themed($m-grey-300);
}
&.handle {
font-size: 20px;
padding-right: 8px;
@include m-theme() {
}
}
&:hover {
transform: scale(1.15);
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.dndPlaceholder {
min-height: 100px;
@include m-theme() {
border: 1px dashed rgba(themed($m-grey-100), 0.8);
}
}
}
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { DndDropEvent, EffectAllowed } from 'ngx-drag-drop';
import {
Component,
ContentChild,
Input,
TemplateRef,
Output,
EventEmitter,
ChangeDetectorRef,
} from '@angular/core';
import {
DndDropEvent,
EffectAllowed,
DndDragImageOffsetFunction,
} from 'ngx-drag-drop';
@Component({
selector: 'm-draggable-list',
template: `
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
>
<div
class="dndPlaceholder"
dndPlaceholderRef
style="min-height:100px;border:1px dashed green;background-color:rgba(0, 0, 0, 0.1)"
></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
class="m-draggableList__listItem"
>
<i class="handle material-icons" dndHandle>reorder</i>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
</li>
</ul>
`,
selector: 'm-draggableList',
templateUrl: 'list.component.html',
})
export class DraggableListComponent {
@Input() data: Array<any>;
@Input() dndEffectAllowed: EffectAllowed = 'copyMove';
@Input() id: string;
@Input() headers: string[];
@Input() disabled: boolean;
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
@Output() emptyListHeaderRowClicked: EventEmitter<any> = new EventEmitter();
@Output() arrayChanged: EventEmitter<any> = new EventEmitter();
dragging: boolean = false;
trackByFunction(index, item) {
return this.id ? item[this.id] + index : index;
}
constructor(private cd: ChangeDetectorRef) {}
onDrop(event: DndDropEvent) {
this.dragging = false;
if (
this.data &&
(event.dropEffect === 'copy' || event.dropEffect === 'move')
......@@ -50,7 +44,7 @@ export class DraggableListComponent {
let dragIndex = this.data.findIndex(
item => event.data[this.id] === item[this.id]
);
let dropIndex = event.index || this.data.length;
let dropIndex = event.index;
// remove element
this.data.splice(dragIndex, 1);
......@@ -60,6 +54,43 @@ export class DraggableListComponent {
}
this.data.splice(dropIndex, 0, event.data);
this.arrayChanged.emit(this.data);
}
}
removeItem(index) {
this.data.splice(index, 1);
this.arrayChanged.emit(this.data);
}
clickedHeaderRow($event) {
if (this.data.length === 0) {
this.emptyListHeaderRowClicked.emit($event);
}
}
dragImageOffsetRight: DndDragImageOffsetFunction = (
event: DragEvent,
dragImage: HTMLElement
) => {
return {
x: dragImage.offsetWidth - 57,
y: event.offsetY + 10,
};
};
/**
* If input is focused then disable dragging
*/
onFocusIn(e: FocusEvent | MouseEvent) {
this.disabled = true;
}
/**
* Re-enable when input not focused
* TODO: Make this smarter.. what if something else disabled the dragging?
*/
onFocusOut(e: FocusEvent | MouseEvent) {
this.disabled = false;
}
}
<div class="m-analyticsFilter__labelWrapper" *ngIf="showLabel">
<div class="m-dropdownSelector__labelWrapper" *ngIf="showLabel">
<span>{{ filter.label }}</span>
<m-tooltip icon="help">
<div>{{ filter?.description }}</div>
......@@ -13,7 +13,7 @@
</m-tooltip>
</div>
<div
class="m-analyticsFilter__wrapper"
class="m-dropdownSelector__wrapper"
[ngClass]="{
expanded: expanded,
dropUp: dropUp
......@@ -21,27 +21,23 @@
(blur)="expanded = false"
tabindex="0"
>
<div
class="m-analyticsFilter__header m-analyticsFilter__row"
(click)="expanded = !expanded"
>
<span class="m-analyticsFilter__option m-analyticsFilter__option--selected">
<div class="m-dropdownSelector__header" (click)="expanded = !expanded">
<span class="m-dropdownSelector__option">
{{ 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__optionsContainer">
<div class="m-dropdownSelector__optionsContainer">
<ng-container *ngFor="let option of filter.options">
<div
class="m-analyticsFilter__option m-analyticsFilter__row"
class="m-dropdownSelector__option"
(click)="updateFilter(option)"
[ngClass]="{
unavailable: option.available === false
}"
>
{{ option.label }}
<!-- <span>{{ option.label }}</span> -->
</div>
</ng-container>
</div>
......
$rounded-top: 3px 3px 0 0;
$rounded-bottom: 0 0 3px 3px;
m-analytics__filter {
m-dropdownSelector {
position: relative;
margin: 0 24px 36px 0;
z-index: 2;
display: block;
}
.m-analyticsFilter__labelWrapper {
.m-dropdownSelector__labelWrapper {
position: absolute;
bottom: 115%;
white-space: nowrap;
......@@ -45,168 +45,179 @@ m-analytics__filter {
}
}
.m-analyticsFilter__wrapper {
.m-dropdownSelector__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);
}
.m-analyticsFilter__header {
.m-dropdownSelector__header {
@include m-theme() {
border-color: themed($m-blue);
}
}
.m-analyticsFilter__optionsContainer {
.m-dropdownSelector__optionsContainer {
display: block;
}
&:not(.dropUp) {
.m-analyticsFilter__header {
.m-dropdownSelector__header {
@include m-theme() {
border-radius: $rounded-top;
}
}
.m-analyticsFilter__optionsContainer {
.m-dropdownSelector__optionsContainer {
border-top: none;
border-radius: $rounded-bottom;
}
}
&.dropUp {
.m-analyticsFilter__header {
.m-dropdownSelector__header {
border-radius: $rounded-bottom;
}
.m-analyticsFilter__optionsContainer {
.m-dropdownSelector__optionsContainer {
bottom: 100%;
border-radius: $rounded-top;
border-bottom: none;
@include m-theme() {
box-shadow: 0px -4px 16px -4px rgba(themed($m-black), 0.15);
}
}
}
}
.m-analyticsFilter__header {
position: relative;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
> * {
width: 180px;
box-sizing: border-box;
}
}
.m-dropdownSelector__header {
position: relative;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-300);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
.m-dropdownSelector__label {
margin-right: 10px;
}
i {
flex-grow: 0;
width: 24px;
height: 24px;
padding-top: 2px;
}
.m-dropdownSelector__option {
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-300);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
color: themed($m-grey-500);
}
.m-analyticsFilter__label {
margin-right: 10px;
}
i {
flex-grow: 0;
width: 24px;
height: 24px;
}
.m-analyticsFilter__option--selected {
}
}
.m-dropdownSelector__optionsContainer {
box-sizing: border-box;
position: absolute;
display: none;
border-radius: 3px;
left: 0px;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
border: 1px solid themed($m-blue);
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
box-shadow: 0px 8px 16px 0px rgba(themed($m-black), 0.15);
}
.m-dropdownSelector__option {
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-500);
background-color: rgba(themed($m-grey-100), 0.2);
}
}
}
.m-analyticsFilter__optionsContainer {
position: absolute;
display: none;
border-radius: 3px;
left: 0px;
transition: box-shadow 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
border: 1px solid themed($m-blue);
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
&:first-child {
padding-top: 14px;
}
&:last-child {
padding-bottom: 14px;
}
}
}
.m-analyticsFilter__row {
display: flex;
justify-content: space-between;
align-items: center;
height: 46px;
padding: 0 20px;
&.m-analyticsFilter__header {
padding-right: 10px;
}
.m-dropdownSelector__header {
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 10px;
}
.m-dropdownSelector__option {
display: inline-block;
padding: 10px 20px;
box-sizing: border-box;
width: inherit;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@include m-theme() {
color: themed($m-grey-300);
}
.m-analyticsFilter__option {
display: inline-block;
box-sizing: border-box;
width: inherit;
border-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.unavailable {
display: none;
text-decoration: line-through;
@include m-theme() {
color: themed($m-grey-300);
}
&.unavailable {
text-decoration: line-through;
@include m-theme() {
color: themed($m-grey-50);
}
}
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-600);
}
color: themed($m-grey-50);
}
}
}
@media screen and (max-width: $min-tablet) {
m-analytics__filter {
.m-analyticsFilter__labelWrapper {
m-dropdownSelector {
.m-dropdownSelector__labelWrapper {
.m-tooltip--bubble {
width: 120px;
}
}
.m-analyticsFilter__wrapper {
> * {
width: 140px;
}
}
}
}
@media screen and (max-width: $max-mobile) {
m-analytics__filter {
.m-analyticsFilter__wrapper {
.m-analyticsFilter__header {
m-dropdownSelector {
.m-dropdownSelector__wrapper {
> * {
width: 160px;
}
.m-dropdownSelector__header {
padding-right: 10px;
i {
display: none;
}
}
.m-analyticsFilter__row {
padding: 0 18px;
height: 40px;
&.m-analyticsFilter__header {
padding-right: 10px;
.m-dropdownSelector__optionsContainer {
.m-dropdownSelector__option {
&:first-child {
padding-top: 11px;
}
&:last-child {
padding-bottom: 11px;
}
}
}
.m-analyticsFilter__option--selected {
.m-dropdownSelector__option {
margin-right: 0;
padding: 8px 18px;
}
}
}
......
......@@ -2,32 +2,30 @@ import {
Component,
OnInit,
Input,
Output,
ChangeDetectionStrategy,
EventEmitter,
} from '@angular/core';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
import { Session } from '../../../services/session';
import { Filter, Option } from '../../../interfaces/dashboard';
@Component({
selector: 'm-analytics__filter',
templateUrl: 'filter.component.html',
selector: 'm-dropdownSelector',
templateUrl: './dropdown-selector.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFilterComponent implements OnInit {
export class DropdownSelectorComponent implements OnInit {
@Input() filter: Filter;
@Input() dropUp: boolean = false;
@Input() showLabel: boolean = true;
@Output() selectionMade: EventEmitter<any> = new EventEmitter();
expanded = false;
options: Array<any> = [];
selectedOption: Option;
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session
) {}
constructor(public session: Session) {}
ngOnInit() {
this.selectedOption =
......@@ -42,25 +40,9 @@ export class AnalyticsFilterComponent implements OnInit {
}
this.selectedOption = option;
if (this.filter.id === 'timespan') {
this.analyticsService.updateTimespan(option.id);
console.log('upDateFilter ', option.id);
return;
}
const selectedFilterStr = `${this.filter.id}::${option.id}`;
this.analyticsService.updateFilter(selectedFilterStr);
this.selectionMade.emit({
option: this.selectedOption,
filterId: this.filter.id,
});
}
// clickHeader() {
// if (this.expanded) {
// console.log('its expanded');
// setTimeout(() => {
// this.expanded = false;
// });
// } else {
// console.log('itsnot expanded');
// }
// document.getElementById("myAnchor").blur();
// }
}
<div class="m-formDescriptor" i18n>
<ng-content></ng-content>
</div>
.m-formDescriptor {
font-size: 15px;
line-height: 20px;
@include m-theme() {
color: themed($m-blue);
border-left: 2px solid themed($m-blue);
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardLayoutComponent } from './dashboard-layout.component';
import { FormDescriptorComponent } from './form-descriptor.component';
describe('DashboardLayoutComponent', () => {
let component: DashboardLayoutComponent;
let fixture: ComponentFixture<DashboardLayoutComponent>;
describe('FormDescriptorComponent', () => {
let component: FormDescriptorComponent;
let fixture: ComponentFixture<FormDescriptorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardLayoutComponent],
declarations: [FormDescriptorComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardLayoutComponent);
fixture = TestBed.createComponent(FormDescriptorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
......
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-formDescriptor',
templateUrl: './form-descriptor.component.html',
})
export class FormDescriptorComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
<div class="m-formToast__toastsContainer">
<ng-container *ngFor="let toast of toasts; let i = index">
<div
class="m-formToast__wrapper"
[ngClass]="{ dismissed: toast.dismissed }"
>
<i
class="material-icons m-formToast__icon--success"
*ngIf="toast.type === 'success'"
>check</i
>
<i
class="material-icons m-formToast__icon--error"
*ngIf="toast.type === 'error'"
>warning</i
>
<i
class="material-icons m-formToast__icon--warning"
*ngIf="toast.type === 'warning'"
>warning</i
>
<i
class="material-icons m-formToast__icon--info"
*ngIf="toast.type === 'info'"
></i>
<p i18n>{{ toast.message }}</p>
<div class="m-formToast__iconWrapper">
<i
class="material-icons m-formToast__icon--close"
(click)="toast.dismissed = true"
>clear</i
>
</div>
</div>
</ng-container>
</div>
m-formToast {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
max-width: 522px;
width: 60%;
transition: all 0.3s ease;
}
.m-formToast__toastsContainer {
display: flex;
flex-flow: column nowrap;
justify-content: flex-end;
}
.m-formToast__wrapper {
box-sizing: border-box;
width: 100%;
font-size: 15px;
line-height: 20px;
padding: 13px;
margin-bottom: 16px;
display: flex;
opacity: 1;
animation-name: fadeIn;
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
animation-duration: 0.4s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
@include m-theme() {
color: themed($m-grey-600);
background-color: themed($m-white);
box-shadow: 0 0 15px 0 rgba(themed($m-black), 0.2);
}
&.dismissed {
display: none;
}
p {
flex-grow: 1;
margin: 0;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
[class*='m-formToast__icon--'] {
margin-right: 10px;
}
.m-formToast__icon--success {
@include m-theme() {
color: themed($m-green-dark);
}
}
.m-formToast__icon--error {
@include m-theme() {
color: themed($m-red);
}
}
.m-formToast__icon--warning {
@include m-theme() {
color: themed($m-amber-dark);
}
}
.m-formToast__icon--info {
margin-right: 14px;
}
.m-formToast__icon--close {
cursor: pointer;
transition: all 0.2s ease-out;
@include m-theme() {
color: themed($m-grey-300);
}
&:hover {
transform: scale(1.2);
@include m-theme() {
color: themed($m-grey-100);
}
}
&:active {
@include m-theme() {
color: themed($m-grey-400);
}
}
}
@media screen and (max-width: $max-mobile) {
m-formToast {
bottom: 48px;
width: 75%;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormToastComponent } from './form-toast.component';
import { FormToastService } from '../../services/form-toast.service';
describe('FormToastComponent', () => {
let component: FormToastComponent;
let fixture: ComponentFixture<FormToastComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [FormToastComponent],
providers: [FormToastService],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FormToastComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { FormToast, FormToastService } from '../../services/form-toast.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'm-formToast',
templateUrl: './form-toast.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormToastComponent implements OnInit, OnDestroy {
toasts: FormToast[] = [];
timeoutIds: number[] = [];
subscription: Subscription;
constructor(
private service: FormToastService,
protected cd: ChangeDetectorRef
) {}
ngOnInit() {
this.subscription = this.service.onToast().subscribe(toast => {
// clear toasts when an empty toast is received
if (!toast.message) {
this.toasts = [];
return;
}
const toastIndex = this.toasts.push(toast) - 1;
console.log(toastIndex);
this.detectChanges();
const toastTimeout = setTimeout(() => {
this.toasts[toastIndex]['dismissed'] = true;
this.detectChanges();
}, 3400);
this.timeoutIds.push(setTimeout(() => toastTimeout));
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
this.timeoutIds.forEach(id => clearTimeout(id));
this.subscription.unsubscribe();
}
}
......@@ -35,24 +35,14 @@
<div *ngSwitchCase="'date'">
<div>
<label class="m-date-selector--label" *ngIf="field.label"
>{{ field.label }}:</label
>
<div
class="m-date-selector--input"
mdl-datetime-picker
<m-date-selector
[date]="form.get(field.key).value"
(dateChange)="form.get(field.key).setValue($event)"
>
<input
type="text"
placeholder="Select a date"
i18n-placeholder="@@COMMON__DATE_SELECTOR__PLACEHOLDER"
[value]="form.get(field.key).value | date"
place
/>
<i class="material-icons">keyboard_arrow_down</i>
</div>
[calendarType]="'both'"
[tooltipIcon]="'keyboard_arrow_down'"
label="{{ field.label }}"
dateFormat="short"
></m-date-selector>
</div>
</div>
......
......@@ -47,4 +47,8 @@ m-marketing {
margin: 0 auto;
box-sizing: border-box;
}
.m-marketing__subText {
font-size: 11px;
}
}
<m-sidebarMenu [catId]="navId"></m-sidebarMenu>
<m-sidebarMenu [menu]="menu"></m-sidebarMenu>
<section class="m-pageLayout__main">
<ng-content select="[m-pageLayout__main]"></ng-content>
</section>
......@@ -2,12 +2,17 @@ m-pageLayout {
display: block;
position: relative;
width: 100%;
padding-top: 56px;
margin-bottom: 48px;
max-width: 1200px;
margin: auto;
min-height: 100%;
padding: 56px 0 48px 0;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
&.isForm {
min-height: 0px;
}
.m-tooltip {
margin-left: 4px;
......@@ -18,7 +23,7 @@ m-pageLayout {
}
}
.m-tooltip--bubble {
z-index: 9999;
z-index: 99;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
......@@ -30,14 +35,14 @@ m-pageLayout {
m-sidebarMenu {
display: block;
box-sizing: border-box;
padding-left: 105px;
width: 245px;
padding-left: 20px;
@include m-theme() {
background-color: themed($m-white);
}
}
.m-pageLayout__main {
margin-left: 350px;
margin-left: 25%;
margin-right: 24px;
@include m-theme() {
......@@ -47,6 +52,12 @@ m-sidebarMenu {
}
@media screen and (max-width: $min-tablet) {
m-pageLayout {
&.isForm {
padding-bottom: 0px;
}
}
.m-pageLayout__main {
display: block;
margin: 0;
......
......@@ -8,6 +8,7 @@ import { PageLayoutComponent } from './page-layout.component';
})
class SidebarMenuComponentMock {
@Input() catId;
@Input() menu;
}
describe('PageLayoutComponent', () => {
......
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, HostBinding } from '@angular/core';
import { Menu } from '../sidebar-menu/sidebar-menu.component';
@Component({
selector: 'm-pageLayout',
templateUrl: './page-layout.component.html',
})
export class PageLayoutComponent implements OnInit {
@Input() navId: string;
@Input() menu: Menu;
@HostBinding('class.isForm') @Input() isForm: boolean = false;
constructor() {}
ngOnInit() {}
......
<div
class="m-poster-date-selector__input"
<m-date-selector
[date]="date"
[hideInput]="true"
[tooltipIcon]="'date_range'"
[tooltipText]="'Schedule Post'"
[i18n]="'@@SCHEDULE__POST__DATE__SELECTOR__TOOLTIP'"
[hideInput]="true"
[calendarType]="'both'"
[class.selected]="hasDateSelected()"
mdl-datetime-picker
[date]="getDate()"
(dateChange)="onDateChange($event)"
>
<input
type="text"
[ngModel]="date | date: dateFormat"
(ngModelChange)="onDateChange($event)"
[hidden]="true"
/>
<m-tooltip icon="date_range">
{{ getDate() || 'Post Immediately' }}
</m-tooltip>
<span></span>
</div>
></m-date-selector>
......@@ -6,7 +6,6 @@ import {
tick,
} from '@angular/core/testing';
import { PosterDateSelectorComponent } from './selector.component';
import { MaterialDateTimePickerDirective } from '../../directives/material/datetimepicker.directive';
import { FormsModule } from '@angular/forms';
import { MockComponent } from '../../../utils/mock';
......@@ -18,11 +17,17 @@ describe('PosterDateSelectorComponent', () => {
TestBed.configureTestingModule({
declarations: [
PosterDateSelectorComponent,
MaterialDateTimePickerDirective,
MockComponent({
selector: 'm-tooltip',
selector: 'm-date-selector',
template: '<ng-content></ng-content>',
inputs: ['icon'],
inputs: [
'date',
'hideInput',
'tooltipIcon',
'tooltipText',
'i18n',
'calendarType',
],
}),
],
imports: [FormsModule],
......
m-shadowboxHeader {
min-height: 116px;
display: block;
a {
font-weight: 400;
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
}
.m-shadowboxHeader__section {
position: relative;
......@@ -16,8 +23,6 @@ m-shadowboxHeader {
.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 {
......@@ -95,8 +100,31 @@ m-shadowboxHeader {
}
}
}
.m-shadowboxLayout__header.hasTitle {
padding: 34px 28px 34px 70px;
}
.m-shadowboxHeader__title {
margin: 0 0 2px 0;
line-height: 32px;
font-weight: 500;
@include m-theme() {
color: themed($m-grey-800);
font-size: 24px;
}
}
.m-shadowboxHeader__subtitle {
line-height: 22px;
margin: 0;
@include m-theme() {
color: themed($m-grey-300);
font-size: 15px;
}
}
@media screen and (max-width: $min-tablet) {
.m-shadowboxLayout__header.hasTitle {
padding: 0 24px;
}
.m-shadowboxHeader__section {
[class*='m-shadowboxHeader__overflowScrollButton--'] {
display: none;
......
......@@ -16,10 +16,10 @@ import {
})
export class ShadowboxHeaderComponent implements AfterViewInit {
@Input() isScrollable: boolean = true;
@Input() metricActivated;
@Input() itemActivated;
@ViewChild('shadowboxHeaderContainer', { static: false })
shadowboxHeaderContainerEl: ElementRef;
shadowboxHeaderContainer;
containerEl: ElementRef;
container;
childClientWidth: number;
faderWidth = 24;
......@@ -44,7 +44,8 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
// ----------------------------------------------------
@HostListener('click', ['$event.target'])
onClick(target) {
console.log('***Clicked on: ', target);
// console.log('***Clicked on: ', target);
// find index of target
// this.slideToActiveMetric(metricIndex);
}
......@@ -73,38 +74,32 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
// 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.container = this.containerEl.nativeElement;
this.isOverflown =
this.shadowboxHeaderContainer.scrollWidth -
this.shadowboxHeaderContainer.clientWidth >
0;
this.container.scrollWidth - this.container.clientWidth > 0;
this.isAtScrollStart =
this.shadowboxHeaderContainer.scrollLeft < this.faderWidth;
this.isAtScrollStart = this.container.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.shadowboxHeaderContainer.scrollWidth -
(this.shadowboxHeaderContainer.scrollLeft +
this.shadowboxHeaderContainer.clientWidth) <
this.container.scrollWidth -
(this.container.scrollLeft + this.container.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.shadowboxHeaderContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.isOverflown && this.container.scrollLeft >= 0 && !this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.shadowboxHeaderContainer.scrollLeft;
let currentScrollLeft = this.container.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.shadowboxHeaderContainer.clientWidth % this.childClientWidth;
this.container.clientWidth % this.childClientWidth;
const completelyVisibleMetricsWidth =
this.shadowboxHeaderContainer.clientWidth - partiallyVisibleMetricWidth;
this.container.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
......@@ -112,8 +107,7 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.shadowboxHeaderContainer.scrollWidth -
completelyVisibleMetricsWidth
this.container.scrollWidth - completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
......@@ -125,7 +119,7 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
);
}
this.shadowboxHeaderContainer.scrollTo({
this.container.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
......
......@@ -2,12 +2,23 @@
*ngIf="hasHeader"
[isScrollable]="scrollableHeader"
[ngClass]="{ isScrollable: scrollableHeader }"
><ng-content
>
<ng-content
select=".m-shadowboxLayout__header"
ngProjectAs=".m-shadowboxLayout__header"
></ng-content
></m-shadowboxHeader>
<div class="m-shadowboxLayout__bottom">
></ng-content>
></m-shadowboxHeader
>
<div *ngIf="!isForm" class="m-shadowboxLayout__bottom">
<ng-content select=".m-shadowboxLayout__body"></ng-content>
<ng-content select=".m-shadowboxLayout__footer"></ng-content>
</div>
<ng-container *ngIf="isForm">
<ng-content
class="m-shadowboxLayout__bottom"
select=".m-shadowboxLayout__bottom"
>
</ng-content>
</ng-container>
......@@ -12,27 +12,121 @@ m-shadowboxHeader.isScrollable {
}
.m-shadowboxLayout__bottom {
position: relative;
transition: all 0.3s ease;
@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;
}
.m-shadowboxLayout__button {
cursor: pointer;
padding: 10px 20px;
background-color: #4fc3a9;
min-height: 43px;
border: 0;
transition: all 0.2s ease;
border-radius: 2px;
outline: 0;
@include m-theme() {
color: themed($m-white);
}
&:hover {
transform: scale(1.02);
background-color: #55ccb2;
@include m-theme() {
box-shadow: 0 3px 3px -2px rgba(themed($m-black), 0.2),
0 2px 5px 0 rgba(themed($m-black), 0.14),
0 1px 7px 0 rgba(themed($m-black), 0.12);
}
}
&:active {
transform: scale(0.999);
@include m-theme() {
box-shadow: 0 3px 2px -2px rgba(themed($m-black), 0.2),
0 2px 3px 0 rgba(themed($m-black), 0.14),
0 1px 5px 0 rgba(themed($m-black), 0.12);
}
}
&:disabled,
&[disabled] {
cursor: default;
@include m-theme() {
background-color: themed($m-grey-100);
}
}
p {
margin: 0;
font-size: 17px;
font-weight: 300;
span {
font-size: 72px;
}
}
}
button {
outline: 0;
}
@keyframes blink {
0% {
opacity: 0.2;
transform: scale(2.1);
font-size: 77px;
}
20% {
opacity: 1;
transform: scale(1.2);
font-size: 90px;
}
100% {
opacity: 0.2;
transform: scale(1.3);
}
}
p.m-shadowboxLayout__buttonStatus--saving {
// line-height:70px
span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
// ---------------------------------------
m-shadowboxLayout.isForm {
margin-top: 69px;
.m-shadowboxLayout__body {
padding: 50px 70px;
}
.m-shadowboxLayout__footer {
padding: 30px 70px;
align-items: center;
}
.m-shadowboxHeader__wrapper {
height: auto;
}
}
// ---------------------------------------
@media screen and (max-width: $min-tablet) {
m-shadowboxLayout {
@include m-theme() {
......@@ -44,4 +138,42 @@ m-shadowboxHeader.isScrollable {
border-top: 1px solid themed($m-grey-100);
}
}
m-shadowboxLayout.isForm {
margin-top: 0px;
m-shadowboxHeader {
min-height: 80px;
}
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top: none;
}
}
.m-shadowboxLayout__body {
padding: 24px 24px 36px 24px;
}
.m-shadowboxLayout__footer {
justify-content: center;
padding: 24px 24px 48px 24px;
}
}
}
@media screen and (max-width: $max-mobile) {
m-shadowboxLayout.isForm {
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top-color: rgba(0, 0, 0, 0);
}
}
.m-shadowboxLayout__footer {
.m-shadowboxLayout__button {
min-width: 50%;
}
}
}
m-shadowboxLayout {
@include m-theme() {
box-shadow: none;
}
}
}
import { Component, OnInit, Input } from '@angular/core';
import { Component, Input, HostBinding } from '@angular/core';
@Component({
selector: 'm-shadowboxLayout',
templateUrl: './shadowbox-layout.component.html',
})
export class ShadowboxLayoutComponent implements OnInit {
export class ShadowboxLayoutComponent {
@Input() scrollableHeader: boolean = true;
@Input() hasHeader: boolean = true;
constructor() {}
@HostBinding('class.isForm') @Input() isForm: boolean = false;
ngOnInit() {}
constructor() {}
}
<button
class="m-shadowboxSubmitButton"
type="submit"
[disabled]="disabled"
[ngClass]="{ saving: saveStatus === 'saving' }"
>
<ng-content></ng-content>
</button>
.m-shadowboxSubmitButton {
min-width: 220px;
position: relative;
cursor: pointer;
padding: 10px 20px;
background-color: #4fc3a9;
min-height: 43px;
border: 0;
transition: all 0.2s ease;
border-radius: 2px;
outline: 0;
@include m-theme() {
color: themed($m-white);
}
&:hover {
&:not(:disabled) {
transform: scale(1.02);
background-color: #55ccb2;
@include m-theme() {
box-shadow: 0 3px 3px -2px rgba(themed($m-black), 0.2),
0 2px 5px 0 rgba(themed($m-black), 0.14),
0 1px 7px 0 rgba(themed($m-black), 0.12);
}
}
}
&:active {
&:not(:disabled) {
transform: scale(0.999);
background-color: #4fc3a9;
@include m-theme() {
box-shadow: 0 3px 2px -2px rgba(themed($m-black), 0.2),
0 2px 3px 0 rgba(themed($m-black), 0.14),
0 1px 5px 0 rgba(themed($m-black), 0.12);
}
}
}
&:disabled,
&[disabled] {
cursor: default;
&:not(.saving) {
@include m-theme() {
background-color: themed($m-grey-100);
}
}
}
}
button {
outline: 0;
}
[class*='m-shadowboxSubmitButton__status'] {
font-size: 17px;
font-weight: 300;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
transform: scale(1.05);
}
100% {
opacity: 0.2;
transform: scale(0.9);
}
}
.m-shadowboxSubmitButton__status--saving {
span {
display: inline-block;
height: 8px;
width: 8px;
margin: 0 6px;
border-radius: 50%;
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
@include m-theme() {
background-color: themed($m-white);
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@media screen and (max-width: $max-mobile) {
m-shadowboxSubmitButton {
min-width: 50%;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsFilterComponent } from './filter.component';
import { ShadowboxSubmitButtonComponent } from './shadowbox-submit-button.component';
describe('AnalyticsFilterComponent', () => {
let component: AnalyticsFilterComponent;
let fixture: ComponentFixture<AnalyticsFilterComponent>;
describe('ShadowboxSubmitButtonComponent', () => {
let component: ShadowboxSubmitButtonComponent;
let fixture: ComponentFixture<ShadowboxSubmitButtonComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsFilterComponent],
declarations: [ShadowboxSubmitButtonComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsFilterComponent);
fixture = TestBed.createComponent(ShadowboxSubmitButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-shadowboxSubmitButton',
templateUrl: './shadowbox-submit-button.component.html',
})
export class ShadowboxSubmitButtonComponent implements OnInit {
@Input() saveStatus: string = 'unsaved';
@Input() disabled: boolean = false;
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 }}
{{ menu.header.label }}
</div>
</div>
<div
......@@ -29,38 +29,38 @@
</div> -->
</div>
</a>
<!-- <ng-container *ngFor="let cat of cats"> -->
<div
class="m-sidebarMenu__catContainer expanded"
*ngIf="cat.header.permissionGranted"
class="m-sidebarMenu__menuContainer expanded"
*ngIf="menu.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" -->
<h3>{{ menu.header.label }}</h3>
</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
<nav class="m-sidebarMenu__linksContainer" *ngIf="menu.links">
<div class="m-sidebarMenu__link" *ngFor="let link of menu.links">
<ng-container *ngIf="link.permissionGranted">
<ng-container *ngIf="!link.newWindow">
<a
(click)="mobileMenuExpanded = false"
[routerLink]="link.path ? '/' + link.path : '../' + link.id"
routerLinkActive="selected"
[routerLinkActiveOptions]="{ exact: true }"
>
<span>{{ link.label }}</span></a
>
</ng-container>
<ng-container *ngIf="link.newWindow"
><a
[routerLink]="link.path ? ['/' + link.path] : ['../' + link.id]"
target="_blank"
class="newWindow"
><i class="material-icons">launch</i>
<span>{{ link.label }}</span></a
></ng-container
></ng-container
>
</div>
</nav>
</div>
<!-- </ng-container> -->
</div>
......@@ -7,7 +7,7 @@ m-sidebarMenu {
.m-sidebarMenu__userWrapper {
display: none;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__menuContainer {
.m-sidebarMenu__linksContainer {
display: block;
cursor: pointer;
......@@ -17,6 +17,15 @@ m-sidebarMenu {
.m-sidebarMenu__sidebar {
position: fixed;
top: 157px;
bottom: 0;
overflow-x: hidden;
overflow-y: scroll;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.m-sidebarMenu__header {
......@@ -35,7 +44,6 @@ m-sidebarMenu {
.m-sidebarMenu__linksContainer {
cursor: pointer;
display: none;
.m-sidebarMenu__link {
a {
display: block;
......@@ -45,6 +53,16 @@ m-sidebarMenu {
@include m-theme() {
color: themed($m-grey-300);
}
&.newWindow {
margin-top: 50px;
display: flex;
align-items: center;
i {
margin-right: 10px;
font-size: 13px;
font-weight: bold;
}
}
}
a.selected,
&:hover a {
......@@ -52,6 +70,9 @@ m-sidebarMenu {
color: themed($m-blue);
}
}
&:last-child {
padding-bottom: 36px;
}
}
}
}
......@@ -127,7 +148,7 @@ m-sidebarMenu {
&.mobileMenuExpanded {
left: 0;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__menuContainer {
.m-sidebarMenu__linksContainer {
display: none;
}
......
......@@ -4,7 +4,7 @@ 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';
import sidebarMenu from '../../../modules/pro/settings/sidebar-menu.default';
describe('SidebarMenuComponent', () => {
let component: SidebarMenuComponent;
......@@ -21,7 +21,7 @@ describe('SidebarMenuComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SidebarMenuComponent);
component = fixture.componentInstance;
component.catId = 'analytics';
component.menu = sidebarMenu;
// component.user = sessionMock.user;
fixture.detectChanges();
});
......
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 {
interface Menu {
header: MenuLink;
links?: MenuLink[];
expanded?: boolean;
}
export { MenuCategory };
export { Menu };
interface MenuLink {
id: string;
......@@ -16,6 +15,7 @@ interface MenuLink {
permissions?: string[];
permissionGranted?: boolean;
path?: string;
newWindow?: boolean;
}
export { MenuLink };
......@@ -24,11 +24,9 @@ export { MenuLink };
templateUrl: './sidebar-menu.component.html',
})
export class SidebarMenuComponent implements OnInit {
@Input() catId: string;
@Input() menu: Menu;
cat: MenuCategory;
mobileMenuExpanded = false;
// activeCat;
minds: Minds;
user;
userRoles: string[] = ['user'];
......@@ -38,38 +36,38 @@ export class SidebarMenuComponent implements OnInit {
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.cat = sidebarMenuCategories.find(cat => cat.header.id === this.catId);
this.getUserRoles();
this.grantPermissionsAndFindActiveCat();
this.grantPermissions();
}
getUserRoles() {
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
// TODO: define & handle other userRole options, e.g. pro, loggedIn
if (this.minds.user.pro) {
this.userRoles.push('pro');
}
}
grantPermissionsAndFindActiveCat() {
// this.cat.forEach(this.cat => {
this.cat.header['permissionGranted'] = this.cat.header.permissions
? this.checkForRoleMatch(this.cat.header.permissions)
grantPermissions() {
this.menu.header['permissionGranted'] = this.menu.header.permissions
? this.checkForRoleMatch(this.menu.header.permissions)
: true;
if (this.cat.links) {
this.cat.links.forEach(link => {
if (this.menu.links) {
this.menu.links.forEach(link => {
link['permissionGranted'] = link.permissions
? this.checkForRoleMatch(link.permissions)
: true;
if (link.id === ':username') {
link.id = this.user.username;
}
if (link.path) {
link.path = link.path.replace(':username', this.user.username);
}
});
}
// if (location.pathname.indexOf(this.cats.header.path) !== -1) {
// this.cats.header['expanded'] = true;
// this.activeCat = this.cat;
// } else {
// this.cat.header['expanded'] = false;
// }
// });
}
checkForRoleMatch(permissionsArray) {
......
......@@ -4,7 +4,6 @@ import { Material as MaterialService } from '../../services/ui';
import { MaterialTextfield } from './material/text-field';
import { MaterialUpload } from './material/upload';
import { MaterialSwitch } from './material/switch';
import { MaterialDateTimePickerDirective } from './material/datetimepicker.directive';
import { MaterialSlider } from './material/slider';
import { MaterialRadio } from './material/radio/radio';
......@@ -30,7 +29,6 @@ export const MDL_DIRECTIVES: any[] = [
MaterialTextfield,
MaterialUpload,
MaterialSwitch,
MaterialDateTimePickerDirective,
MaterialSlider,
MaterialRadio,
];
import {
Directive,
EventEmitter,
HostListener,
Input,
Output,
} from '@angular/core';
import { DatePipe } from '@angular/common';
import { default as DateTimePicker } from 'material-datetime-picker';
@Directive({
selector: '[mdl-datetime-picker]',
providers: [DatePipe],
})
export class MaterialDateTimePickerDirective {
@Input() date;
@Output() dateChange: EventEmitter<any> = new EventEmitter<any>();
private open: boolean = false;
private picker;
constructor(public datePipe: DatePipe) {}
@HostListener('click')
onHostClick() {
if (!this.open) {
let options = {};
if (this.date) {
options = { default: new Date(this.date).toString() };
}
this.picker = new DateTimePicker(options)
.on('submit', this.submitCallback.bind(this))
.on('close', this.close.bind(this));
this.open = true;
this.picker.open();
}
}
private submitCallback(value) {
this.dateChange.emit(this.datePipe.transform(value.format(), 'short'));
this.close();
}
private close() {
this.picker.off('submit', this.submitCallback);
this.picker.off('close', this.close);
this.open = false;
}
}
......@@ -14,7 +14,6 @@ export class BlockListService {
protected storage: Storage
) {
this.blocked = new BehaviorSubject(JSON.parse(this.storage.get('blocked')));
this.fetch();
}
fetch() {
......
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
export interface FormToast {
type?: 'success' | 'error' | 'warning' | 'info' | null;
message?: string;
}
@Injectable()
export class FormToastService {
private subject = new Subject<FormToast>();
constructor() {}
onToast(): Observable<FormToast> {
return this.subject.asObservable();
}
success(message: string) {
const toast: FormToast = {
message: message,
type: 'success',
};
this.trigger(toast);
}
error(message: string) {
const toast: FormToast = {
message: message,
type: 'error',
};
this.trigger(toast);
}
warn(message: string) {
const toast: FormToast = {
message: message,
type: 'warning',
};
this.trigger(toast);
}
inform(message: string) {
const toast: FormToast = {
message: message,
type: 'info',
};
this.trigger(toast);
}
trigger(toast: FormToast) {
if (!toast.type) {
toast.type = 'info';
}
this.subject.next(toast);
}
}
import { Injectable } from '@angular/core';
import { SiteService } from './site.service';
import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
@Injectable()
export class SsoService {
protected readonly minds = window.Minds;
constructor(
protected site: SiteService,
protected client: Client,
protected session: Session
) {
this.listen();
}
listen() {
this.session.isLoggedIn((is: boolean) => {
if (is) {
this.auth();
}
});
}
isRequired(): boolean {
return this.site.isProDomain;
}
async connect() {
try {
const connect: any = await this.client.postRaw(
`${this.minds.site_url}api/v2/sso/connect`
);
if (connect && connect.token && connect.status === 'success') {
const authorization: any = await this.client.post(
'api/v2/sso/authorize',
{
token: connect.token,
}
);
if (authorization && authorization.user) {
this.session.inject(authorization.user);
}
}
} catch (e) {
console.error(e);
}
}
async auth() {
try {
const connect: any = await this.client.post('api/v2/sso/connect');
if (connect && connect.token && connect.status === 'success') {
await this.client.postRaw(
`${this.minds.site_url}api/v2/sso/authorize`,
{
token: connect.token,
}
);
}
} catch (e) {
console.error(e);
}
}
}
......@@ -114,9 +114,7 @@
></minds-admin-monetization>
<minds-admin-programs *ngIf="filter == 'programs'"></minds-admin-programs>
<minds-admin-payouts *ngIf="filter == 'payouts'"></minds-admin-payouts>
<minds-admin-withdrawals
*ngIf="filter == 'withdrawals'"
></minds-admin-withdrawals>
<m-admin-withdrawals *ngIf="filter == 'withdrawals'"></m-admin-withdrawals>
<minds-admin-featured *ngIf="filter == 'featured'"></minds-admin-featured>
<minds-admin-tagcloud *ngIf="filter == 'tagcloud'"></minds-admin-tagcloud>
<m-admin--verify *ngIf="filter == 'verify'"></m-admin--verify>
......
<div class="mdl-grid m-admin-withdrawals" style="max-width: 900px">
<div class="m-admin-withdrawals">
<div class="m-admin-withdrawals__legend">
<ng-container *ngIf="user" i18n>
<b>@{{ user }}</b
>'s withdrawals
</ng-container>
<ng-container *ngIf="!user" i18n>
Pending Withdrawals
</ng-container>
</div>
<div
class="m-admin-withdrawals--user"
*ngIf="user"
i18n="@@M__ADMIN__WITHDRAWALS__USERNAME_TITLE"
class="m-admin-withdrawals__card"
*ngFor="let request of withdrawals; let i = index"
>
<b>@{{ user }}</b
>'s withdrawals
</div>
<ng-container *ngIf="request">
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Requested</div>
<table class="mdl-data-table mdl-js-data-table m-border" [mdl]>
<thead>
<tr class="m-admin--withdrawals--ledger-header">
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="@@M__ADMIN__WITHDRAWALS__DATE_COL"
>
Date
</th>
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="Transaction@@M__ADMIN__WITHDRAWALS__TX_COL"
>
Tx
</th>
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="@@M__ADMIN__WITHDRAWALS__AMOUNT_COL"
>
Amount
</th>
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="@@M__ADMIN__WITHDRAWALS__COMPLETED_COL"
>
Completed?
</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let withdrawal of withdrawals; let i = index">
<tr class="m-admin--withdrawals--ledger-row">
<td class="mdl-data-table__cell--non-numeric">
{{ withdrawal.timestamp * 1000 | date: 'short' }}
</td>
<td
class="mdl-data-table__cell--non-numeric m-admin--withdrawals--ledger-tx"
<div class="m-admin-withdrawalsCardCell__body">
{{ request.timestamp * 1000 | date: 'short' }}
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>User</div>
<div class="m-admin-withdrawalsCardCell__body">
<a [routerLink]="['/', request.user?.username]"
>@{{ request.user?.username }}</a
>
{{ withdrawal.tx }}
</td>
<td>{{ withdrawal.amount | token: 18 | number: '1.0-4' }}</td>
<td class="mdl-data-table__cell--non-numeric">
{{ withdrawal.completed ? 'Yes' : 'No' }}
</td>
</tr>
</ng-container>
<tr
class="m-admin--withdrawals--ledger-row m-wire-console--ledger-row-placeholder"
*ngIf="!inProgress && (!withdrawals || !withdrawals.length)"
>
<td
colspan="4"
style="text-align:center"
i18n="@@M__ADMIN__WITHDRAWALS__NO_WITHDRAWALS_NOTE"
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Signed up</div>
<div class="m-admin-withdrawalsCardCell__body">
{{ request.user?.time_created * 1000 | date: 'shortDate' }}
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Referrer</div>
<div class="m-admin-withdrawalsCardCell__body">
<ng-container *ngIf="request.referrer; else organicText">
<a [routerLink]="['/', request.referrer.username]"
>@{{ request.referrer.username }}</a
>
</ng-container>
<ng-template #organicText>
<ng-container>(organic)</ng-container>
</ng-template>
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Amount</div>
<div
class="m-admin-withdrawalsCardCell__body m-admin-withdrawalsCardCell__body--bolder"
>
No withdrawals to show. You can access a user's withdrawal ledger by
using the admin drop-down on their channel.
</td>
</tr>
</tbody>
</table>
{{ request.amount | token: 18 | number: '1.0-4' }}
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Status</div>
<div class="m-admin-withdrawalsCardCell__body">
{{ request.status?.replace('_', ' ') | uppercase }}
</div>
</div>
<div
class="m-admin-withdrawalsCard__cell m-admin-withdrawalsCard__cell--actions"
*ngIf="request.status === 'pending_approval'"
>
<div class="m-admin-withdrawalsCardCell__body">
<a
[routerLink]="[
'/admin/withdrawals',
{ user: request.user?.username }
]"
>History</a
>
<button
class="mf-button mf-button--smaller"
[disabled]="inProgress"
(click)="approve(request)"
>
Approve
</button>
<button
class="mf-button mf-button--destructive mf-button--smaller"
[disabled]="inProgress"
(click)="reject(request)"
>
Reject
</button>
</div>
</div>
</ng-container>
</div>
<div
class="m-admin-withdrawals__notice"
*ngIf="!inProgress && !withdrawals?.length"
>
No withdrawals to show. You can access a user's ledger by using the admin
drop-down on their channel.
</div>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
>
</infinite-scroll>
></infinite-scroll>
</div>
.m-admin-withdrawals {
.mdl-data-table {
width: 100%;
max-width: 900px;
margin: 16px auto 0;
.m-admin-withdrawals__legend {
font-size: 16px;
font-weight: 600;
text-align: center;
margin: 8px 0 16px;
letter-spacing: 1px;
}
.m-admin--withdrawals--ledger-tx {
font-size: 12px;
.m-admin-withdrawals__card {
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-width: 600px;
margin: 0 auto 32px;
padding: 8px;
border: 1px solid;
border-radius: 4px;
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
border-color: themed($m-grey-50);
}
}
.m-admin-withdrawalsCard__cell {
flex-grow: 1;
flex-shrink: 0;
box-sizing: border-box;
min-width: 25%;
padding: 8px;
&--actions {
width: 100%;
text-align: right;
padding-top: 24px;
margin-top: 16px;
border-top: 1px dotted;
@include m-theme() {
border-color: themed($m-grey-50);
}
}
a {
color: inherit;
font: inherit;
text-decoration: none;
border-bottom: 1px dotted;
cursor: pointer;
@include m-theme() {
border-color: themed($m-grey-300);
}
}
.m-admin-withdrawalsCardCell__label {
font-weight: 600;
margin-bottom: 2px;
@include m-theme() {
color: themed($m-grey-300);
}
}
.m-admin-withdrawalsCardCell__body {
&--bolder {
font-weight: bold;
letter-spacing: 0.5px;
}
}
&--actions .m-admin-withdrawalsCardCell__body {
> * {
display: inline-block;
vertical-align: middle;
margin: 0 8px 8px;
}
}
}
.m-admin-withdrawals--user {
width: 100%;
font-size: 14px;
font-weight: 300;
.m-admin-withdrawals__notice {
max-width: 600px;
margin: 32px auto;
padding: 16px;
border: 1px solid;
border-radius: 4px;
text-align: center;
padding: 8px 8px 32px;
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
border-color: themed($m-grey-50);
}
}
}
......@@ -4,7 +4,7 @@ import { ActivatedRoute } from '@angular/router';
@Component({
moduleId: module.id,
selector: 'minds-admin-withdrawals',
selector: 'm-admin-withdrawals',
templateUrl: 'withdrawals.component.html',
})
export class AdminWithdrawals {
......@@ -45,12 +45,19 @@ export class AdminWithdrawals {
this.inProgress = true;
const params = {
limit: 50,
offset: this.offset,
};
if (this.user) {
params['user'] = this.user;
} else {
params['status'] = 'pending_approval';
}
this.client
.get(`api/v2/admin/rewards/withdrawals`, {
limit: 50,
offset: this.offset,
user: this.user,
})
.get(`api/v2/admin/rewards/withdrawals`, params)
.then((response: any) => {
if (!response.withdrawals) {
this.inProgress = false;
......@@ -71,4 +78,58 @@ export class AdminWithdrawals {
this.inProgress = false;
});
}
async approve(withdrawal) {
if (!confirm("Do you want to approve this withdrawal? There's no UNDO.")) {
return;
}
this.inProgress = true;
try {
const endpoint = `api/v2/admin/rewards/withdrawals/${[
withdrawal.user_guid,
withdrawal.timestamp,
withdrawal.tx,
].join('/')}`;
await this.client.put(endpoint);
withdrawal.status = 'approved';
} catch (e) {
alert(
`There was an issue while approving withdrawal: ${(e && e.message) ||
'Unknown server error'}`
);
}
this.inProgress = false;
}
async reject(withdrawal) {
if (!confirm("Do you want to reject this withdrawal? There's no UNDO.")) {
return;
}
this.inProgress = true;
try {
const endpoint = `api/v2/admin/rewards/withdrawals/${[
withdrawal.user_guid,
withdrawal.timestamp,
withdrawal.tx,
].join('/')}`;
await this.client.delete(endpoint);
withdrawal.status = 'rejected';
} catch (e) {
alert(
`There was an issue while rejecting withdrawal: ${(e && e.message) ||
'Unknown server error'}`
);
}
this.inProgress = false;
}
}
......@@ -54,7 +54,6 @@ import { AnalyticsLayoutChartComponent } from './v2/layouts/layout-chart/layout-
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';
import { AnalyticsFilterComponent } from './v2/components/filter/filter.component';
import { AnalyticsChartComponent } from './v2/components/chart/chart.component';
import { AnalyticsTableComponent } from './v2/components/table/table.component';
import { AnalyticsDashboardService } from './v2/dashboard.service';
......@@ -71,16 +70,7 @@ const routes: Routes = [
path: 'analytics',
component: AnalyticsComponent,
children: [
{ path: '', redirectTo: 'channel', pathMatch: 'full' },
{
path: 'channel',
component: ChannelAnalyticsComponent,
children: [
{ path: '', redirectTo: 'activity', pathMatch: 'full' },
{ path: 'activity', component: ChannelGeneralAnalyticsComponent },
{ path: 'reach', component: ChannelReachAnalyticsComponent },
],
},
{ path: '', redirectTo: 'dashboard/traffic', pathMatch: 'full' },
{
path: 'admin',
component: AdminAnalyticsComponent,
......@@ -92,7 +82,7 @@ const routes: Routes = [
],
},
{
path: 'dashboard/',
path: 'dashboard',
redirectTo: 'dashboard/traffic',
pathMatch: 'full',
},
......@@ -165,7 +155,6 @@ const routes: Routes = [
AnalyticsLayoutSummaryComponent,
AnalyticsMetricsComponent,
AnalyticsFiltersComponent,
AnalyticsFilterComponent,
AnalyticsChartComponent,
AnalyticsTableComponent,
AnalyticsSearchComponent,
......
<div class="m-analytics__filtersContainer">
<!-- <ng-container *ngFor="let filter of filters$ | async"> -->
<ng-container *ngFor="let filter of filters">
<m-analytics__filter
class="filter"
<m-dropdownSelector
[filter]="filter"
[dropUp]="true"
></m-analytics__filter>
(selectionMade)="selectionMade($event)"
></m-dropdownSelector>
</ng-container>
</div>
......@@ -43,7 +43,12 @@ export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
});
}
// TODO: remove all of this once channel search is ready
selectionMade($event) {
this.analyticsService.updateFilter(
`${$event.filterId}::${$event.option.id}`
);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
<m-pageLayout navId="analytics">
<m-pageLayout [menu]="menu">
<div class="m-analyticsDashboard" *ngIf="ready$ | async" m-pageLayout__main>
<m-dashboardLayout>
<ng-container m-dashboardLayout__header>
......@@ -16,13 +16,14 @@
<m-analytics__search></m-analytics__search>
</div> -->
<!-- <div class="m-analyticsDashboard__channelFilter" *ngIf="session.isAdmin()">
<m-analytics__filter [filter]="channelFilter"></m-analytics__filter>
<m-dropdownSelector [filter]="channelFilter"></m-dropdownSelector>
</div> -->
<div class="m-analyticsDashboard__timespanFilter">
<m-analytics__filter
<m-dropdownSelector
[filter]="timespanFilter"
[showLabel]="false"
></m-analytics__filter>
(selectionMade)="filterSelectionMade($event)"
></m-dropdownSelector>
</div>
</div>
</ng-container>
......@@ -34,7 +35,7 @@
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
>Upgrade to Pro</a
>
<a
*ngIf="
......
......@@ -30,22 +30,14 @@ m-analytics__dashboard {
}
}
}
m-analytics__filter {
m-dropdownSelector {
margin: 0;
}
.m-analyticsFilter__wrapper {
.m-dropdownSelector__wrapper {
> * {
width: 180px;
}
}
.m-analyticsDashboard__timespanFilter {
.m-analytics__filterWrapper {
margin-top: 0px;
}
.m-analytics__filterLabel {
display: none;
}
}
}
.m-analyticsDashboard__description {
margin: 8px 16px 32px 0;
......@@ -53,22 +45,15 @@ m-analytics__dashboard {
@include m-theme() {
color: themed($m-grey-300);
}
a {
font-weight: 400;
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
}
@media screen and (max-width: $min-tablet) {
.m-dashboardLayout__header {
m-analytics__filter {
m-dropdownSelector {
margin: 0 16px 8px 0;
.m-analyticsFilter__wrapper {
.m-dropdownSelector__wrapper {
> * {
width: 160px;
width: 180px;
}
}
}
......@@ -80,9 +65,11 @@ m-analytics__dashboard {
@media screen and (max-width: $max-mobile) {
.m-dashboardLayout__header {
.m-analyticsFilter__wrapper {
> * {
width: 140px;
m-dropdownSelector {
.m-dropdownSelector__wrapper {
> * {
width: 160px;
}
}
}
}
......
......@@ -16,6 +16,8 @@ import { Session } from '../../../services/session';
import { AnalyticsDashboardService } from './dashboard.service';
import { Filter } from './../../../interfaces/dashboard';
import sidebarMenu from './sidebar-menu.default';
import { Menu } from '../../../common/components/sidebar-menu/sidebar-menu.component';
@Component({
selector: 'm-analytics__dashboard',
......@@ -24,6 +26,7 @@ import { Filter } from './../../../interfaces/dashboard';
providers: [AnalyticsDashboardService],
})
export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
menu: Menu = sidebarMenu;
paramsSubscription: Subscription;
ready$ = this.analyticsService.ready$;
......@@ -63,8 +66,6 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.updateCategory(cat);
if (cat === 'summary') {
this.layout = 'summary';
} else {
this.layout = 'chart';
}
});
......@@ -103,6 +104,12 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
}
}
filterSelectionMade($event) {
if ($event.filterId === 'timespan') {
this.analyticsService.updateTimespan($event.option.id);
}
}
updateTimespan(timespanId) {
// TODO: update url
// this.analyticsService.updateTimespan(timespanId);
......
const sidebarMenu = {
header: {
id: 'analytics',
label: 'Analytics',
permissions: ['admin', 'user'],
},
links: [
{
id: 'summary',
label: 'Summary',
permissions: ['admin'],
// path: '/some/path/outside/analytics/dashboard',
},
{
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'],
// },
],
};
export default sidebarMenu;
......@@ -196,14 +196,7 @@ export class ChannelSidebar {
}
get proSettingsRouterLink() {
const isAdmin = window.Minds.Admin;
const route: any[] = ['/pro/settings'];
if (isAdmin) {
route.push({ user: this.user.username });
}
const route: any[] = ['/pro/' + this.user.username + '/settings'];
return route;
}
}
......@@ -148,6 +148,10 @@ export class ChannelSortedComponent implements OnInit {
return;
}
if (activity.time_created > Date.now() / 1000) {
this.scheduledCount += 1;
}
this.entities.unshift(activity);
let feedItem = {
......
......@@ -117,7 +117,9 @@
>
<div
class="m-media-content--play-count"
*ngIf="entity['play:count'] || entity['play:count'] === 0"
*ngIf="
(false && entity['play:count']) || entity['play:count'] === 0
"
>
<span i18n="@@MINDS__MEDIA__PLAYS_COUNTER"
>{{ entity['play:count'] | number }} plays</span
......
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -23,6 +23,8 @@ export class SignupOnScrollModal implements OnInit, OnDestroy {
routerSubscription: Subscription;
@Input() disableScrollListener: true;
@ViewChild('modal', { static: true }) modal: SignupModal;
constructor(
......@@ -41,6 +43,10 @@ export class SignupOnScrollModal implements OnInit, OnDestroy {
}
listen() {
if (!this.disableScrollListener) {
return;
}
this.routerSubscription = this.router.events.subscribe(
(navigationEvent: NavigationEnd) => {
try {
......@@ -90,7 +96,10 @@ export class SignupOnScrollModal implements OnInit, OnDestroy {
}
unListen() {
this.routerSubscription.unsubscribe();
if (this.routerSubscription) {
this.routerSubscription.unsubscribe();
}
this.unlistenScroll();
}
......
<form class="m-form">
<p>
You can receive Bitcoin (BTC) payments via wire by inputing a receiver
You can receive Bitcoin (BTC) payments via wire by inputting a receiver
address below. Note: You may want to rotate this address frequently to avoid
3rd parties tracking your transactions.
</p>
......
......@@ -124,4 +124,6 @@
</div>
<m-overlay-modal #overlayModal></m-overlay-modal>
<m-modal-signup-on-scroll></m-modal-signup-on-scroll>
<m-modal-signup-on-scroll
[disableScrollListener]="false"
></m-modal-signup-on-scroll>
......@@ -229,7 +229,7 @@ export class ProChannelComponent implements OnInit, AfterViewInit, OnDestroy {
this.detectChanges();
try {
this.channel = await this.channelService.loadAndAuth(this.username);
this.channel = await this.channelService.load(this.username);
this.bindCssVariables();
this.shouldOpenWireModal();
......
......@@ -74,21 +74,16 @@ export class ProChannelService implements OnDestroy {
this.isLoggedIn$.unsubscribe();
}
async loadAndAuth(id: string): Promise<MindsUser> {
async load(id: string): Promise<MindsUser> {
try {
this.currentChannel = void 0;
const response = (await this.client.get(`api/v2/pro/channel/${id}`)) as {
channel;
me?;
};
this.currentChannel = response.channel;
if (this.site.isProDomain && response.me) {
this.session.login(response.me);
}
if (!this.currentChannel.pro_settings.tag_list) {
this.currentChannel.pro_settings.tag_list = [];
}
......@@ -111,7 +106,6 @@ export class ProChannelService implements OnDestroy {
try {
const response = (await this.client.get(`api/v2/pro/channel/${id}`)) as {
channel;
me?;
};
this.currentChannel = response.channel;
......@@ -163,7 +157,7 @@ export class ProChannelService implements OnDestroy {
throw new Error('No channel');
}
const endpoint = `api/v2/pro/content/${this.currentChannel.guid}/all/top`;
const endpoint = `api/v2/pro/content/${this.currentChannel.guid}/all`;
const qs = {
limit: params.limit || 24,
from_timestamp: params.offset || '',
......
......@@ -23,7 +23,7 @@
<m-pro__subscribeButton (onAction)="closeMenu()"></m-pro__subscribeButton>
</li>
<li>
<li class="m-proHamburgerMenu__linkWrapper">
<a
[routerLink]="feedRouterLink"
routerLinkActive="m-proHamburgerMenu__item--active"
......@@ -91,7 +91,7 @@
<li class="m-proHamburgerMenu__spacer"></li>
<ng-container *ngIf="items && items.length">
<li *ngFor="let item of items">
<li *ngFor="let item of items" class="m-proHamburgerMenu__linkWrapper">
<a
[class.m-proHamburgerMenu__item--active]="item.isActive()"
(click)="item.onClick(); closeMenu()"
......
......@@ -26,6 +26,7 @@ m-pro__hamburger-menu {
top: 30px;
right: 40px;
padding: 8px;
cursor: pointer;
}
> ul {
......@@ -47,7 +48,9 @@ m-pro__hamburger-menu {
text-transform: uppercase;
font-weight: 700;
letter-spacing: 1px;
&.m-proHamburgerMenu__linkWrapper {
cursor: pointer;
}
&.m-proHamburgerMenu__logo {
height: 100px;
......@@ -114,3 +117,7 @@ m-pro__hamburger-menu {
body.hamburger-menu--open {
overflow: hidden;
}
.m-proHamburgerMenu__trigger {
cursor: pointer;
}
......@@ -34,8 +34,6 @@ export class ProChannelListComponent implements OnInit, OnDestroy {
entities: any[] = [];
algorithm: string;
query: string;
period: string;
......@@ -97,7 +95,7 @@ export class ProChannelListComponent implements OnInit, OnDestroy {
default:
throw new Error('Unknown type');
}
this.algorithm = params['algorithm'] || 'top';
this.query = params['query'] || '';
this.period = params['period'] || '';
this.selectedHashtag = params['hashtag'] || 'all';
......@@ -149,7 +147,7 @@ export class ProChannelListComponent implements OnInit, OnDestroy {
params.sync = 1;
}
let url = `api/v2/pro/content/${this.channelService.currentChannel.guid}/${this.type}/${this.algorithm}`;
let url = `api/v2/pro/content/${this.channelService.currentChannel.guid}/${this.type}`;
try {
this.feedsService
......
......@@ -9,13 +9,17 @@
</h1>
<h2 ngPreserveWhitespaces i18n>
The ultimate platform for <em>creators and brands</em>
A new revenue model for <em>creators</em>
</h2>
<p class="m-marketing__description" i18n>
Earn revenue for your content and upgrade your channel into a full
blown website with professional tools you need to do what you love.
</p>
<ul class="m-marketing__points" i18n>
<li i18n>Get paid for your traffic and referrals</li>
<li i18n>Launch your own website</li>
<li i18n>
Receive multi-currency tips and subscription payments from fans
</li>
<li i18n>Supports video, images, blogs and more</li>
</ul>
<div class="m-proMarketing__subscription">
<m-pro--subscription
......@@ -54,7 +58,8 @@
<ul class="m-marketing__points">
<li i18n>
$1 for every 1,000 pageviews
$1 for every 1,000 pageviews on your content (increases to $5
between 100K and 1M pageviews/month)
</li>
<li i18n>
......@@ -62,9 +67,16 @@
</li>
<li i18n>
25% commission on all referred sales
25% commission on all referred sales of Pro or
<a routerLink="/tokens">Minds Tokens</a>
</li>
</ul>
<a
class="m-marketing__subText"
routerLink="/minds/blog/how-to-earn-money-with-pro-1046186757943361536"
>Learn More</a
>
</div>
<div
class="m-grid__column-6 m-grid__column-12--mobile m-marketing__image"
......@@ -95,7 +107,7 @@
<p class="m-marketing__description" i18n>
Morph your channel into a full blown website with your own subdomain
or custom domain, logo, theme, categories and footer.
or custom domain, newsfeed, logo, theme, categories and footer.
</p>
<ul class="m-marketing__points">
......@@ -141,16 +153,18 @@
<div
class="m-grid__column-6 m-grid__column-12--mobile m-marketing__body"
>
<h2 i18n>Regain your freedom</h2>
<h2 i18n>Regain control of your business online</h2>
<p class="m-marketing__description" i18n>
Tired of the manipulation and unfair treatment from traditional
social networks?
Tired of demonetization, censorship and unfair treatment from
traditional social networks? Minds offers a fully transparent,
privacy-focused platform with no bias, hidden algorithms or
censorship.
</p>
<p class="m-marketing__description" i18n>
Minds offers a fully transparent, privacy-focused platform with no
bias, hidden algorithms or censorship.
Leverage the blockchain and crypto payments to eliminate the
middleman and maintain autonomy over your revenue streams.
</p>
</div>
<div
......@@ -166,56 +180,6 @@
</div>
</div>
<!-- Section 5 -->
<div class="m-marketing__section m-marketing__section--style-4">
<div class="m-grid m-marketing__wrapper">
<div
class="m-grid__column-6 m-grid__column-12--mobile m-marketing__body"
>
<h2 i18n>A sustainable revenue model for everyone.</h2>
<p class="m-marketing__description" i18n>
We are you. We are sharing our revenue with you to directly
compensate for contributions to the network. As a community-owned
company, it is essential to keep everyone's incentives aligned and
inspired.
</p>
<p class="m-marketing__description" i18n>
The more traffic and revenue that you drive, the more you earn and
the healthier the ecosystem becomes.
</p>
<div class="m-marketing__actionButtons">
<a
class="mf-button mf-button--smaller mf-button--hollow-mono"
routerLink="/upgrades"
target="_blank"
i18n
>
Learn About Upgrades
</a>
</div>
</div>
<div
class="m-grid__column-6 m-grid__column-12--mobile m-marketing__image"
>
<span>
<img
class="m-marketing__image--1"
[src]="cdnAssetsUrl + 'assets/product-pages/pro/pro-5.jpg'"
/>
<img
class="m-marketing__image--2"
[src]="cdnAssetsUrl + 'assets/product-pages/pro/pro-5-ux.png'"
/>
</span>
</div>
</div>
</div>
<!-- Final section -->
<div class="m-marketing__section m-marketing__section--tail">
......@@ -234,7 +198,9 @@
<h3 i18n>Professional media</h3>
<p class="m-marketing__description" i18n>
Upload and share up to 500 GB of HD video and photography (1080p).
Upload videos or images up to 60 minutes in length, 5 GB in size and
1080p in resolution. Publish your own blogs with our easy-to-use
editor.
</p>
</div>
......
......@@ -45,9 +45,11 @@ const routes: Routes = [
component: ProMarketingComponent,
},
{
path: 'settings',
component: ProSettingsComponent,
path: ':username/settings',
redirectTo: ':username/settings/general',
pathMatch: 'full',
},
{ path: ':username/settings/:tab', component: ProSettingsComponent },
{
path: ':username',
component: ProChannelComponent,
......
const sidebarMenu = {
header: {
id: 'pro_settings',
label: 'Pro Settings',
permissions: ['pro'],
},
links: [
{
id: 'general',
label: 'General',
},
{
id: 'theme',
label: 'Theme',
},
{
id: 'assets',
label: 'Assets',
},
{
id: 'hashtags',
label: 'Hashtags',
},
{
id: 'footer',
label: 'Footer',
},
{
id: 'domain',
label: 'Domain',
},
{
id: 'payouts',
label: 'Payouts',
},
{
id: 'subscription',
label: 'Pro Subscription',
path: 'pro',
},
{
id: ':username',
label: 'View Pro Channel',
path: 'pro/:username',
newWindow: true,
},
],
};
export default sidebarMenu;
......@@ -56,6 +56,14 @@
Coming soon
</button>
</ng-template>
<p class="m-marketing__subText">
By purchasing PRO you agree to
<a
href="https://cdn-assets.minds.com/pro-terms-27-11-2019.pdf"
target="_blank"
>Terms &amp; Conditions</a
>.
</p>
</div>
</div>
......
......@@ -109,6 +109,17 @@
<span i18n="@@M__ACTION__SUGGESTED_HASHTAGS">Suggested Hashtags</span>
</a> -->
<a
class="m-page--sidebar--navigation--item"
*ngIf="session.getLoggedInUser().pro"
[routerLink]="[
'/pro/' + session.getLoggedInUser().username + '/settings'
]"
>
<i class="material-icons">business_center</i>
<span i18n>Pro Settings</span>
</a>
<a
class="m-page--sidebar--navigation--item"
(click)="openReferralsModal()"
......
......@@ -69,7 +69,7 @@
#withdrawPending
i18n="@@WALLET__TOKENS__WITHDRAW__LEDGER__PENDING_LABEL"
>
PENDING
{{ withdrawal.status.replace('_', ' ') | uppercase }}
</ng-template>
<div class="mdl-layout-spacer"></div>
......
......@@ -8,20 +8,23 @@
class="m-border mdl-color--white m-token-withdraw"
*ngIf="session.getLoggedInUser().rewards"
>
<p>
<ng-container i18n="@@WALLET__TOKENS__WITHDRAW__REQUEST_DESC">
You can request to withdraw your token rewards to your 'onchain' wallet
below. Note: a small amount of ETH will be charged to cover the
transaction fee. Withdrawals may take a few hours to complete.
</ng-container>
<ng-container
*ngIf="withholding"
i18n="@@WALLET__TOKENS__WITHDRAW__REQUEST_UNAVAILABLE_DESC"
>
{{ withholding | number }} tokens are unavailable due to credit card
payment. They will be released after 30 days the payment occurred.
</ng-container>
<p i18n>
You can request to withdraw up to {available, plural, =1
{{{available | number}} token} other {{{available | number}} tokens}} from
your rewards to your <b>OnChain</b> wallet below.
</p>
<p *ngIf="withholding" i18n>
{withholding, plural, =1
{{{withholding | number}} token} other {{{withholding | number}} tokens}}
are unavailable due to credit card payment. They will be released after 30
days the payment occurred.
</p>
<p class="m-token-withdraw__note" i18n>
Note: a small amount of ETH will be charged to cover the transaction fee.
Withdrawals <b>go through an approval process</b>
and may take up to 72 hours to complete.
</p>
<div class="m-token-withdraw--form">
......
......@@ -2,9 +2,18 @@
margin-bottom: $minds-padding;
padding: $minds-padding * 2;
> p {
margin-bottom: 8px;
&.m-token-withdraw__note {
font-size: 12px;
opacity: 0.7;
}
}
.m-token-withdraw--form {
display: flex;
margin-bottom: $minds-padding;
margin: 16px 0 8px;
.m-token-withdraw--input,
.m-token-withdraw--submit-button {
......
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.