...
 
Commits (59)
......@@ -229,7 +229,7 @@ review:start:
image: minds/helm-eks:latest
script:
- aws eks update-kubeconfig --name=sandbox
- git clone --branch=sandbox-wip https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/minds/helm-charts.git
- git clone --branch=master https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/minds/helm-charts.git
- "helm upgrade \
--install \
--reuse-values \
......
......@@ -19,6 +19,7 @@
"assets": ["src/assets", "src/favicon.ico"],
"styles": [
"node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"node_modules/plyr/dist/plyr.css",
"node_modules/material-design-icons/iconfont/material-icons.css",
"src/main.css"
],
......
// import 'cypress-file-upload';
context('Blogs', () => {
const closeButton = '[data-cy=data-minds-conversation-close]';
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
......@@ -8,6 +11,14 @@ context('Blogs', () => {
return cy.login(true);
}
});
// ensure no messenger windows are open.
cy.get('body').then(($body) => {
if ($body.find(closeButton).length) {
cy.get(closeButton)
.click({multiple: true});
}
});
});
beforeEach(() => {
......@@ -208,7 +219,7 @@ context('Blogs', () => {
cy.get('.m-blog--title').contains(title);
cy.get('.minds-blog-body p').contains(body);
};
it('should not be able to create a new blog if no title or banner are specified', () => {
cy.visit('/blog/edit/new');
cy.get('.m-button--submit').click();
......
import generateRandomId from '../../support/utilities';
/**
* @author Ben Hayward
* @create date 2019-08-09 14:42:51
......@@ -6,6 +8,9 @@
*/
context('Comment Threads', () => {
const testUsername = generateRandomId();
const testPassword = generateRandomId() + 'rR.7';
const testMessage = {
1: 'test tier 1',
2: 'test tier 2',
......@@ -17,8 +22,6 @@ context('Comment Threads', () => {
const deletePostButton = ".m-modal-confirm-buttons > button:contains('Delete')";
const postCommentButton = 'm-comment__poster > div > div.minds-body > div > div > a.m-post-button';
const thumbsUpCounters = '.m-comment__toolbar > div > minds-button-thumbs-up > a > span';
const thumbsDownCounters = '.m-comment__toolbar > div > minds-button-thumbs-down > a > span';
// pass in tier / tree depth.
const replyButton = `minds-activity:first .m-comment__toolbar > div > span`;
......@@ -26,6 +29,11 @@ context('Comment Threads', () => {
const commentInput = `minds-activity:first m-text-input--autocomplete-container > minds-textarea > div`;
const commentContent = `minds-activity:first m-comments__tree .m-comment__bubble > p`;
const thumbsUpCounters = '[data-cy=data-minds-thumbs-up-counter]' //'minds-button-thumbs-up > a > span';
const thumbsDownCounters = '[data-cy=data-minds-thumbs-down-counter]';
const thumbsUpButton = '[data-cy=data-minds-thumbs-up-button]'
const thumbsDownButton = '[data-cy=data-minds-thumbs-down-button]'
before(() => {
//make a post new.
cy.getCookie('minds_sess')
......@@ -44,6 +52,10 @@ context('Comment Threads', () => {
beforeEach(()=> {
cy.preserveCookies();
cy.server();
cy.route('GET', '**/api/v2/comments/**').as('commentsOpen');
cy.route('POST', '**/api/v1/comments/**').as('postComment');
cy.route('PUT', '**/api/v1/thumbs/**').as('thumbsPut');
});
after(() => {
......@@ -63,13 +75,21 @@ context('Comment Threads', () => {
cy.get(commentContent).contains(testMessage[1]);
//Add the second level of comments
cy.get(replyButton).click();
cy.get(replyButton)
.click()
.wait('@commentsOpen')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
cy.get(commentInput)
.first()
.type(testMessage[2]);
cy.get(postCommentButton)
.first()
.click();
cy.get(commentContent).contains(testMessage[2]);
......@@ -78,30 +98,108 @@ context('Comment Threads', () => {
.find('m-comments__tree m-comments__thread m-comment')
.find('m-comments__thread m-comment:nth-child(2) .m-comment__toolbar > div > span')
.last()
.click();
.click()
.wait('@commentsOpen')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
cy.get(commentInput)
.first()
.type(testMessage[3]);
cy.get(postCommentButton)
.first()
.click();
cy.get(commentContent).contains(testMessage[3]);
//click thumbs up and down
cy.get('.m-comment__toolbar')
.find('minds-button-thumbs-up')
.click({multiple: true});
cy.get(commentContent).contains(testMessage[3]);
// Waiting on component init here.
// If still not fully loaded will not break,
// but may mean some of the buttons aren't tested.
cy.wait(1000);
// scope further get requests down to within the comments toolbar
// avoids clicking thumbs in activity feed.
cy.get('.m-comment__toolbar').within(($list) => {
cy.get('.m-comment__toolbar')
.find('minds-button-thumbs-down')
// thumbs up and down
cy.get(thumbsUpButton)
.click({multiple: true});
// check the values
cy.get(thumbsDownButton)
.click({multiple: true});
// check counters
cy.get(thumbsUpCounters)
.each((counter) => expect(counter.context.innerHTML).to.eql('1'));
.each((counter) => {
expect(counter[0].innerHTML).to.eql('1');
});
});
cy.get(thumbsDownCounters)
.each((counter) => expect(counter.context.innerHTML).to.eql('1'));
.each((counter) => {
expect(counter[0].innerHTML).to.eql('1');
});
});
it('should allow the user to make a mature comment', () => {
// type message
cy.get('minds-textarea')
.last()
.type("naughty message");
// click mature
cy.get('.m-mature-button')
.last()
.click();
// post and await response
cy.get('.m-post-button')
.last()
.click()
.wait('@postComment')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
// Making sure we don't act upon other comments
cy.get('.m-comment__bubble').parent().within($list => {
cy.contains('naughty message')
.should('not.have.class', 'm-mature-text');
cy.get('.m-redButton')
.click();
cy.contains('naughty message')
.should('have.class', 'm-mature-text');
});
// get share link
cy.get(postMenu).click();
cy.contains('Share').click();
// store share link
cy.get('.m-share__copyableLinkText')
.invoke('val')
.then(val => {
// log out
cy.logout();
// visit link
cy.visit(val);
// assert toggle works.
cy.contains('naughty message')
.should('have.class', 'm-mature-text');
cy.get('.m-mature-text-toggle')
.click();
cy.contains('naughty message')
.should('not.have.class', 'm-mature-text');
});
});
})
......@@ -116,18 +116,47 @@ context('Discovery', () => {
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Other')").click();
});
it('should allow the user to filter by a single hashtag', () => {
it('should allow the user to turn off single hashtag filter and view all posts', () => {
cy.visit('/newsfeed/global/top');
cy.get('m-hashtagssidebarselector__item')
.first()
.click();
});
it('should allow the user to turn off single hashtag filter and view all posts', () => {
it.skip('should allow the user to toggle a single hashtag and then toggle back to the initial feed', () => {
cy.visit('/newsfeed/global/top');
cy.get('m-hashtagssidebarselector__item')
.first()
.find('.m-hashtagsSidebarSelectorList__visibility > i')
.click();
})
// get first label value
cy.get('.m-hashtagsSidebarSelectorList__label').first().invoke('text').then((text) => {
// repeat twice to capture full cycle.
Cypress._.times(2, (i) => {
// split hashtag off of label text
let label = text.split('#')[1];
// click switch
toggleFirstVisibilitySwitch();
// check location name has updated
cy.location('pathname')
.should('eq', `/newsfeed/global/top;period=12h;hashtag=${label}`);
// click switch
toggleFirstVisibilitySwitch();
// check location name has updated
cy.location('pathname')
.should('eq', `/newsfeed/global/top;period=12h`);
});
});
});
// click first visibility switch
const toggleFirstVisibilitySwitch = () => {
cy.get('m-hashtagssidebarselector__item')
.first()
.find('.m-hashtagsSidebarSelectorList__visibility > i')
.click();
}
})
......@@ -41,14 +41,19 @@ context('Groups', () => {
cy.get('.m-groups-save > button').contains('Create').click();
cy.route("POST", "**/api/v1/groups/group/*/banner*").as("postBanner");
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
}).wait('@postBanner').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
// get current groups count of sidebar
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
}).wait('@postBanner').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
//check count changed.
cy.get('.m-groupSidebarMarkers__list').children().should('have.length', size + 1);
});
cy.get('.m-groupInfo__name').contains('test');
cy.get('.m-groupInfo__description').contains('This is a test');
......@@ -73,11 +78,9 @@ context('Groups', () => {
})
it('should be able to toggle conversation and comment on it', () => {
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.get(`m-group--sidebar-markers li:nth-child(${size - 1})`).click();
});
// toggle the conversation
cy.get('.m-groupGrid__right').should('be.visible');
......@@ -98,9 +101,9 @@ context('Groups', () => {
})
it('should post an activity inside the group and record the view when scrolling', () => {
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.get(`m-group--sidebar-markers li:nth-child(${size - 1})`).click();
});
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
......@@ -128,14 +131,18 @@ context('Groups', () => {
});
it('should delete a group', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.get(`m-group--sidebar-markers li:nth-child(${size - 1})`).click();
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.get('minds-groups-settings-button ul.minds-dropdown-menu > li:nth-child(8)').contains('Delete Group').click();
cy.get('minds-groups-settings-button m-modal .mdl-button--raised').contains('Confirm').click();
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.contains('Delete Group').click();
cy.contains('Confirm').click();
cy.location('pathname').should('eq', '/groups/member');
cy.location('pathname').should('eq', '/groups/member');
cy.get('.m-groupSidebarMarkers__list').children().should('have.length', size - 1);
});
})
})
......@@ -5,36 +5,36 @@ context('Login', () => {
})
it('should login', () => {
cy.get('.m-v2-topbar__Container__LoginWrapper > a').click();
cy.get('.m-v2-topbar__Container__LoginWrapper > a').contains('Login').click();
cy.location('pathname').should('eq', '/login');
// it should have a login form
cy.get('.m-login').should('be.visible');
cy.get('.m-login__wrapper').should('be.visible');
cy.get('minds-form-login .m-login-box .mdl-cell:first-child input').type(Cypress.env().username);
cy.get('minds-form-login .m-login-box .mdl-cell:last-child input').type(Cypress.env().password);
cy.get('minds-form-login .m-btn--login').click();
cy.get('minds-form-login .mf-button--alt').click();
cy.location('pathname')
.should('eq', '/newsfeed/subscriptions');
})
it('should fail to login because of incorrect password', () => {
cy.get('.m-v2-topbar__Container__LoginWrapper > a').click();
cy.get('.m-v2-topbar__Container__LoginWrapper > a').contains('Login').click();
cy.location('pathname').should('eq', '/login');
// it should have a login form
cy.get('.m-login').should('be.visible');
cy.get('.m-login__wrapper').should('be.visible');
cy.get('minds-form-login .m-login-box .mdl-cell:first-child input').type(Cypress.env().username);
cy.get('minds-form-login .m-login-box .mdl-cell:last-child input').type(Cypress.env().password + '1');
cy.get('minds-form-login .m-btn--login').click();
cy.get('minds-form-login .mf-button--alt').click();
cy.wait(500);
......
......@@ -661,4 +661,56 @@ context('Newsfeed', () => {
});
});
// enable once failing tests are fixed
it.skip('should post an nsfw activity when value is held by the selector (is blue) but it has not been clicked yet', () => {
// click on nsfw dropdown
cy.get(
'minds-newsfeed-poster m-nsfw-selector .m-dropdown--label-container'
).click();
// select Nudity
cy.get('minds-newsfeed-poster m-nsfw-selector .m-dropdownList__item')
.contains('Nudity')
.click();
// click away
cy.get('minds-newsfeed-poster m-nsfw-selector .minds-bg-overlay').click();
// navigate away from newsfeed and back.
cy.get('[data-cy=data-minds-nav-wallet-button]').first().click(); // bottom bar exists, so take first child
cy.get('[data-cy=data-minds-nav-newsfeed-button]').first().click();
newActivityContent('This is a nsfw post');
postActivityAndAwaitResponse(200);
// should have the mature text toggle
cy.get(
'.minds-list > minds-activity:first-child .message .m-mature-text-toggle'
).should('not.have.class', 'mdl-color-text--red-500');
cy.get(
'.minds-list > minds-activity:first-child .message .m-mature-message-content'
).should('have.class', 'm-mature-text');
// click the toggle
cy.get(
'.minds-list > minds-activity:first-child .message .m-mature-text-toggle'
).click();
// text should be visible now
cy.get(
'.minds-list > minds-activity:first-child .message .m-mature-text-toggle'
).should('have.class', 'mdl-color-text--red-500');
cy.get(
'.minds-list > minds-activity:first-child .message .m-mature-message-content'
).should('not.have.class', 'm-mature-text');
cy.get(
'.minds-list > minds-activity:first-child .message .m-mature-message-content'
).contains('This is a nsfw post');
deleteActivityFromNewsfeed();
});
});
context('Onboarding', () => {
const email = 'test@minds.com';
const password = 'Passw0rd!';
const name = "Tester";
const description = "I am a tester, with a not so lengthy description";
const welcomeText = "Welcome to Minds!";
const usernameField = 'minds-form-register #username';
const emailField = 'minds-form-register #email';
const passwordField = 'minds-form-register #password';
const password2Field = 'minds-form-register #password2';
const nameField = '#display-name';
const descriptionfield = '#description';
const phoneNumberInput = '#phone';
const countryDropdown = 'm-phone-input--country > div';
const ukOption = 'm-phone-input--country > ul > li:nth-child(2)';
const dialcode = '.m-phone-input--dial-code';
const checkbox = 'minds-form-register label:nth-child(2) .mdl-ripple--center';
const submitButton = 'minds-form-register .mdl-card__actions button';
const nextButton = '.m-channelOnboarding__next';
const submitPhoneButton = 'm-channel--onboarding--rewards > div > div > button';
const loadingSpinner = '.mdl-spinner__gap-patch';
const getTopic = (i) => `m-onboarding--topics > div > ul > li:nth-child(${i}) span`;
before(() => {
cy.clearCookies();
cy.visit('/login');
//type values
cy.get(usernameField).focus().type(Math.random().toString(36).replace('0.', ''));
cy.get(emailField).focus().type(email);
cy.get(passwordField).focus().type(password);
cy.get(password2Field).focus().type(password);
cy.get(checkbox).click();
//submit
cy.get(submitButton).click();
//onboarding modal shown
cy.get('m-onboarding--topics > div > h2:nth-child(1)')
.contains(welcomeText);
});
it('should allow a user to run through onboarding modals', () => {
//select topics
cy.get(getTopic(3)).click().should('have.class', 'selected')
cy.get(getTopic(4)).click().should('have.class', 'selected')
cy.get(getTopic(5)).click().should('have.class', 'selected')
//click
cy.get(nextButton).click();
//TODO: Skipped over for now as subscribed channels is not working on staging environment.
cy.get(nextButton).click();
cy.get(nameField).clear().type(name);
cy.get(descriptionfield).type(description);
cy.get(nextButton).click();
//set dialcode
cy.get(countryDropdown).click();
cy.get(ukOption).click();
cy.get(dialcode).contains('+44');
//type number
cy.get(phoneNumberInput).type('7700000000');
//submit and check loading spinner.
cy.get(submitPhoneButton).click();
cy.get(loadingSpinner).should('be.visible');
cy.get(nextButton).click();
});
});
context('Onboarding', () => {
const remindText = 'remind test text';
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit(`/onboarding`);
// create two test groups
});
beforeEach(() => {
cy.preserveCookies();
});
it('should go through the process of onboarding', () => {
// notice should appear
cy.get('h1.m-onboarding__noticeTitle').contains('Welcome to the Minds Community');
cy.get('h2.m-onboarding__noticeTitle').contains(`@${Cypress.env().username}`);
// should redirect to /hashtags
cy.get('.m-onboarding__form button.mf-button').contains("Let's Get Setup").click();
cy.wait(1000);
// should be in the hashtags step
// should have a Profile Setup title
cy.get('.m-onboarding__form > h2').contains('Profile Setup');
// should have a progressbar, with the hashtags step highlighted
cy.get('.m-onboardingProgressbar__item--selected span').contains('1');
cy.get('.m-onboardingProgressbar__item--selected span').contains('Hashtags');
// should have a description
cy.get('.m-onboarding__form .m-onboarding__description').contains('Select some hashtags that are of interest to you.');
// should have a list of selectable hashtags
cy.get('.m-hashtags__list li.m-hashtagsList__item').contains('Art').click();
cy.get('.m-hashtags__list li.m-hashtagsList__item.m-hashtagsList__item--selected').contains('Art');
cy.get('.m-hashtags__list li.m-hashtagsList__item').contains('Journalism').click();
cy.get('.m-hashtags__list li.m-hashtagsList__item.m-hashtagsList__item--selected');
cy.get('.m-hashtags__list li.m-hashtagsList__item').contains('Music').click();
cy.get('.m-hashtags__list li.m-hashtagsList__item.m-hashtagsList__item--selected');
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Continue').click();
// should be in the info step
cy.get('.m-onboardingProgressbar__item--selected span').contains('2');
cy.get('.m-onboardingProgressbar__item--selected span').contains('Info');
// should have a Mobile Phone Number input
cy.get('.m-onboarding__controls .m-onboarding__control label').contains('Mobile Phone Number');
// open country dropdown
cy.get('.m-onboarding__controls .m-phone-input--selected-flag').click();
// click on UK
cy.get('.m-phone-input--country-list li span[data-minds=54]').click();
// Uk should be selected
cy.get('.m-phone-input--selected-flag .m-phone-input--dial-code').contains('+54');
// add the number
cy.get('#phone').type('012345678');
// should have a Location input
cy.get('.m-onboarding__controls > .m-onboarding__control label[data-minds=location]').contains('Location');
cy.get('.m-onboarding__controls > .m-onboarding__control input[data-minds=locationInput]').type('London');
// should have Date of Birth inputs
cy.get('.m-onboarding__controls > .m-onboarding__control label[data-minds=dateOfBirth]').contains('Date of Birth');
// open month selection and pick February
cy.get('.m-onboarding__controls > .m-onboarding__control select[data-minds=monthDropdown]').select('February');
// open day selection and pick 2nd
cy.get('.m-onboarding__controls > .m-onboarding__control select[data-minds=dayDropdown]').select('2');
// open year selection and pick 1991
cy.get('.m-onboarding__controls > .m-onboarding__control select[data-minds=yearDropdown]').select('1991');
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Continue').click();
// should be in the Groups step
// should have a groups list
// cy.get('.m-groupList__list').should('exist');
// clicking on a group join button should join the group
// cy.get('.m-groupList__list .m-groupList__item:first-child .m-join__subscribe').contains('add').click();
// // button should change to a check, and clicking on it should leave the group
// cy.get('.m-groupList__list .m-groupList__item:first-child .m-join__subscribed').contains('check').click();
// cy.get('.m-groupList__list .m-groupList__item:first-child .m-join__subscribe i').contains('add');
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Continue').click();
// should be in the Channels step
// should have a channels list
// cy.get('.m-channelList__list').should('exist');
// // clicking on a group join button should join the group
// cy.get('.m-channelList__list .m-channelList__item:first-child .m-join__subscribe').contains('add').click();
// // button should change to a check, and clicking on it should leave the channel
// cy.get('.m-channelList__list .m-channelList__item:first-child .m-join__subscribed').contains('check').click();
// cy.get('.m-channelList__list .m-channelList__item:first-child .m-join__subscribe i').contains('add');
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Finish').click();
// should be in the newsfeed
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
});
});
This diff is collapsed.
......@@ -5,11 +5,6 @@ context('Registration', () => {
const username = generateRandomId();
const password = `${generateRandomId()}0oA!`;
const email = 'test@minds.com';
const noSymbolPass = 'Passw0rd';
const welcomeText = "Welcome to Minds!";
const passwordDontMatch = "Passwords must match.";
const passwordInvalid = " Password must have more than 8 characters. Including uppercase, numbers, special characters (ie. !,#,@), and cannot have spaces. ";
const usernameField = 'minds-form-register #username';
const emailField = 'minds-form-register #email';
......@@ -20,8 +15,8 @@ context('Registration', () => {
beforeEach(() => {
cy.clearCookies();
cy.visit('/login');
cy.location('pathname').should('eq', '/login');
cy.visit('/register');
cy.location('pathname').should('eq', '/register');
cy.server();
cy.route("POST", "**/api/v1/register").as("register");
});
......@@ -39,55 +34,21 @@ context('Registration', () => {
cy.get(usernameField)
.focus()
.type(username);
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password);
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton)
.click()
.wait('@register').then((xhr) => {
expect(xhr.status).to.equal(200);
});
//onboarding modal shown
cy.contains(welcomeText);
});
it('should display an error if password is invalid', () => {
cy.get(usernameField)
.focus()
.type(generateRandomId());
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(noSymbolPass);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(noSymbolPass);
cy.get(checkbox)
.click({force: true});
......@@ -98,37 +59,22 @@ context('Registration', () => {
expect(xhr.status).to.equal(200);
});
cy.scrollTo('top');
cy.contains(passwordInvalid);
cy.wait(500);
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
});
it('should display an error if passwords do not match', () => {
cy.get(usernameField)
.focus()
.type(generateRandomId());
cy.get(emailField)
.focus()
.type(email);
cy.get('minds-form-register #password')
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password + '!');
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton).click();
cy.scrollTo('top');
cy.contains(passwordDontMatch);
cy.get('.m-register__error').contains('Passwords must match');
});
})
import 'cypress-file-upload';
/**
* @author Marcelo, Ben and Brian
* @author Marcelo, Ben and Brian
* @create date 2019-08-09 22:54:02
* @modify date 2019-08-09 22:54:02
* @desc Custom commands for access through cy.[cmd]();
*
*
* For more comprehensive examples of custom
* commands please read more here:
* https://on.cypress.io/custom-commands
*
* -- This is a parent command --
* Cypress.Commands.add('login', (email, password) => { ... })
* -- This is a child command --
* Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
* -- This is a dual command --
* Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
* -- This is will overwrite an existing command --
* Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
*/
......@@ -56,7 +56,7 @@ const defaults = {
const loginForm = {
password: 'minds-form-login .m-login-box .mdl-cell:last-child input',
username: 'minds-form-login .m-login-box .mdl-cell:first-child input',
submit: 'minds-form-login .m-btn--login',
submit: 'minds-form-login .mf-button',
}
const poster = {
......@@ -84,7 +84,7 @@ Cypress.Commands.add('login', (canary = false, username, password) => {
cy.get(loginForm.username).focus().type(username);
cy.get(loginForm.password).focus().type(password);
cy.get(loginForm.submit)
.focus()
.click({force: true})
......@@ -104,21 +104,21 @@ Cypress.Commands.add('logout', () => {
/**
* Register a user, be sure to delete the user following this.
*
*
* ! LOG-OUT PRIOR TO CALLING !
*
*
* @param { string } username - The username. Note that the requested username will NOT be freed up upon deletion
* @param { string } password - The users password.
* @returns void
*/
Cypress.Commands.add('newUser', (username = '', password = '') => {
cy.visit('/login')
cy.visit('/register')
.location('pathname')
.should('eq', `/login`);
.should('eq', `/register`);
cy.server();
cy.route("POST", '**/api/v1/register').as('registerPOST');
cy.get(registerForm.username).focus().type(username);
cy.get(registerForm.email).focus().type(defaults.email);
cy.get(registerForm.password).focus().type(password);
......@@ -137,7 +137,7 @@ Cypress.Commands.add('newUser', (username = '', password = '') => {
//onboarding modal shown.
cy.get(onboarding.welcomeTextContainer)
.contains(onboarding.welcomeText);
//skip onboarding.
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
......@@ -151,14 +151,14 @@ Cypress.Commands.add('preserveCookies', () => {
/**
* Deletes a user. Use carefully on sandbox or you may lose your favorite test account.
*
*
* ! LOG-IN PRIOR TO CALLING !
*
*
* @param { string } username - The username. TODO: when both params provided log the user in too
* @param { string } password - The password.
* @returns void
*/
Cypress.Commands.add('deleteUser', (username, password) => {
Cypress.Commands.add('deleteUser', (username, password) => {
cy.server();
cy.route("POST", '**/api/v2/settings/password/validate').as('validatePost');
cy.route("POST", '**/api/v2/settings/delete').as('deletePOST');
......@@ -170,7 +170,7 @@ Cypress.Commands.add('deleteUser', (username, password) => {
cy.get(settings.deleteAccountButton).click({ force: true });
cy.get('#password').focus().type(password);
cy.get(settings.deleteSubmitButton).click({ force: true })
cy.get(settings.deleteSubmitButton).click({ force: true })
.wait('@validatePost').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
......@@ -193,9 +193,9 @@ Cypress.Commands.add('uploadFile', (selector, fileName, type = '') => {
cy.fixture(fileName).then((content) => {
cy.log("Content", fileName);
cy.get(selector).upload({
fileContent: content,
fileName: fileName,
mimeType: type
fileContent: content,
fileName: fileName,
mimeType: type
});
});
});
......@@ -218,7 +218,7 @@ Cypress.Commands.add('post', (message) => {
/**
* Sets the feature flag cookie.
* @param { Object } flags - JSON object containing flags to turn on
* @param { Object } flags - JSON object containing flags to turn on
* e.g. { dark mode:false, es-feeds: true }
* @returns void
*/
......
This diff is collapsed.
......@@ -33,6 +33,7 @@
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.is-pro-domain]="isProDomain"
>
<m-emailConfirmation></m-emailConfirmation>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<span
class="m-blockchain--wallet-address-notice--action"
......
......@@ -113,7 +113,10 @@ export class Minds {
this.minds.user.language,
this.minds.language
);
window.location.reload(true);
setTimeout(() => {
window.location.reload(true);
});
}
}
});
......
......@@ -52,7 +52,6 @@ import { BanModule } from './modules/ban/ban.module';
import { BlogModule } from './modules/blogs/blog.module';
import { SearchModule } from './modules/search/search.module';
import { MessengerModule } from './modules/messenger/messenger.module';
import { HomepageModule } from './modules/homepage/homepage.module';
import { NewsfeedModule } from './modules/newsfeed/newsfeed.module';
import { MediaModule } from './modules/media/media.module';
import { AuthModule } from './modules/auth/auth.module';
......@@ -73,6 +72,8 @@ import { ChannelContainerModule } from './modules/channel-container/channel-cont
import { UpgradesModule } from './modules/upgrades/upgrades.module';
import * as Sentry from '@sentry/browser';
import { HomepageModule } from './modules/homepage/homepage.module';
import { OnboardingV2Module } from './modules/onboarding-v2/onboarding.module';
Sentry.init({
dsn: 'https://3f786f8407e042db9053434a3ab527a2@sentry.io/1538008', // TODO: do not hardcard
......@@ -127,6 +128,7 @@ export class SentryErrorHandler implements ErrorHandler {
PaymentsModule,
MindsFormsModule,
OnboardingModule,
OnboardingV2Module,
NotificationModule,
GroupsModule,
BlogModule,
......
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { RouterModule, Router, Routes } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MINDS_PIPES } from './pipes/pipes';
......@@ -127,9 +127,20 @@ import { FormDescriptorComponent } from './components/form-descriptor/form-descr
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { SsoService } from './services/sso.service';
import { MindsProAvatarComponent } from '../modules/pro/pro-avatar/pro-avatar.component';
import { V2TopbarService } from './layout/v2-topbar/v2-topbar.service';
import { DateDropdownsComponent } from './components/date-dropdowns/date-dropdowns.component';
import { SidebarMarkersService } from './layout/sidebar/markers.service';
import { EmailConfirmationComponent } from './components/email-confirmation/email-confirmation.component';
PlotlyModule.plotlyjs = PlotlyJS;
const routes: Routes = [
{
path: 'email-confirmation',
redirectTo: '/',
},
];
@NgModule({
imports: [
NgCommonModule,
......@@ -138,6 +149,7 @@ PlotlyModule.plotlyjs = PlotlyJS;
FormsModule,
ReactiveFormsModule,
PlotlyModule,
RouterModule.forChild(routes),
],
declarations: [
MINDS_PIPES,
......@@ -243,6 +255,8 @@ PlotlyModule.plotlyjs = PlotlyJS;
FormToastComponent,
ShadowboxSubmitButtonComponent,
MindsProAvatarComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
],
exports: [
MINDS_PIPES,
......@@ -343,6 +357,8 @@ PlotlyModule.plotlyjs = PlotlyJS;
FormToastComponent,
ShadowboxSubmitButtonComponent,
MindsProAvatarComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
],
providers: [
SiteService,
......@@ -409,6 +425,14 @@ PlotlyModule.plotlyjs = PlotlyJS;
useFactory: router => new RouterHistoryService(router),
deps: [Router],
},
{
provide: V2TopbarService,
useFactory: V2TopbarService._,
},
{
provide: SidebarMarkersService,
useFactory: SidebarMarkersService._,
},
],
entryComponents: [
NotificationsToasterComponent,
......
......@@ -67,4 +67,9 @@ m-announcement {
}
}
}
.m-announcement__clickable {
cursor: pointer;
font-weight: bold;
}
}
......@@ -14,7 +14,7 @@ import { Client } from '../../../services/api';
<ng-content></ng-content>
</div>
<div class="m-announcement--close" (click)="close()">
<div class="m-announcement--close" *ngIf="canClose" (click)="close()">
<i class="material-icons">close</i>
</div>
</div>
......@@ -24,6 +24,8 @@ export class AnnouncementComponent {
minds: Minds = window.Minds;
hidden: boolean = false;
@Input() id: string = 'default';
@Input() canClose: boolean = true;
@Input() remember: boolean = true;
constructor(private storage: Storage) {}
......@@ -32,7 +34,10 @@ export class AnnouncementComponent {
}
close() {
this.storage.set('hide-announcement:' + this.id, true);
if (this.remember) {
this.storage.set('hide-announcement:' + this.id, true);
}
this.hidden = true;
}
}
......@@ -130,6 +130,10 @@ export class MindsAvatar {
* @returns true if the object guid matches the currently logged in user guid
*/
isOwnerAvatar(): boolean {
return this.minds.user && this.object.guid === this.minds.user.guid;
return (
this.minds.user &&
this.object &&
this.object.guid === this.minds.user.guid
);
}
}
......@@ -32,25 +32,31 @@
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div
[ngSwitch]="rawData?.unit"
*ngFor="let value of hoverInfo.values"
class="m-chartV2__hoverInfo__row--primary"
>
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number: '1.0-0' | abbr }}
{{ rawData.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchCase="eth">
{{ hoverInfo.value | number: '1.3-3' }} ETH
</ng-template>
<ng-template ngSwitchCase="tokens">
{{ hoverInfo.value | number: '1.1-3' }} Tokens
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.0-3' }} {{ rawData?.unit }}
</ng-template>
<span
class="m-chartV2__hoverInfoRow__hex"
[style.background-color]="value.color"
></span>
<ng-container [ngSwitch]="rawData?.unit">
<ng-template ngSwitchCase="number">
{{ value.value | number: '1.0-0' | abbr }}
{{ value.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ value.value | currency }} USD
</ng-template>
<ng-template ngSwitchCase="eth">
{{ value.value | number: '1.3-3' }} ETH
</ng-template>
<ng-template ngSwitchCase="tokens">
{{ value.value | number: '1.1-3' }} Tokens
</ng-template>
<ng-template ngSwitchDefault>
{{ value.value | number: '1.0-3' }} {{ rawData?.unit }}
</ng-template>
</ng-container>
</div>
<div class="m-chartV2__hoverInfo__row" *ngIf="isComparison">
vs
......
......@@ -58,6 +58,14 @@ m-chartV2 {
}
}
.m-chartV2__hoverInfoRow__hex {
width: 6px;
height: 6px;
display: inline-block;
margin-right: 2px;
border-radius: 50%;
}
.m-chartV2__hoverInfo__closeBtn {
display: none;
font-size: 15px;
......
......@@ -76,11 +76,12 @@ export class ChartV2Component implements OnInit, OnDestroy {
? this.rawData.visualisation.segments.slice(0, 1)
: this.rawData.visualisation.segments;
if (this.segments.length === 2) {
this.isComparison = true;
// this.isComparison = true;
// Reverse the segments so comparison line is layered behind current line
this.segments.reverse();
// this.segments.reverse();
// Current line should be blue, not grey
this.swapSegmentColors();
// this.swapSegmentColors();
this.detectChanges();
}
this.themeSubscription = this.themeService.isDark$.subscribe(isDark => {
this.isDark = isDark;
......@@ -131,7 +132,9 @@ export class ChartV2Component implements OnInit, OnDestroy {
this.segments.forEach((segment, index) => {
const segmentMarkerFills = [];
for (let i = 0; i < this.pointsPerSegment; i++) {
segmentMarkerFills[i] = this.getColor('m-white');
segmentMarkerFills[i] = this.getColor(
chartPalette.segmentColorIds[index]
);
}
this.markerFills.push(segmentMarkerFills);
});
......@@ -169,12 +172,12 @@ export class ChartV2Component implements OnInit, OnDestroy {
y: this.unpack(this.segments[i].buckets, 'value'),
};
if (this.segments[i].comparison) {
segment.line.dash = 'dot';
}
this.data[i] = segment;
});
if (this.isComparison) {
this.data[0].line.dash = 'dot';
}
}
setLayout() {
......@@ -252,7 +255,7 @@ export class ChartV2Component implements OnInit, OnDestroy {
onHover($event) {
this.hoverPoint = $event.points[0].pointIndex;
this.addMarkerFill();
this.emptyMarkerFill();
if (!this.isMini) {
this.showShape($event);
}
......@@ -265,7 +268,7 @@ export class ChartV2Component implements OnInit, OnDestroy {
}
onUnhover($event) {
this.emptyMarkerFill();
this.addMarkerFill();
this.hideShape();
this.hoverInfoDiv.style.opacity = 0;
this.detectChanges();
......@@ -306,24 +309,37 @@ export class ChartV2Component implements OnInit, OnDestroy {
}
populateHoverInfo() {
const pt = this.isComparison ? 1 : 0;
// TODO: format value strings here and remove ngSwitch from template?
this.hoverInfo['date'] = this.segments[pt].buckets[this.hoverPoint].date;
this.hoverInfo['date'] = this.segments[0].buckets[this.hoverPoint].date;
this.hoverInfo['value'] =
this.rawData.unit !== 'usd'
? this.segments[pt].buckets[this.hoverPoint].value
: this.segments[pt].buckets[this.hoverPoint].value / 100;
if (this.isComparison && this.segments[1]) {
this.hoverInfo['comparisonValue'] =
this.rawData.unit !== 'usd'
? this.segments[0].buckets[this.hoverPoint].value
: this.segments[0].buckets[this.hoverPoint].value / 100;
this.hoverInfo['comparisonDate'] = this.segments[0].buckets[
this.hoverPoint
].date;
? this.segments[0].buckets[this.hoverPoint].value
: this.segments[0].buckets[this.hoverPoint].value / 100;
this.hoverInfo['values'] = [];
for (const pt in this.segments) {
const segment = this.segments[pt];
this.hoverInfo['values'][pt] = {
value:
this.rawData.unit !== 'usd'
? segment.buckets[this.hoverPoint].value
: segment.buckets[this.hoverPoint].value / 100,
label: segment.label || this.rawData.label,
color: this.getColor(chartPalette.segmentColorIds[pt]),
};
}
// if (this.isComparison && this.segments[1]) {
// this.hoverInfo['comparisonValue'] =
// this.rawData.unit !== 'usd'
// ? this.segments[0].buckets[this.hoverPoint].value
// : this.segments[0].buckets[this.hoverPoint].value / 100;
//
// this.hoverInfo['comparisonDate'] = this.segments[0].buckets[
// this.hoverPoint
// ].date;
// }
}
positionHoverInfo($event) {
......
m-date__dropdowns {
display: flex;
justify-content: space-between;
select {
display: inline-block;
background-color: #fff;
box-sizing: border-box;
margin: 0 10px 0 0;
padding: 8px 10px;
height: 36px;
min-width: 80px;
max-width: 90px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 16px;
line-height: 21px;
border-radius: 2px;
@include m-theme() {
color: themed($m-grey-800);
background-color: themed($m-white);
border: 1px solid #e2e2e2;
}
// month
&:nth-child(1) {
min-width: 120px;
}
// day
&:nth-child(2) {
min-width: 59px;
}
// year
&:nth-child(3) {
min-width: 77px;
}
}
}
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
@Component({
selector: 'm-date__dropdowns',
template: `
<select
data-minds="monthDropdown"
[ngModel]="selectedMonth"
(ngModelChange)="selectMonth($event)"
>
<option *ngFor="let month of monthNames">{{ month }}</option>
</select>
<select
data-minds="dayDropdown"
[ngModel]="selectedDay"
(ngModelChange)="selectDay($event)"
>
<option *ngFor="let day of days">{{ day }}</option>
</select>
<select
data-minds="yearDropdown"
[ngModel]="selectedYear"
(ngModelChange)="selectYear($event)"
>
<option *ngFor="let year of years">{{ year }}</option>
</select>
`,
})
export class DateDropdownsComponent implements OnInit {
@Output() selectedDateChange: EventEmitter<string> = new EventEmitter<
string
>();
monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
days = [1];
years = [];
selectedMonth = 'January';
selectedDay = '1';
selectedYear = new Date().getFullYear();
constructor() {}
ngOnInit() {
this.years = this.range(100, this.selectedYear, false);
this.selectedYear = this.years[0];
this.selectMonth('January');
}
selectMonth(month: string) {
this.selectedMonth = month;
this.populateDays(
this.getDaysInMonth(this.getMonthNumber(month), this.selectedYear)
);
this.selectedDateChange.emit(this.buildDate());
}
selectDay(day: string) {
this.selectedDay = day;
this.selectedDateChange.emit(this.buildDate());
}
selectYear(year) {
this.selectedYear = year;
this.populateDays(
this.getDaysInMonth(this.getMonthNumber(this.selectedMonth), year)
);
this.selectedDateChange.emit(this.buildDate());
}
buildDate() {
let date: string = '';
if (this.selectedMonth !== '') {
if (this.selectedYear) {
date = `${this.pad(this.selectedYear, 4)}-`;
}
const monthIndex = this.monthNames.findIndex(
item => item === this.selectedMonth
);
date += `${this.pad(monthIndex + 1, 2)}`;
if (this.selectedDay) {
date += `-${this.pad(this.selectedDay, 2)}`;
}
}
return date;
}
private populateDays(maxDays: number) {
this.days = this.range(maxDays, 1);
}
private getMonthNumber(month: string): number {
return this.monthNames.indexOf(month);
}
private getDaysInMonth(month, year): number {
// let date = new Date(Date.UTC(year, month, 1));
const date = new Date(year, month, 1);
let day = 0;
while (date.getMonth() === month) {
day = date.getDate();
date.setDate(date.getDate() + 1);
}
return day;
}
private range(size, startAt = 0, grow = true): Array<number> {
return Array.from(Array(size).keys()).map(i => {
if (grow) {
return i + startAt;
} else {
return startAt - i;
}
});
}
private pad(val: any, pad: number = 0) {
if (!pad) {
return val;
}
return (Array(pad + 1).join('0') + val).slice(-pad);
}
}
<ng-container *ngIf="shouldShow">
<m-announcement
id="email-confirmation"
[canClose]="canClose"
[remember]="false"
>
Please confirm your email address.
<ng-container *ngIf="!sent"
>Didn't get it?
<span class="m-announcement__clickable" (click)="send()"
>Click here to send again.</span
></ng-container
>
</m-announcement>
</ng-container>
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
} from '@angular/core';
import { EmailConfirmationService } from './email-confirmation.service';
import { Session } from '../../../services/session';
import { Subscription } from 'rxjs';
/**
* Component that displays an announcement-like banner
* asking the user to confirm their email address and a link
* to re-send the confirmation email.
* @see AnnouncementComponent
*/
@Component({
providers: [EmailConfirmationService],
selector: 'm-emailConfirmation',
templateUrl: 'email-confirmation.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmailConfirmationComponent implements OnInit, OnDestroy {
sent: boolean = false;
shouldShow: boolean = false;
canClose: boolean = false;
protected userEmitter$: Subscription;
protected canCloseTimer: number;
protected minds = window.Minds;
constructor(
protected service: EmailConfirmationService,
protected session: Session,
protected cd: ChangeDetectorRef
) {}
ngOnInit(): void {
this.setShouldShow(this.session.getLoggedInUser());
this.userEmitter$ = this.session.userEmitter.subscribe(user => {
this.sent = false;
this.setShouldShow(user);
this.detectChanges();
});
this.canCloseTimer = window.setTimeout(() => {
this.canClose = true;
this.detectChanges();
}, 3000);
}
ngOnDestroy(): void {
window.clearTimeout(this.canCloseTimer);
if (this.userEmitter$) {
this.userEmitter$.unsubscribe();
}
}
/**
* Re-calculates the visibility of the banner
* @param {Object} user
*/
setShouldShow(user): void {
this.shouldShow =
!this.minds.from_email_confirmation &&
user &&
user.email_confirmed === false;
}
/**
* Uses the service to re-send the confirmation email
*/
async send(): Promise<void> {
this.sent = true;
this.detectChanges();
try {
const sent = await this.service.send();
if (!sent) {
this.sent = false;
}
} catch (e) {}
this.detectChanges();
}
detectChanges(): void {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
import { Injectable } from '@angular/core';
import { Client } from '../../../services/api/client';
/**
* API implementation service for Email Confirmation component
* @see EmailConfirmationComponent
*/
@Injectable()
export class EmailConfirmationService {
constructor(protected client: Client) {}
/**
* Attempts to re-send the confirmation email to the current logged in user
*/
async send(): Promise<boolean> {
const response = (await this.client.post(
'api/v2/email/confirmation/resend',
{}
)) as any;
return Boolean(response && response.sent);
}
}
......@@ -8,8 +8,9 @@
<h4 class="m-marketingFooter__sloganText" i18n>
Take back control of your social media
</h4>
<div class="m-marketingFooter__text">&copy; {{ year }} Minds, Inc.</div>
<div class="m-marketingFooter__text" *ngIf="!isMobile">
&copy; {{ year }} Minds, Inc.
</div>
</div>
<div
......@@ -201,6 +202,9 @@
<div
class="m-grid__column-12 m-grid__column-12--mobile m-marketingFooter__column"
>
<div class="m-marketingFooter__text" *ngIf="isMobile">
&copy; {{ year }} Minds, Inc.
</div>
<ul class="m-marketingFooter__inlineList m-marketingFooter__legalLinks">
<li>
<a routerLink="/p/terms" i18n>
......
......@@ -40,6 +40,14 @@ m-marketing__footer {
}
.m-marketingFooter__column {
@media screen and(max-width: $m-grid-max-mobile) {
@for $i from 1 through 5 {
&:nth-child(#{$i}) {
grid-row: $i;
}
}
}
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 32px;
......@@ -53,9 +61,7 @@ m-marketing__footer {
width: 60%;
margin: 0 auto;
@media screen and (max-width: $m-grid-min-vp) {
width: 60%;
grid-row: 999;
@media screen and (max-width: $m-grid-max-mobile) {
margin: 32px 0 0;
}
}
......@@ -75,7 +81,12 @@ m-marketing__footer {
}
&.m-marketingFooter__sloganText {
font-weight: 400;
margin: 0 0 21px;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
......@@ -115,15 +126,6 @@ m-marketing__footer {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
display: inline-block;
margin-right: 1em;
&:last-child {
margin-right: 0;
}
}
a {
color: inherit;
font-weight: normal;
......@@ -149,7 +151,7 @@ m-marketing__footer {
text-align: right;
padding-right: 92px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
text-align: inherit;
padding-right: initial;
}
......
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostListener,
OnInit,
} from '@angular/core';
@Component({
selector: 'm-marketing__footer',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'footer.component.html',
})
export class MarketingFooterComponent {
export class MarketingFooterComponent implements OnInit {
readonly year: number = new Date().getFullYear();
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
isMobile: boolean;
constructor(protected cd: ChangeDetectorRef) {}
ngOnInit() {
this.onResize();
}
@HostListener('window:resize')
onResize() {
this.isMobile = window.innerWidth <= 480;
this.detectChanges();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -3,6 +3,10 @@
m-marketing {
display: block;
font-family: Roboto, sans-serif;
overflow-x: hidden;
min-width: 320px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@include m-theme() {
background: themed($m-white);
......
......@@ -2,23 +2,40 @@ import {
ChangeDetectionStrategy,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { MindsTitle } from '../../../services/ux/title';
import { V2TopbarService } from '../../layout/v2-topbar/v2-topbar.service';
@Component({
selector: 'm-marketing',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'marketing.component.html',
})
export class MarketingComponent implements OnInit {
export class MarketingComponent implements OnInit, OnDestroy {
@Input() pageTitle: string = '';
@Input() showBottombar: boolean = true;
@Input() forceBackground: boolean = true;
constructor(protected title: MindsTitle) {}
constructor(
protected title: MindsTitle,
private topbarService: V2TopbarService
) {}
ngOnInit() {
if (this.pageTitle) {
this.title.setTitle(this.pageTitle);
}
this.topbarService.toggleMarketingPages(
true,
this.showBottombar,
this.forceBackground
);
}
ngOnDestroy() {
this.topbarService.toggleMarketingPages(false);
}
}
......@@ -119,6 +119,13 @@
}
}
a.m-marketing__link {
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
.m-marketing__links {
@media screen and (max-width: $m-grid-min-vp) {
text-align: center;
......@@ -169,4 +176,110 @@
}
}
}
span.m-marketing__imageUX {
span.m-marketing__imageTick {
border-radius: 50%;
background-color: #4fc3a9;
color: white;
width: 63px;
height: 63px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 39px -4px rgba(0, 0, 0, 0.5);
z-index: 3;
@media screen and (max-width: $max-mobile) {
width: 10vw;
height: 10vw;
font-size: 5vw;
}
}
.m-marketing__imageTick--left {
position: absolute;
bottom: 62px;
left: -21px;
@media screen and(min-width: $max-mobile) and (max-width: $m-grid-min-vp) {
bottom: -23%;
left: 8.5%;
}
@media screen and (max-width: $max-mobile) {
//bottom: -116px;
//left: 2px;
bottom: -27%;
left: 2.5%;
}
}
.m-marketing__imageTick--right {
position: absolute;
bottom: 8px;
left: -45px;
@media screen and(min-width: $max-mobile) and (max-width: $m-grid-min-vp) {
bottom: 0;
right: 0;
left: auto;
}
@media screen and (max-width: $max-mobile) {
//bottom: -116px;
//left: 2px;
bottom: -27%;
left: 2.5%;
}
}
img {
box-shadow: 0 0 39px -4px rgba(0, 0, 0, 0.5);
}
}
.m-marketing__quotation {
display: flex;
flex-direction: column;
align-items: flex-end;
margin: 0 auto;
@media screen and(min-width: $m-grid-min-vp) {
width: 730px;
}
h3 {
font-size: 28px;
font-weight: bold;
line-height: 37px;
margin-bottom: 0;
}
h4 {
font-size: 14px;
line-height: 19px;
margin: 0;
@include m-theme() {
color: themed($m-grey-300);
}
}
}
.mf-button.mf-button--alt.mf-button--gradient {
background: #5dbac0; /* Old Browsers */
background: linear-gradient(
45deg,
#4eb69f 0%,
#4fc3aa 49%,
#4eb69f 49%,
#4fc3aa 100%
);
}
}
......@@ -41,7 +41,7 @@
);
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
right: 0;
bottom: -3vw;
transform: none;
......@@ -54,7 +54,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin: 15px 0 15px;
text-align: center;
}
......@@ -65,7 +65,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
......@@ -76,7 +76,18 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@media screen and(min-width: $m-grid-max-tablet) {
padding-right: 200px;
}
@media screen and(min-width: 900px) and (max-width: $m-grid-min-vp) {
padding-right: 100px;
}
@media screen and(max-width: 900px) {
padding-right: 0;
}
@include m-theme() {
color: themed($m-grey-300);
......@@ -86,7 +97,12 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
font-size: 17px;
line-height: 24px;
}
@media screen and (max-width: $m-grid-max-mobile) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
......@@ -105,7 +121,7 @@
height: 547px;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 2%, 0% 97%, 100% 100%, 100% 0%);
......@@ -131,7 +147,7 @@
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......@@ -150,7 +166,7 @@
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......
......@@ -5,7 +5,7 @@
&.m-marketing__section--style-3 {
margin-bottom: 100px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: 80px;
}
......@@ -14,7 +14,7 @@
z-index: 0;
padding: 80px 0 80px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0;
}
}
......@@ -25,7 +25,7 @@
padding: 0;
min-height: 330px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0 30px 0;
min-height: 0;
}
......@@ -50,7 +50,7 @@
);
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......@@ -61,7 +61,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
......@@ -72,7 +72,10 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@media screen and(min-width: $min-tablet) {
padding-right: 200px;
}
@include m-theme() {
color: themed($m-grey-300);
......@@ -82,7 +85,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
......@@ -126,7 +129,12 @@
height: 518px;
clip-path: polygon(0% 1%, 0% 100%, 100% 96%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
width: 100%;
//height: auto;
}
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 100%, 100% 97%, 100% 0%);
......@@ -142,7 +150,7 @@
bottom: 35px;
transform: translate(15px, 0);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
right: auto;
left: 50%;
bottom: 0;
......@@ -157,7 +165,7 @@
position: relative;
width: 100%;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
......@@ -182,7 +190,7 @@
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......
......@@ -45,8 +45,34 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@media screen and(min-width: $min-tablet) {
padding-right: 200px;
}
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 30px;
text-align: center;
}
}
ol.m-marketing__description {
padding-left: 16px;
& > li {
margin-bottom: 16px;
font-size: 18px;
line-height: 27px;
}
//@media screen and(min-width: $m-grid-min-vp),
// screen and (max-width: $m-grid-max-mobile) {
// padding-right: 100px;
//}
}
ol.m-marketing__description > li,
p.m-marketing__description {
@include m-theme() {
color: themed($m-grey-300);
}
......@@ -59,8 +85,7 @@
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
line-height: 27px;
}
}
......@@ -91,17 +116,22 @@
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
grid-row: 1;
}
//
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 547px;
clip-path: polygon(0% 1%, 0% 96%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
width: 100%;
//height: auto;
}
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
......@@ -116,8 +146,14 @@
left: 0;
bottom: 35px;
transform: translate(-15px, 0);
z-index: 2;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
width: 100%;
height: auto;
}
@media screen and (max-width: $m-grid-max-mobile) {
right: auto;
left: 50%;
bottom: 0;
......@@ -127,13 +163,13 @@
}
}
span {
span:not(.m-marketing__imageUX):not(.m-marketing__imageTick) {
display: inline-block;
position: relative;
width: 100%;
text-align: right;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
......
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
&.m-marketing__section--style-5 {
margin-bottom: 100px;
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: 80px;
}
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 80px 0 80px;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
padding: 95px 0 0;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0 30px 0;
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-max-mobile) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
@media screen and(min-width: $min-tablet) {
padding-right: 200px;
}
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-max-mobile) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
ul.m-marketing__points {
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
> li em {
font-style: normal;
@include m-theme() {
color: themed($m-black);
}
@include m-on-theme(dark) {
color: #ffffff;
font-weight: bold;
}
}
}
.m-marketing__image {
position: relative;
z-index: 0;
grid-column-start: 1;
grid-row: 1;
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 518px;
clip-path: polygon(0% 1%, 0% 100%, 100% 96%, 100% 0%);
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
width: 100%;
}
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 100%, 100% 97%, 100% 0%);
}
}
img.m-marketing__image--2 {
object-fit: contain;
width: 358px;
height: 191px;
position: absolute;
right: 0;
bottom: 35px;
transform: translate(15px, 0);
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
width: 100%;
}
@media screen and (max-width: $m-grid-max-mobile) {
right: auto;
left: 50%;
bottom: 0;
transform: translate(-50%, 50%);
width: 85vw;
height: 45.35vw;
}
}
span {
display: inline-block;
position: relative;
width: 100%;
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
&.m-marketing__image--noUxSample {
margin-bottom: 40px;
}
}
// Deco
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 191px;
height: 191px;
transform: translate(-60px, -58px);
background: url('<%= APP_CDN %>/assets/marketing/deco_3.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
}
}
}
}
}
......@@ -23,6 +23,8 @@
<div class="m-phone-input--flag" [ngClass]="country.flagClass"></div>
</div>
<span class="m-phone-input--country-name">{{ country.name }}</span>
<span class="m-phone-input--dial-code">+{{ country.dialCode }}</span>
<span class="m-phone-input--dial-code" [attr.data-minds]="country.dialCode">
+{{ country.dialCode }}
</span>
</li>
</ul>
......@@ -5,6 +5,7 @@ import {
ViewChild,
Output,
EventEmitter,
OnInit,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
......@@ -16,7 +17,7 @@ import { CountryCode } from './countries';
selector: 'm-phone-input--country',
templateUrl: 'country.component.html',
})
export class PhoneInputCountryComponent {
export class PhoneInputCountryComponent implements OnInit {
@Output('country') selectedCountryEvt = new EventEmitter();
countries: Array<Country> = [];
selectedCountry: Country = new Country();
......
......@@ -14,7 +14,7 @@ m-shadowboxHeader.isScrollable {
position: relative;
transition: all 0.3s ease;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
border-top: 1px solid rgba(themed($m-grey-200), 0.4);
background-color: themed($m-white);
}
}
......
......@@ -37,7 +37,11 @@
<h3>{{ menu.header.label }}</h3>
</div>
<nav class="m-sidebarMenu__linksContainer" *ngIf="menu.links">
<nav
class="m-sidebarMenu__linksContainer"
*ngIf="menu.links"
data-minds="sidebarMenuLinks"
>
<div class="m-sidebarMenu__link" *ngFor="let link of menu.links">
<ng-container *ngIf="link.permissionGranted">
<ng-container *ngIf="!link.newWindow">
......
......@@ -56,10 +56,10 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
id: '30d',
label: '30d',
},
/*{
{
id: '1y',
label: '1y'
},*/
label: '1y',
},
];
customTypes: Array<{ id; label; icon? }> = [
......
......@@ -15,11 +15,18 @@ import { SignupModalService } from '../../../modules/modals/signup/service';
inputs: ['_object: object'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<a (click)="thumb()" [ngClass]="{ selected: has() }">
<a
(click)="thumb()"
[ngClass]="{ selected: has() }"
data-cy="data-minds-thumbs-down-button"
>
<i class="material-icons">thumb_down</i>
<span class="minds-counter" *ngIf="object['thumbs:down:count'] > 0">{{
object['thumbs:down:count'] | number
}}</span>
<span
class="minds-counter"
*ngIf="object['thumbs:down:count'] > 0"
data-cy="data-minds-thumbs-down-counter"
>{{ object['thumbs:down:count'] | number }}</span
>
</a>
`,
styles: [
......
......@@ -16,11 +16,18 @@ import { SignupModalService } from '../../../modules/modals/signup/service';
inputs: ['_object: object'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<a (click)="thumb()" [ngClass]="{ selected: has() }">
<a
(click)="thumb()"
[ngClass]="{ selected: has() }"
data-cy="data-minds-thumbs-up-button"
>
<i class="material-icons">thumb_up</i>
<span class="minds-counter" *ngIf="object['thumbs:up:count'] > 0">{{
object['thumbs:up:count'] | number
}}</span>
<span
class="minds-counter"
*ngIf="object['thumbs:up:count'] > 0"
data-cy="data-minds-thumbs-up-counter"
>{{ object['thumbs:up:count'] | number }}</span
>
</a>
`,
styles: [
......
......@@ -10,7 +10,7 @@ m-body {
}
&.has-v2-navbar {
margin-top: 52px;
margin-top: 51px;
}
&.is-pro-domain {
......
......@@ -3,6 +3,7 @@ import {
ComponentFactoryResolver,
ViewChild,
HostListener,
AfterViewInit,
} from '@angular/core';
import { Storage } from '../../../services/storage';
......@@ -10,12 +11,13 @@ import { Sidebar } from '../../../services/ui/sidebar';
import { Session } from '../../../services/session';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { GroupsSidebarMarkersComponent } from '../../../modules/groups/sidebar-markers/sidebar-markers.component';
import { SidebarMarkersService } from './markers.service';
@Component({
selector: 'm-sidebar--markers',
templateUrl: 'markers.component.html',
})
export class SidebarMarkersComponent {
export class SidebarMarkersComponent implements AfterViewInit {
@ViewChild(DynamicHostDirective, { static: true }) host: DynamicHostDirective;
minds = window.Minds;
......@@ -24,12 +26,17 @@ export class SidebarMarkersComponent {
componentRef;
componentInstance: GroupsSidebarMarkersComponent;
visible: boolean = true;
constructor(
public session: Session,
public storage: Storage,
public sidebar: Sidebar,
private sidebarMarkersService: SidebarMarkersService,
private _componentFactoryResolver: ComponentFactoryResolver
) {}
) {
this.sidebarMarkersService.setContainer(this);
}
ngAfterViewInit() {
const isLoggedIn = this.session.isLoggedIn((is: boolean) => {
......
import { SidebarMarkersComponent } from './markers.component';
export class SidebarMarkersService {
private container: SidebarMarkersComponent;
static _() {
return new SidebarMarkersService();
}
setContainer(container: SidebarMarkersComponent) {
this.container = container;
return this;
}
toggleVisibility(visible: boolean) {
this.container.checkSidebarVisibility(visible);
}
}
......@@ -43,7 +43,6 @@
<li
class="m-dropdownList__item m-user-menuDropdown__Item"
(click)="closeMenu()"
*ngIf="getCurrentUser()?.pro"
>
<a routerLink="/analytics/dashboard/traffic">
<i class="material-icons">timeline</i>
......
......@@ -5,6 +5,7 @@
routerLinkActive="m-v2-topbarNav__Item--active"
title="Newsfeed"
i18n-title
data-cy="data-minds-nav-newsfeed-button"
>
<i class="material-icons">home</i>
<span class="m-v2-topbarNavItem__Text" i18n>Newsfeed</span>
......@@ -16,6 +17,7 @@
routerLinkActive="m-v2-topbarNav__Item--active"
title="Discovery"
i18n-title
data-cy="data-minds-nav-discovery-button"
>
<i class="material-icons">search</i>
<span class="m-v2-topbarNavItem__Text" i18n>Discovery</span>
......@@ -27,13 +29,19 @@
routerLinkActive="m-v2-topbarNav__Item--active"
title="Wallet"
i18n-title
data-cy="data-minds-nav-wallet-button"
>
<i class="material-icons">account_balance</i>
<span class="m-v2-topbarNavItem__Text" i18n>Wallet</span>
</a>
</ng-template>
<div class="m-v2-topbar__Top">
<div
class="m-v2-topbar__Top"
[class.m-v2-topbar__marketingPages]="marketingPages"
[class.m-v2-topbar__noBackground]="!showBackground"
[style.visibility]="showTopbar ? 'visible' : 'hidden'"
>
<div class="m-v2-topbar">
<div class="m-v2-topbar__Container--left">
<nav class="m-v2-topbar__Nav">
......@@ -75,11 +83,41 @@
</div>
</ng-container>
<ng-template #loggedOutRightContainer>
<div class="m-v2-topbar__Container__LoginWrapper">
<a routerLink="/login" title="Login" i18n-title>
Login / Signup
</a>
</div>
<ng-container
*mIfFeature="'register_pages-december-2019'; else singleButton"
>
<ng-container *ngIf="!onAuthPages">
<div class="m-v2-topbar__Container__LoginWrapper">
<a
class="m-v2-topbarLoginWrapper__login"
routerLink="/login"
title="Login"
i18n-title
>
Login
</a>
<a
class="m-v2-topbarLoginWrapper__joinMindsNow"
routerLink="/register"
title="Join Minds Now"
i18n-title
>
Join Minds Now
</a>
</div>
</ng-container>
</ng-container>
<ng-template #singleButton>
<ng-container *ngIf="!onAuthPages">
<div class="m-v2-topbar__Container__LoginWrapper">
<a routerLink="/login" title="Login" i18n-title>
Login / Signup
</a>
</div>
</ng-container>
</ng-template>
</ng-template>
<div class="m-v2-topbar__UserMenu" *ngIf="getCurrentUser()">
......@@ -90,7 +128,7 @@
</div>
</div>
<div class="m-v2-topbar__Bottom">
<div class="m-v2-topbar__Bottom" *ngIf="showBottombar">
<ng-container *ngTemplateOutlet="navLinks"></ng-container>
</div>
......
......@@ -27,6 +27,44 @@
width: 100%;
}
&.m-v2-topbar__noBackground {
@include m-theme() {
background-color: transparent;
}
}
&.m-v2-topbar__marketingPages {
flex-direction: row;
@include m-theme() {
border: none;
}
.m-v2-topbar {
padding: 15px 0 15px;
max-width: 1084px;
margin: 0 auto;
@media screen and (max-width: 1168px) {
margin: 0 25px;
}
.m-v2-topbarNavItem__Logo {
margin: 0;
padding: 0;
}
}
.m-v2-topbar__Container__LoginWrapper > a {
margin-right: 40px;
@include m-theme() {
background: transparent;
border: 1px solid themed($m-black-always);
color: themed($m-black-always);
}
}
}
m-search--bar {
> .mdl-textfield {
padding: 8px 0;
......@@ -123,7 +161,6 @@
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.m-v2-topbar__Container--left {
......@@ -153,7 +190,7 @@
border-top: 3px solid transparent;
text-decoration: none;
@include m-theme() {
color: themed($m-grey-800);
color: themed($m-grey-800) !important;
}
&.m-v2-topbarNav__Item--active {
......@@ -271,11 +308,41 @@
font-family: 'Roboto', sans-serif;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white);
background-color: themed($m-white);
border: 1px solid themed($m-blue);
color: themed($m-blue);
}
}
> a.m-v2-topbarLoginWrapper__login,
> a.m-v2-topbarLoginWrapper__joinMindsNow {
font-size: 16px;
line-height: 21px;
font-weight: normal;
text-transform: none;
white-space: nowrap;
@include m-theme() {
color: themed($m-grey-800) !important;
}
}
> a.m-v2-topbarLoginWrapper__login {
padding: 0;
border: none !important;
@media screen and(max-width: $max-mobile) {
margin-right: 10px;
}
}
> a.m-v2-topbarLoginWrapper__joinMindsNow {
@include m-theme() {
border: 1px solid themed($m-grey-800) !important;
}
margin-right: 0 !important;
border-radius: 4px;
}
}
.m-v2-topbar__Bottom {
......
......@@ -6,11 +6,16 @@ import {
OnInit,
OnDestroy,
ViewChild,
HostListener,
HostBinding,
} from '@angular/core';
import { Session } from '../../../services/session';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { NotificationsToasterComponent } from '../../../modules/notifications/toaster.component';
import { ThemeService } from '../../../common/services/theme.service';
import { V2TopbarService } from './v2-topbar.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
@Component({
selector: 'm-v2-topbar',
......@@ -21,6 +26,12 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
minds = window.Minds;
timeout;
isTouchScreen = false;
forceBackground: boolean = true;
showBackground: boolean = true;
showSeparateLoginBtns: boolean = false;
marketingPages: boolean = false;
showTopbar: boolean = true;
showBottombar: boolean = true;
@ViewChild(DynamicHostDirective, { static: true })
notificationsToasterHost: DynamicHostDirective;
......@@ -28,16 +39,32 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
componentRef;
componentInstance: NotificationsToasterComponent;
onAuthPages: boolean = false; // sets to false if we're on login or register pages
router$;
constructor(
protected session: Session,
protected cd: ChangeDetectorRef,
private themeService: ThemeService,
protected componentFactoryResolver: ComponentFactoryResolver
protected componentFactoryResolver: ComponentFactoryResolver,
protected topbarService: V2TopbarService,
protected router: Router
) {}
ngOnInit() {
this.loadComponent();
this.session.isLoggedIn(() => this.detectChanges());
this.listen();
this.topbarService.setContainer(this);
}
toggleVisibility(visible: boolean) {
this.showTopbar = visible;
this.showBottombar = visible;
this.detectChanges();
}
getCurrentUser() {
......@@ -56,6 +83,33 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
this.componentInstance = this.componentRef.instance;
}
/**
* Marketing pages set this to true in order to change how the topbar looks
* @param value
* @param showBottombar
*/
toggleMarketingPages(
value: boolean,
showBottombar = true,
forceBackground: boolean = true
) {
this.marketingPages = value;
this.showSeparateLoginBtns = value;
this.showBottombar = value && showBottombar;
this.forceBackground = forceBackground;
this.onScroll();
this.detectChanges();
}
@HostListener('window:scroll')
onScroll() {
this.showBackground = this.forceBackground
? true
: this.marketingPages
? window.document.body.scrollTop > 52
: true;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......@@ -84,4 +138,27 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
clearTimeout(this.timeout);
}
}
private listen() {
this.setOnAuthPages(this.router.url);
this.router$ = this.router.events.subscribe(
(navigationEvent: NavigationEnd) => {
if (navigationEvent instanceof NavigationEnd) {
if (!navigationEvent.urlAfterRedirects) {
return;
}
this.setOnAuthPages(
navigationEvent.urlAfterRedirects || navigationEvent.url
);
}
}
);
}
private setOnAuthPages(url) {
this.onAuthPages = url === '/login' || url === '/register';
this.detectChanges();
}
}
import { V2TopbarComponent } from './v2-topbar.component';
export class V2TopbarService {
private container: V2TopbarComponent;
static _() {
return new V2TopbarService();
}
setContainer(container: V2TopbarComponent) {
this.container = container;
return this;
}
toggleMarketingPages(
value: boolean,
showBottombar: boolean = true,
forceBackground: boolean = true
) {
if (this.container) {
this.container.toggleMarketingPages(
value,
showBottombar,
forceBackground
);
}
}
toggleVisibility(visible: boolean) {
this.container.toggleVisibility(visible);
}
}
......@@ -16,6 +16,8 @@ import { switchMap, map, tap, first } from 'rxjs/operators';
export class FeedsService {
limit: BehaviorSubject<number> = new BehaviorSubject(12);
offset: BehaviorSubject<number> = new BehaviorSubject(0);
fallbackAt: number | null = null;
fallbackAtIndex: BehaviorSubject<number | null> = new BehaviorSubject(null);
pageSize: Observable<number>;
pagingToken: string = '';
canFetchMore: boolean = true;
......@@ -50,6 +52,22 @@ export class FeedsService {
.setCastToActivities(this.castToActivities)
.getFromFeed(feed)
),
tap(feed => {
if (feed.length && this.fallbackAt) {
for (let i = 0; i < feed.length; i++) {
const entity: any = feed[i].getValue();
if (
entity &&
entity.time_created &&
entity.time_created < this.fallbackAt
) {
this.fallbackAtIndex.next(i);
break;
}
}
}
}),
tap(feed => {
if (feed.length)
// We should have skipped but..
......@@ -143,6 +161,8 @@ export class FeedsService {
response.entities = response.activity;
}
if (response.entities.length) {
this.fallbackAt = response['fallback_at'];
this.fallbackAtIndex.next(null);
this.rawFeed.next(this.rawFeed.getValue().concat(response.entities));
this.pagingToken = response['load-next'];
} else {
......@@ -168,6 +188,8 @@ export class FeedsService {
* To clear data.
*/
clear(): FeedsService {
this.fallbackAt = null;
this.fallbackAtIndex.next(null);
this.offset.next(0);
this.pagingToken = '';
this.rawFeed.next([]);
......
<div class="m-embed-video" *ngIf="object.subtype == 'video'">
<m-video
[autoplay]="false"
[muted]="false"
[src]="[{ 'uri': object.src['720.mp4'] }]"
<m-videoPlayer
[guid]="object.guid"
[playCount]="object['play:count']"
[poster]="object['thumbnail_src']"
></m-video>
[autoplay]="false"
[shouldPlayInModal]="false"
>
</m-videoPlayer>
</div>
@import './grid-values';
@import '../../stylesheets/themes';
@import 'defaults';
.mf-button {
display: inline-block;
......@@ -63,7 +64,7 @@
}
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $max-mobile) {
display: block;
font-size: 15px;
padding: 12px 15px;
......
$m-grid-min-vp: 1168px;
$m-grid-max-mobile: 540px;
$m-grid-max-tablet: 900px;
$m-grid-cols: 12;
$m-grid-gap: 20px;
@import './grid-values';
@import '../../stylesheets/defaults';
.m-grid {
display: grid;
......@@ -10,13 +11,30 @@
.m-grid__column-#{$i} {
grid-column: auto / span $i;
}
.m-grid__column__skip-#{$i} {
grid-column-start: $i !important;
}
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
@for $i from 1 through $m-grid-cols {
.m-grid__column-#{$i}--tablet {
grid-column: auto / span $i;
}
.m-grid__column__skip-#{$i}--tablet {
grid-column-start: $i !important;
}
}
}
@media screen and (max-width: $m-grid-max-mobile) {
@for $i from 1 through $m-grid-cols {
.m-grid__column-#{$i}--mobile {
grid-column: auto / span $i;
}
.m-grid__column__skip-#{$i}--mobile {
grid-column-start: $i !important;
}
}
}
}
......@@ -28,17 +28,13 @@ export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
// TODO: remove all of this once channel search is ready
// Temporarily remove channel search from channel filter options
this.subscription = this.analyticsService.filters$.subscribe(filters => {
this.filters = filters;
const channelFilter = filters.find(filter => filter.id === 'channel');
channelFilter.options = channelFilter.options.filter(option => {
return option.id === 'all' || option.id === 'self';
});
this.filters.find(filter => filter.id === 'channel').options =
channelFilter.options;
if (channelFilter) {
channelFilter.options = channelFilter.options.filter(option => {
return option.id === 'all' || option.id === 'self';
});
}
this.filters = filters;
this.detectChanges();
});
}
......
......@@ -30,9 +30,11 @@
badChange: metric.hasChanged && !metric.positiveTrend
}"
>
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0">arrow_downward</i>
<span>{{ metric.delta | percent: '1.0-1' }}</span>
<ng-container *ngIf="metric.hasChanged">
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0">arrow_downward</i>
</ng-container>
<span *ngIf="metric.delta">{{ metric.delta | percent: '1.0-1' }}</span>
</div>
</div>
</ng-container>
......
......@@ -48,27 +48,39 @@ export class AnalyticsMetricsComponent implements OnInit {
this.userRoles.includes(role)
);
if (metric.summary) {
let delta;
if (metric.summary.comparison_value !== 0) {
delta =
(metric.summary.current_value -
metric.summary.comparison_value) /
(metric.summary.comparison_value || 0);
const cur: number = metric.summary.current_value || 0;
const cmp: number = metric.summary.comparison_value || 0;
let delta: number, hasChanged: boolean, positiveTrend: boolean;
if (cur === cmp) {
// Same values, no changes
hasChanged = false;
delta = 0;
} else if (cmp === 0) {
// Comparison value is 0, cannot calculate %
hasChanged = true;
delta = Infinity; // Will display infinity symbol
positiveTrend = cur > 0;
} else {
delta = 1;
// Normal cases
hasChanged = true;
delta = (cur - cmp) / cmp;
positiveTrend = delta > 0;
}
metric['delta'] = delta;
metric['hasChanged'] = delta === 0 ? false : true;
if (!metric.summary.comparison_positive_inclination) {
// If "comparison positive inclination" is not true, it
// represents a "not-so-good" metric. So we'll flip the colors.
// Upwards will be "bad"
// Downwards will be "good"
if (
(delta > 0 && metric.summary.comparison_positive_inclination) ||
(delta < 0 && !metric.summary.comparison_positive_inclination)
) {
metric['positiveTrend'] = true;
} else {
metric['positiveTrend'] = false;
positiveTrend = !positiveTrend;
}
metric['delta'] = delta;
metric['hasChanged'] = hasChanged;
metric['positiveTrend'] = positiveTrend;
}
}
return metrics;
......
......@@ -32,10 +32,10 @@
class="m-analyticsDashboard__description"
*ngIf="description$ | async as description"
>
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to Pro</a
<span *ngIf="!session.getLoggedInUser().pro">
In order to start earning,
<a routerLink="/pro">upgrade to Pro</a>.</span
>
<a
*ngIf="
......@@ -49,6 +49,7 @@
>Enable payouts</a
>
</ng-container>
{{ description }}
</p>
<m-analytics__layout--chart
m-dashboardLayout__body
......
......@@ -40,7 +40,7 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
label: 'Timespan',
options: [],
};
channelFilter: Filter;
// channelFilter: Filter;
layout = 'chart';
constructor(
......@@ -66,6 +66,8 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.updateCategory(cat);
if (cat === 'summary') {
this.layout = 'summary';
} else {
this.layout = 'chart';
}
});
......@@ -87,13 +89,18 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.detectChanges();
});
this.analyticsService.filters$.subscribe(filters => {
this.channelFilter = filters.find(filter => filter.id === 'channel');
// TODO: remove this once channel search is ready
// Temporarily remove channel search from filter options
this.channelFilter.options = this.channelFilter.options.filter(option => {
return option.id === 'all' || option.id === 'self';
});
// const channelFilter = filters.find(filter => filter.id === 'channel');
// if (channelFilter) {
// this.channelFilter = channelFilter;
// // Temporarily remove channel search from filter options
// this.channelFilter.options = this.channelFilter.options.filter(
// option => {
// return option.id === 'all' || option.id === 'self';
// }
// );
// }
this.detectChanges();
});
......
......@@ -13,7 +13,7 @@ const fakeData: Array<any> = [
comparison_interval: 30,
from_ts_ms: 1567296000000,
from_ts_iso: '2019-09-01T00:00:00+00:00',
selected: false,
selected: true,
},
{
id: '12m',
......
<div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<!-- <div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
</div> -->
<m-shadowboxLayout
*ngIf="selectedMetric && selectedMetric.visualisation"
[hasHeader]="selectedMetric.visualisation.type === 'chart'"
......@@ -13,6 +13,9 @@
class="m-shadowboxLayout__body"
[ngClass]="{ isTable: isTable, isMobile: isMobile }"
>
<div class="m-analytics__spinnerContainer" *ngIf="loading$ | async">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
<m-analytics__chart
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__chart>
......
......@@ -4,3 +4,6 @@
min-width: 420px;
}
}
.m-analytics__spinnerContainer {
min-height: 331px;
}
......@@ -62,7 +62,7 @@ export class AnalyticsLayoutSummaryComponent implements OnInit {
},
{
id: 'earnings_total',
label: 'Total PRO Earnings',
label: 'Total Pro Earnings',
unit: 'usd',
interval: 'day',
endpoint: this.url + 'earnings',
......
<div class="m-login">
<div>
<h3 i18n="@@MINDS__HOME__LOGIN__LOGIN">Login to Minds</h3>
<minds-form-login
(done)="loggedin()"
(doneRegistered)="registered()"
></minds-form-login>
<div class="m-grid" *mIfFeature="'register_pages-december-2019'; else oldLogin">
<div
class="m-grid__column-7 m-grid__column__skip-5 m-grid__column-10--tablet m-grid__column__skip-2--tablet m-grid__column-12--mobile m-grid__column__skip-1--mobile"
>
<div class="m-login__wrapper">
<minds-form-login
[showBigButton]="true"
[showInlineErrors]="true"
[showLabels]="true"
[showTitle]="true"
(done)="loggedin()"
(doneRegistered)="registered()"
>
</minds-form-login>
</div>
</div>
</div>
<ng-template #oldLogin>
<div class="m-login">
<div>
<h3 i18n="@@MINDS__HOME__LOGIN__LOGIN">Login to Minds</h3>
<minds-form-login
(done)="loggedin()"
(doneRegistered)="registered()"
></minds-form-login>
</div>
<div>
<h3 i18n="@@M__COMMON__START_A_CHANNEL_MINDS_REF">
Not on Minds? Start a Minds channel
</h3>
<minds-form-register
[referrer]="referrer"
(done)="registered()"
parentId="/login"
></minds-form-register>
<div>
<h3 i18n="@@M__COMMON__START_A_CHANNEL_MINDS_REF">
Not on Minds? Start a Minds channel
</h3>
<minds-form-register
[referrer]="referrer"
(done)="registered()"
parentId="/login"
></minds-form-register>
</div>
</div>
</div>
</ng-template>
......@@ -2,30 +2,210 @@
m-login {
display: block;
@include m-theme() {
background-color: themed($m-white);
}
}
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
.m-login {
margin: 100px auto;
display: flex;
max-width: 990px;
flex-direction: row;
min-height: 100%;
&:not(.m-login__newDesign) {
.m-login {
margin: 100px auto;
display: flex;
max-width: 990px;
flex-direction: row;
min-height: 100%;
@media screen and (max-width: $max-mobile) {
flex-direction: column;
}
@media screen and (max-width: $max-mobile) {
flex-direction: column;
}
> div {
margin: 16px;
flex: 1;
> div {
margin: 16px;
flex: 1;
}
h3 {
font-weight: 800;
font-size: 18px;
margin: 0 8px;
}
}
}
h3 {
font-weight: 800;
font-size: 18px;
margin: 0 8px;
&.m-login__newDesign {
margin-top: -52px;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 50%;
left: 30%;
clip-path: polygon(55% 0, 100% 0, 100% 11%, 18% 87%);
@include m-theme() {
background: themed($m-amber-medium);
}
//@media screen and (max-width: $max-mobile) {
// right: 0;
// bottom: -3vw;
// clip-path: polygon(83% 0%, 100% 0, 116% 22%, 30% 43%);
//}
}
@media screen and (max-width: $m-grid-max-mobile) {
margin: -52px 26px 50px;
&::before {
position: absolute;
top: 0;
right: 0;
clip-path: polygon(55% 0, 100% 0, 100% 30%, 18% 87%);
background: url(http://localhost/en/assets/marketing/deco_2-straight.svg)
no-repeat;
}
}
.m-grid {
padding: 15vh 0 0;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 10vh 0 0;
}
}
.m-login__wrapper {
display: block;
max-width: 692px;
filter: drop-shadow(-1px 0px 8px rgba(50, 50, 0, 0.5));
}
minds-form-login {
display: block;
background-color: #fcfcfc;
padding: 86px 67px;
clip-path: polygon(0 2%, 100% 0, 100% 97%, 0 95%);
@media screen and (max-width: $m-grid-max-mobile) {
clip-path: polygon(0 2%, 100% 0, 100% 100%, 0 99%);
padding: 55px 26px 47px;
h3,
.m-register__alreadyAUser {
text-align: center;
}
.mdl-card__actions {
margin-top: 35px;
label.mdl-checkbox {
margin-bottom: 50px;
}
}
}
h3 {
font-size: 36px;
line-height: 48px;
font-weight: bold;
@include m-theme() {
color: themed($m-grey-800);
}
}
.mdl-cell {
margin: 0;
padding-bottom: 25px;
}
form {
background: transparent !important;
.mdl-card__supporting-text {
overflow: visible;
}
.mdl-cell {
width: 100%;
}
input:not([type='checkbox']) {
padding: 10px 15px;
height: 37px;
font-weight: normal;
@include m-theme() {
color: themed($m-grey-800);
}
&:active,
&:focus {
@include m-theme() {
outline: 1px solid themed($m-blue);
}
}
}
&.m-loginBox__bigButton {
.mdl-card__actions {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
button.mf-button {
width: 132px;
height: 60px;
}
}
}
}
span,
label {
font-size: 14px;
line-height: 19px;
}
label:not(.mdl-checkbox) {
display: inline-block;
margin-bottom: 10px;
@include m-theme() {
color: themed($m-grey-300);
}
}
label.mdl-checkbox {
display: flex;
align-items: center;
padding-top: 0;
margin-bottom: 33px;
}
.mdl-card__actions {
flex-direction: column;
align-items: flex-start;
margin-top: 55px;
padding: 0;
& > *:not(button) {
color: #4a4a4a !important;
}
a {
color: #4a90e2;
}
button {
align-self: flex-end;
@include m-theme() {
background-color: themed($m-aqua);
}
}
}
}
}
}
......@@ -27,6 +27,11 @@ import { mindsTitleMock } from '../../mocks/services/ux/minds-title.service.mock
import { signupModalServiceMock } from '../../mocks/modules/modals/signup/signup-modal-service.mock';
import { SignupModalService } from '../modals/signup/service';
import { By } from '@angular/platform-browser';
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { IfFeatureDirective } from '../../common/directives/if-feature.directive';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { MockService } from '../../utils/mock';
@Component({
selector: 'minds-form-login',
......@@ -35,6 +40,10 @@ import { By } from '@angular/platform-browser';
class MindsFormLoginMock {
@Output() done: EventEmitter<any> = new EventEmitter<any>();
@Output() doneRegistered: EventEmitter<any> = new EventEmitter<any>();
@Input() showBigButton: boolean = false;
@Input() showInlineErrors: boolean = false;
@Input() showTitle: boolean = false;
@Input() showLabels: boolean = false;
}
@Component({
......@@ -57,6 +66,7 @@ describe('LoginComponent', () => {
MindsFormLoginMock,
MindsFormRegisterMock,
LoginComponent,
IfFeatureDirective,
],
imports: [
RouterTestingModule,
......@@ -71,6 +81,8 @@ describe('LoginComponent', () => {
{ provide: OnboardingService, useValue: onboardingServiceMock },
{ provide: MindsTitle, useValue: mindsTitleMock },
{ provide: SignupModalService, useValue: signupModalServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: V2TopbarService, useValue: MockService(V2TopbarService) },
],
}).compileComponents();
}));
......@@ -80,6 +92,8 @@ describe('LoginComponent', () => {
jasmine.clock().uninstall();
jasmine.clock().install();
featuresServiceMock.mock('register_pages-december-2019', false);
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance;
......
import { Component } from '@angular/core';
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -8,20 +8,23 @@ import { MindsTitle } from '../../services/ux/title';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { OnboardingService } from '../onboarding/onboarding.service';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
@Component({
selector: 'm-login',
templateUrl: 'login.component.html',
})
export class LoginComponent {
export class LoginComponent implements OnInit, OnDestroy {
errorMessage: string = '';
twofactorToken: string = '';
hideLogin: boolean = false;
inProgress: boolean = false;
referrer: string;
minds = window.Minds;
private redirectTo: string;
@HostBinding('class.m-login__newDesign')
newDesign: boolean = false;
flags = {
canPlayInlineVideos: true,
......@@ -29,6 +32,8 @@ export class LoginComponent {
paramsSubscription: Subscription;
private redirectTo: string;
constructor(
public client: Client,
public router: Router,
......@@ -36,8 +41,9 @@ export class LoginComponent {
public title: MindsTitle,
private modal: SignupModalService,
private loginReferrer: LoginReferrerService,
public session: Session,
private onboarding: OnboardingService
private featuresService: FeaturesService,
private topbarService: V2TopbarService,
public session: Session
) {}
ngOnInit() {
......@@ -58,21 +64,33 @@ export class LoginComponent {
if (/iP(hone|od)/.test(window.navigator.userAgent)) {
this.flags.canPlayInlineVideos = false;
}
this.newDesign = this.featuresService.has('register_pages-december-2019');
if (this.newDesign) {
this.topbarService.toggleVisibility(false);
}
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
this.topbarService.toggleVisibility(true);
}
loggedin() {
if (this.referrer) this.router.navigateByUrl(this.referrer);
else if (this.redirectTo) this.navigateToRedirection();
else this.loginReferrer.navigate();
if (this.referrer) {
this.router.navigateByUrl(this.referrer);
} else if (this.redirectTo) {
this.navigateToRedirection();
} else {
this.loginReferrer.navigate();
}
}
registered() {
if (this.redirectTo) this.navigateToRedirection();
else {
if (this.redirectTo) {
this.navigateToRedirection();
} else {
this.loginReferrer.navigate({
defaultUrl: '/' + this.session.getLoggedInUser().username,
});
......
<section class="m-register--hero">
<div class="m-register--hero--video">
<video autoplay muted loop *ngIf="!videoError; else fallback">
<source
[src]="minds.cdn_assets_url + 'assets/videos/what-1/what-1.mp4'"
type="video/mp4"
(error)="onSourceError()"
/>
</video>
<ng-template #fallback>
<img [src]="minds.cdn_assets_url + 'assets/photos/cover.png'" />
</ng-template>
</div>
<div class="m-register--hero--inner">
<div class="m-register--hero--overlay"></div>
<div class="m-register--hero--slogans">
<h1 i18n="@@M__SOCIAL_NETWORK_SLOGAN">Where minds gather</h1>
<h3 i18n="@@MINDS__HOME__register__LAUNCH_CTA">
The leading open source social network for Internet freedom. Earn crypto
and free promotion for your contributions.
</h3>
</div>
<div class="m-register--signup" [hidden]="session.isLoggedIn()">
<div
class="m-grid"
*mIfFeature="'register_pages-december-2019'; else registerBlock"
>
<div
class="m-grid__column-7 m-grid__column__skip-5 m-grid__column-10--tablet m-grid__column__skip-2--tablet m-grid__column-12--mobile m-grid__column__skip-1--mobile"
>
<div class="m-register__wrapper">
<minds-form-register
[showTitle]="true"
[showBigButton]="true"
[showPromotions]="false"
[showLabels]="true"
[showInlineErrors]="true"
(done)="registered()"
[referrer]="referrer"
parentId="/register"
></minds-form-register>
>
</minds-form-register>
</div>
</div>
</section>
</div>
<div class="mdl-grid mdl-grid--no-spacing m-register--footer">
<section class="mdl-cell mdl-cell--12-col m-footer">
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" />
<ul class="m-footer-nav m-footer-nav-inline">
<li
*ngFor="let page of navigation.getItems('footer')"
class="m-footer-nav-item "
>
<a
*ngIf="page.path && page.path.indexOf('p/') > -1"
[routerLink]="[page.path]"
>{{ page.title }}</a
>
<a
*ngIf="page.path.indexOf('p/') < 0"
[href]="page.path"
target="_blank"
>{{ page.title }}</a
>
</li>
</ul>
<span class="copyright" i18n="@@M__COMMON__COPYRIGHT_YEAR"
>&#169; Minds {{ '2019' }}</span
>
<ng-template #registerBlock>
<section class="m-register--hero">
<div class="m-register--hero--video">
<video autoplay muted loop *ngIf="!videoError; else fallback">
<source
[src]="minds.cdn_assets_url + 'assets/videos/what-1/what-1.mp4'"
type="video/mp4"
(error)="onSourceError()"
/>
</video>
<ng-template #fallback>
<img [src]="minds.cdn_assets_url + 'assets/photos/cover.png'" />
</ng-template>
</div>
<div class="m-register--hero--inner">
<div class="m-register--hero--overlay"></div>
<div class="m-register--hero--slogans">
<h1 i18n="@@M__SOCIAL_NETWORK_SLOGAN">Where minds gather</h1>
<h3 i18n="@@MINDS__HOME__register__LAUNCH_CTA">
The leading open source social network for Internet freedom. Earn
crypto and free promotion for your contributions.
</h3>
</div>
<div class="m-register--signup" [hidden]="session.isLoggedIn()">
<minds-form-register
(done)="registered()"
[referrer]="referrer"
parentId="/register"
></minds-form-register>
</div>
</div>
</section>
</div>
<div class="mdl-grid mdl-grid--no-spacing m-register--footer">
<section class="mdl-cell mdl-cell--12-col m-footer">
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" />
<ul class="m-footer-nav m-footer-nav-inline">
<li
*ngFor="let page of navigation.getItems('footer')"
class="m-footer-nav-item "
>
<a
*ngIf="page.path && page.path.indexOf('p/') > -1"
[routerLink]="[page.path]"
>{{ page.title }}</a
>
<a
*ngIf="page.path.indexOf('p/') < 0"
[href]="page.path"
target="_blank"
>{{ page.title }}</a
>
</li>
</ul>
<span class="copyright" i18n="@@M__COMMON__COPYRIGHT_YEAR"
>&#169; Minds {{ '2019' }}</span
>
</section>
</div>
</ng-template>
......@@ -15,7 +15,11 @@ import { OnboardingService } from '../onboarding/onboarding.service';
import { onboardingServiceMock } from '../../mocks/modules/onboarding/onboarding.service.mock.spec';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { MockComponent } from '../../utils/mock';
import { MockComponent, MockService } from '../../utils/mock';
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { IfFeatureDirective } from '../../common/directives/if-feature.directive';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
describe('RegisterComponent', () => {
let comp: RegisterComponent;
......@@ -31,6 +35,7 @@ describe('RegisterComponent', () => {
outputs: ['done'],
}),
RegisterComponent,
IfFeatureDirective,
],
imports: [RouterTestingModule, ReactiveFormsModule],
providers: [
......@@ -38,7 +43,8 @@ describe('RegisterComponent', () => {
{ provide: Client, useValue: clientMock },
{ provide: SignupModalService, useValue: signupModalServiceMock },
{ provide: LoginReferrerService, useValue: loginReferrerServiceMock },
{ provide: OnboardingService, useValue: onboardingServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: V2TopbarService, useValue: MockService(V2TopbarService) },
],
}).compileComponents();
}));
......@@ -48,6 +54,7 @@ describe('RegisterComponent', () => {
fixture = TestBed.createComponent(RegisterComponent);
comp = fixture.componentInstance;
featuresServiceMock.mock('register_pages-december-2019', false);
window.Minds.cdn_assets_url = 'http://dev.minds.io/';
comp.flags.canPlayInlineVideos = true;
......
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, OnInit, OnDestroy, HostBinding } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -9,6 +9,10 @@ import { Session } from '../../services/session';
import { SignupModalService } from '../modals/signup/service';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { OnboardingService } from '../onboarding/onboarding.service';
import { MindsTitle } from '../../services/ux/title';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { OnboardingV2Service } from '../onboarding-v2/service/onboarding.service';
@Component({
selector: 'm-register',
......@@ -22,6 +26,9 @@ export class RegisterComponent implements OnInit, OnDestroy {
inProgress: boolean = false;
videoError: boolean = false;
referrer: string;
@HostBinding('class.m-register__newDesign')
newDesign: boolean = false;
private redirectTo: string;
flags = {
canPlayInlineVideos: true,
......@@ -37,15 +44,32 @@ export class RegisterComponent implements OnInit, OnDestroy {
private loginReferrer: LoginReferrerService,
public session: Session,
private onboarding: OnboardingService,
public navigation: NavigationService
public navigation: NavigationService,
private featuresService: FeaturesService,
private topbarService: V2TopbarService,
private onboardingService: OnboardingV2Service,
public title: MindsTitle
) {
if (this.session.isLoggedIn()) {
this.router.navigate(['/newsfeed']);
return;
}
this.newDesign = this.featuresService.has('register_pages-december-2019');
if (this.newDesign) {
this.topbarService.toggleVisibility(false);
}
}
ngOnInit() {
if (this.session.isLoggedIn()) {
this.loginReferrer.register('/newsfeed');
this.loginReferrer.navigate();
}
this.redirectTo = localStorage.getItem('redirect');
// Set referrer if there is one
this.paramsSubscription = this.route.queryParams.subscribe(params => {
if (params['referrer']) {
......@@ -53,12 +77,26 @@ export class RegisterComponent implements OnInit, OnDestroy {
}
});
this.title.setTitle('Register');
if (/iP(hone|od)/.test(window.navigator.userAgent)) {
this.flags.canPlayInlineVideos = false;
}
}
registered() {
if (this.redirectTo) {
this.navigateToRedirection();
return;
}
if (this.featuresService.has('onboarding-december-2019')) {
if (this.onboardingService.shouldShow()) {
this.router.navigate(['/onboarding']);
}
return;
}
this.router.navigate(['/' + this.session.getLoggedInUser().username]);
}
......@@ -70,5 +108,22 @@ export class RegisterComponent implements OnInit, OnDestroy {
if (this.paramsSubscription) {
this.paramsSubscription.unsubscribe();
}
this.topbarService.toggleVisibility(true);
}
private navigateToRedirection() {
const uri = this.redirectTo.split('?', 2);
const extras = {};
if (uri[1]) {
extras['queryParams'] = {};
for (const queryParamString of uri[1].split('&')) {
const queryParam = queryParamString.split('=');
extras['queryParams'][queryParam[0]] = queryParam[1];
}
}
this.router.navigate([uri[0]], extras);
}
}
......@@ -45,18 +45,7 @@
<h2>Experiments</h2>
<ul class="m-canaryExperiments__list">
<li>
Multi Currency Wire
<a href="https://gitlab.com/minds/front/merge_requests/508"
>(front!508)</a
>
- 17th September '19
</li>
<li>
Post scheduler
<a href="https://gitlab.com/minds/front/merge_requests/494"
>(front!494)</a
>
- 17th September '19
Discovery algorithm by post age - 11th December '19
</li>
</ul>
</div>
......
<div class="m-channel--explicit-overlay--content">
<h3>
This channel contains mature content
</h3>
<div
class="m-btn m-btn--slim m-btn--action m-channel--explicit-overlay--action"
(click)="disableFilter()"
>
View
<div class="m-channel--explicit-overlay--container" *ngIf="!hidden">
<div class="m-channel--explicit-overlay--content">
<h3>
This channel contains content that is NSFW
</h3>
<div
class="m-btn m-btn--slim m-btn--action m-channel--explicit-overlay--action"
(click)="disableFilter()"
>
View
</div>
</div>
</div>
m-channel--explicit-overlay {
.m-channel--explicit-overlay--container {
display: flex;
justify-content: center;
align-items: center;
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExplicitOverlayComponent } from './overlay.component';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { Storage } from '../../../services/storage';
import { Router } from '@angular/router';
import { storageMock } from '../../../../tests/storage-mock.spec';
let routerMock = new (function() {
this.navigate = jasmine.createSpy('navigate');
})();
describe('OverlayComponent', () => {
let comp: ExplicitOverlayComponent;
let fixture: ComponentFixture<ExplicitOverlayComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ExplicitOverlayComponent],
imports: [],
providers: [
{ provide: Storage, useValue: storageMock },
{ provide: Session, useValue: sessionMock },
{ provide: Router, useValue: routerMock },
],
}).compileComponents();
}));
beforeEach(done => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 10;
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(ExplicitOverlayComponent);
comp = fixture.componentInstance;
comp.hidden = true;
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should not show overlay when mature visibility is set', () => {
comp.channel = {
mature_visibility: true,
};
comp.showOverlay();
fixture.detectChanges();
expect(comp.hidden).toBeTruthy();
});
it('should overlay when channel is mature', () => {
comp._channel = {
is_mature: true,
};
comp.showOverlay();
fixture.detectChanges();
expect(comp.hidden).toBeFalsy();
});
it('should overlay when channel is nsfw for one reason', () => {
comp._channel = {
nsfw: [1],
};
comp.showOverlay();
fixture.detectChanges();
expect(comp.hidden).toBeFalsy();
});
it('should overlay when channel is nsfw for multiple reason', () => {
comp._channel = {
nsfw: [1, 2, 3],
};
comp.showOverlay();
fixture.detectChanges();
expect(comp.hidden).toBeFalsy();
});
it('should overlay not show overlay if channel is not nsfw, mature and no mature_visibility', () => {
comp._channel = {
mature_visibility: false,
is_mature: false,
nsfw: [],
};
comp.showOverlay();
fixture.detectChanges();
expect(comp.hidden).toBeTruthy();
});
it('should not register undefined values as a false positive, and show the overlay', () => {
comp._channel = {
mature_visibility: undefined,
is_mature: undefined,
nsfw: undefined,
};
comp.showOverlay();
fixture.detectChanges();
expect(comp.hidden).toBeTruthy();
});
});
import { Component, HostBinding, Input } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Session } from '../../../services/session';
import { Router } from '@angular/router';
import { Storage } from '../../../services/storage';
......@@ -8,16 +8,12 @@ import { Storage } from '../../../services/storage';
templateUrl: 'overlay.component.html',
})
export class ExplicitOverlayComponent {
@HostBinding('hidden') hidden: boolean;
_channel: any;
public hidden = true;
public _channel: any;
@Input() set channel(value: any) {
this._channel = value;
this.hidden =
!this._channel ||
!this._channel.is_mature ||
this._channel.mature_visibility;
this.showOverlay();
}
constructor(
......@@ -34,8 +30,31 @@ export class ExplicitOverlayComponent {
this.router.navigate(['/login']);
}
disableFilter() {
/**
* Disables overlay screen, revealing channel.
*/
protected disableFilter(): void {
this._channel.mature_visibility = true;
this.hidden = true;
}
/**
* Determines whether the channel overlay should be shown
* over the a channel.
*/
public showOverlay(): void {
if (!this._channel) {
return;
}
if (this._channel.mature_visibility) {
this.hidden = true;
} else if (this._channel.is_mature) {
this.hidden = false;
} else if (this._channel.nsfw && this._channel.nsfw.length > 0) {
this.hidden = false;
} else {
this.hidden = true;
}
}
}
......@@ -263,16 +263,14 @@
</button>
</div>
<ng-container *mIfFeature="'purchase-pro'">
<a
*ngIf="showBecomeProButton"
class="m-btn m-link-btn m-btn--with-icon m-btn--slim m-btn--action"
routerLink="/pro"
>
<i class="material-icons">business_center</i>
<span i18n>Become Pro</span>
</a>
</ng-container>
<a
*ngIf="showBecomeProButton"
class="m-btn m-link-btn m-btn--with-icon m-btn--slim m-btn--action"
[routerLink]="proSettingsRouterLink"
>
<i class="material-icons">business_center</i>
<span i18n>Try Pro</span>
</a>
<a
*ngIf="showProSettings"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.