...
 
Commits (63)
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');
});
});
})
import generateRandomId from '../../support/utilities';
const groupId = generateRandomId();
context('Groups', () => {
before(() => {
cy.getCookie('minds_sess')
......@@ -25,7 +29,7 @@ context('Groups', () => {
cy.uploadFile('minds-banner #file', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
// add a name
cy.get('.m-group-info-name > input').type('test');
cy.get('.m-group-info-name > input').type(groupId);
// add a description
cy.get('.m-group-info-brief-description > textarea').type('This is a test');
......@@ -41,15 +45,20 @@ 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__name').contains(groupId);
cy.get('.m-groupInfo__description').contains('This is a test');
// open settings button
......@@ -58,7 +67,7 @@ context('Groups', () => {
cy.get('minds-groups-settings-button ul.minds-dropdown-menu li:first-child').contains('Edit').click();
// edit name
cy.get('.m-groupInfo__name input').type(' group');
cy.get('.m-groupInfo__name input').type(' edit');
// edit description
cy.get('.m-groupInfo__description textarea').type(' group');
......@@ -68,16 +77,12 @@ context('Groups', () => {
cy.get('minds-groups-settings-button ul.minds-dropdown-menu li:first-child').contains('Save').click();
cy.get('.m-groupInfo__name').contains('test group');
cy.get('.m-groupInfo__name').contains(groupId + ' edit');
cy.get('.m-groupInfo__description').contains('This is a test group');
})
it('should be able to toggle conversation and comment on it', () => {
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.contains(groupId).click();
// toggle the conversation
cy.get('.m-groupGrid__right').should('be.visible');
......@@ -98,9 +103,7 @@ 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.contains(groupId).click();
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
......@@ -110,7 +113,7 @@ context('Groups', () => {
cy.get('.m-posterActionBar__PostButton').click();
// the activity should show that it was posted in this group
cy.get('.minds-list minds-activity .body a:nth-child(2)').contains('(test group)');
cy.get('.minds-list minds-activity .body a:nth-child(2)').contains(`(${groupId} edit)`);
cy.get('.minds-list minds-activity .m-mature-message-content').contains('This is a post');
......@@ -127,15 +130,25 @@ context('Groups', () => {
});
});
it('should navigate to discovery when Find a Group clicked', () => {
cy.contains('Find a Group').click()
cy.location('pathname')
.should('eq', '/newsfeed/global/top%3Bperiod%3D12h%3Btype%3Dgroups%3Ball%3D1');
});
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 - 2})`).click();
cy.contains(groupId).click();
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.contains('Delete Group').click();
cy.contains('Confirm').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();
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);
......
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');
});
});
......@@ -11,15 +11,17 @@ context('Pro Product Page', () => {
cy.preserveCookies();
});
const upgradeButton = 'm-pro--subscription .mf-button';
const upgradeButton = '[data-cy=data-minds-pro-upgrade-button]';
const wirePaymentsComponent = 'm-wire__paymentscreator .m-wire--creator';
it('should show a coming soon button', () => {
cy.visit('/pro');
it('should show an Upgrade to Pro button', () => {
cy.visit('/pro')
.location('pathname')
.should('eq', '/pro');
cy.get(upgradeButton)
.should('be.visible')
.should('contain', 'Coming soon')
.should('contain', 'Upgrade to Pro')
.click();
});
......
......@@ -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: '[data-cy=data-minds-login-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.
......@@ -20,6 +20,7 @@
"private": true,
"dependencies": {
"@angular/animations": "~8.0.3",
"@angular/cdk": "^8.2.3",
"@angular/common": "~8.0.3",
"@angular/compiler": "~8.0.3",
"@angular/core": "~8.0.3",
......@@ -28,7 +29,7 @@
"@angular/platform-browser": "~8.0.3",
"@angular/platform-browser-dynamic": "~8.0.3",
"@angular/router": "~8.0.3",
"@sentry/browser": "^5.6.2",
"@sentry/browser": "^5.11.1",
"angular-plotly.js": "^1.3.2",
"bn.js": "^4.11.8",
"braintree-web": "3.41.0",
......@@ -39,10 +40,11 @@
"ethjs-provider-signer": "^0.1.4",
"ethjs-signer": "0.1.1",
"global": "^4.3.2",
"material-datetime-picker": "git+https://github.com/Minds/material-datetime-picker.git",
"material-design-icons": "~3.0.1",
"material-design-lite": "~1.3.0",
"medium-editor": "^5.23.2",
"moment": "^2.24.0",
"ng-pick-datetime": "^7.0.0",
"ngx-drag-drop": "^2.0.0",
"ngx-plyr": "^3.0.1",
"plotly.js": "^1.47.4",
......
......@@ -34,13 +34,13 @@
[class.is-pro-domain]="isProDomain"
>
<m-emailConfirmation></m-emailConfirmation>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<m-announcement [id]="'festival:sale'" *mIfFeature="'radiocity'">
<span
class="m-blockchain--wallet-address-notice--action"
routerLink="/tokens"
i18n="@@BLOCKCHAIN__SALE__NOTICE"
i18n="@@MINDS_FESTIVAL_TICKET_SALE"
>
The MINDS token is now live. Learn more here.
BREAKING: Tickets on sale for "MINDS: FESTIVAL OF IDEAS" @ Radio City on
6/13/2020. HELP US SELL OUT FAST!
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
......@@ -58,7 +58,10 @@
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<m-modal-signup-on-scroll *ngIf="!isProDomain"></m-modal-signup-on-scroll>
<m-modal-signup
*ngIf="!isProDomain && !session.getLoggedInUser()"
[open]="false"
></m-modal-signup>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
......
......@@ -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,
......
......@@ -121,12 +121,18 @@ import { PageLayoutComponent } from './components/page-layout/page-layout.compon
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
import { ShadowboxHeaderComponent } from './components/shadowbox-header/shadowbox-header.component';
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime';
import { DropdownSelectorComponent } from './components/dropdown-selector/dropdown-selector.component';
import { ShadowboxSubmitButtonComponent } from './components/shadowbox-submit-button/shadowbox-submit-button.component';
import { FormDescriptorComponent } from './components/form-descriptor/form-descriptor.component';
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { SsoService } from './services/sso.service';
import { PagesService } from './services/pages.service';
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';
import { HorizontalFeedService } from './services/horizontal-feed.service';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -145,6 +151,8 @@ const routes: Routes = [
FormsModule,
ReactiveFormsModule,
PlotlyModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
RouterModule.forChild(routes),
],
declarations: [
......@@ -251,6 +259,7 @@ const routes: Routes = [
FormToastComponent,
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
],
exports: [
MINDS_PIPES,
......@@ -351,10 +360,12 @@ const routes: Routes = [
FormToastComponent,
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
],
providers: [
SiteService,
SsoService,
PagesService,
{
provide: AttachmentService,
useFactory: AttachmentService._,
......@@ -417,6 +428,15 @@ const routes: Routes = [
useFactory: router => new RouterHistoryService(router),
deps: [Router],
},
{
provide: V2TopbarService,
useFactory: V2TopbarService._,
},
{
provide: SidebarMarkersService,
useFactory: SidebarMarkersService._,
},
HorizontalFeedService,
],
entryComponents: [
NotificationsToasterComponent,
......
import { Component, EventEmitter, Input } from '@angular/core';
import { Component, EventEmitter, Input, OnInit } from '@angular/core';
import { Storage } from '../../../services/storage';
import { Client } from '../../../services/api';
......@@ -20,7 +20,7 @@ import { Client } from '../../../services/api';
</div>
`,
})
export class AnnouncementComponent {
export class AnnouncementComponent implements OnInit {
minds: Minds = window.Minds;
hidden: boolean = false;
@Input() id: string = 'default';
......
......@@ -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
);
}
}
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);
}
}
m-date-selector {
.m-date-selector--label {
text-transform: uppercase;
letter-spacing: 2.5px;
align-self: center;
font-size: 12px;
@include m-theme() {
color: themed($m-grey-600);
}
.m-dateSelector__label {
text-transform: uppercase;
letter-spacing: 2.5px;
align-self: center;
font-size: 12px;
@include m-theme() {
color: themed($m-grey-600);
}
}
.m-dateSelector__input {
font-size: 12px;
text-transform: uppercase;
text-align: center;
padding: 8px 0;
@media screen and (max-width: $min-tablet) {
margin: 8px 0;
}
@include m-theme() {
color: themed($m-grey-600);
background-color: themed($m-white);
}
}
.m-date-selector--input {
display: flex;
align-items: center;
.cdk-overlay-container {
z-index: 10000 !important;
.owl-dt-container {
@include m-theme() {
color: themed($m-grey-600);
background-color: themed($m-white);
//border: 1px solid themed($m-grey-100);
}
background: themed($m-white);
input {
font-size: 12px;
text-transform: uppercase;
background-color: transparent;
padding: 8px 0;
border: none;
text-align: center;
width: auto;
height: auto;
align-self: center;
@include m-theme() {
color: rgba(themed($m-black), 0.54);
* {
color: themed($m-grey-600);
}
}
i {
vertical-align: middle;
cursor: pointer;
margin-right: 8px;
.owl-dt-calendar-cell-selected {
background-color: themed($m-blue);
color: themed($m-white);
}
}
}
}
.m-dateSelector__input--hidden {
visibility: hidden;
position: absolute;
right: 0;
bottom: 0;
}
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DatePipe } from '@angular/common';
/**
* Date and time picker / selector.
* @author Ben Hayward
*/
@Component({
moduleId: module.id,
selector: 'm-date-selector',
template: `
<label class="m-date-selector--label" *ngIf="label">{{ label }}</label>
<div
class="m-date-selector--input"
mdl-datetime-picker
[date]="date"
(dateChange)="onDateChange($event)"
<label class="m-dateSelector__label" *ngIf="label">{{ label }} </label>
<input
class="m-dateSelector__input"
[ngClass]="{ 'm-dateSelector__input--hidden': hideInput }"
[owlDateTimeTrigger]="dt"
[owlDateTime]="dt"
[min]="min"
[max]="max"
[ngModel]="date"
(ngModelChange)="onDateChange($event)"
/>
<owl-date-time [pickerType]="calendarType" #dt></owl-date-time>
<m-tooltip
*ngIf="tooltipIcon"
icon="{{ tooltipIcon }}"
[owlDateTimeTrigger]="dt"
i18n-label="{{ i18n }}"
>
<input
type="text"
placeholder="Select a date"
i18n-placeholder="@@COMMON__DATE_SELECTOR__PLACEHOLDER"
[ngModel]="date | date: dateFormat"
(ngModelChange)="onDateChange($event)"
/>
<i class="material-icons">keyboard_arrow_down</i>
</div>
{{ tooltipText }}
</m-tooltip>
`,
providers: [DatePipe],
})
export class DateSelectorComponent {
@Input() label: string;
@Input() date: string;
@Output() dateChange: EventEmitter<any> = new EventEmitter<any>();
@Input() dateFormat: string = 'short';
@Input() dateFormat: string = 'short'; // legacy. TODO: implement localization.
@Input() label: string; // label for input.
onDateChange(newDate) {
@Input() hideInput = false; // text input showing the date.
@Input() calendarType = 'calendar'; // timer/calendar/both.
@Input() i18n?: string; // i18n string to accompany tooltip text.
@Input() tooltipIcon?: string; // tooltip icon.
@Input() tooltipText?: string; // tooltip text.
protected _date: any;
@Input('date') // parse input into Date object.
set date(value: number) {
// If ms not included in timestamp, multiply..
if (value && value.toString().length <= 10) {
value = value * 1000;
}
this._date = new Date(value ? value : Date.now());
}
get date() {
return this._date;
}
protected _min: Date;
@Input('min') // parse input into Date object.
set min(value) {
this._min = new Date(value);
}
get min(): any {
return this._min;
}
protected _max: Date;
@Input('max') // parse input into Date object.
set max(value) {
this._max = new Date(value);
}
get max(): any {
return this._max;
}
/**
* Called when date changes.
* @param newDate - the new date.
*/
public onDateChange(newDate: number): void {
this.dateChange.emit(newDate);
}
}
......@@ -8,6 +8,9 @@ import {
import { EmailConfirmationService } from './email-confirmation.service';
import { Session } from '../../../services/session';
import { Subscription } from 'rxjs';
import { NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
import { filter } from 'rxjs/operators';
/**
* Component that displays an announcement-like banner
......@@ -27,12 +30,15 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
canClose: boolean = false;
protected userEmitter$: Subscription;
protected routerEvent$: Subscription;
protected canCloseTimer: number;
protected minds = window.Minds;
constructor(
protected service: EmailConfirmationService,
protected session: Session,
protected router: Router,
protected location: Location,
protected cd: ChangeDetectorRef
) {}
......@@ -42,7 +48,6 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
this.userEmitter$ = this.session.userEmitter.subscribe(user => {
this.sent = false;
this.setShouldShow(user);
this.detectChanges();
});
......@@ -50,6 +55,13 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
this.canClose = true;
this.detectChanges();
}, 3000);
this.routerEvent$ = this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
this.setShouldShow(this.session.getLoggedInUser());
this.detectChanges();
});
}
ngOnDestroy(): void {
......@@ -58,6 +70,10 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
if (this.userEmitter$) {
this.userEmitter$.unsubscribe();
}
if (this.routerEvent$) {
this.routerEvent$.unsubscribe();
}
}
/**
......@@ -66,6 +82,7 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
*/
setShouldShow(user): void {
this.shouldShow =
!(this.location.path().indexOf('/onboarding') === 0) &&
!this.minds.from_email_confirmation &&
user &&
user.email_confirmed === false;
......
......@@ -3,16 +3,16 @@
<ul class="m-footer-nav m-footer-nav-inline">
<li
*ngFor="let page of navigation.getItems('footer')"
class="m-footer-nav-item "
class="m-footer-nav-item"
>
<a
*ngIf="page.path && page.path.indexOf('p/') === 0"
*ngIf="pagesService.isInternalLink(page.path)"
[routerLink]="['/' + page.path]"
class="mdl-color-text--blue-grey-100"
>{{ page.title }}</a
>
<a
*ngIf="page.path.indexOf('p/') !== 0"
*ngIf="!pagesService.isInternalLink(page.path)"
[href]="page.path"
target="_blank"
class="mdl-color-text--blue-grey-100"
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
import { PagesService } from '../../services/pages.service';
import { MockComponent, MockService } from '../../../utils/mock';
import { Navigation as NavigationService } from '../../../services/navigation';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
FooterComponent,
MockComponent({
selector: 'a',
inputs: ['routerLink'],
}),
],
providers: [
{
provide: NavigationService,
useValue: MockService(NavigationService),
},
{ provide: PagesService, useValue: MockService(PagesService) },
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
......@@ -2,11 +2,15 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Navigation as NavigationService } from '../../../services/navigation';
import { PagesService } from '../../services/pages.service';
@Component({
selector: 'm-footer',
templateUrl: 'footer.component.html',
})
export class FooterComponent {
constructor(public navigation: NavigationService) {}
constructor(
public navigation: NavigationService,
public pagesService: PagesService
) {}
}
......@@ -35,24 +35,14 @@
<div *ngSwitchCase="'date'">
<div>
<label class="m-date-selector--label" *ngIf="field.label"
>{{ field.label }}:</label
>
<div
class="m-date-selector--input"
mdl-datetime-picker
<m-date-selector
[date]="form.get(field.key).value"
(dateChange)="form.get(field.key).setValue($event)"
>
<input
type="text"
placeholder="Select a date"
i18n-placeholder="@@COMMON__DATE_SELECTOR__PLACEHOLDER"
[value]="form.get(field.key).value | date"
place
/>
<i class="material-icons">keyboard_arrow_down</i>
</div>
[calendarType]="'both'"
[tooltipIcon]="'keyboard_arrow_down'"
label="{{ field.label }}"
dateFormat="short"
></m-date-selector>
</div>
</div>
......
@import '../../../foundation/grid-values';
.m-marketing__asFeaturedIn {
min-width: 320px;
max-width: 1084px;
margin: 45px auto 80px;
......
......@@ -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>
......
......@@ -2,7 +2,7 @@
m-marketing__footer {
display: block;
margin: 105px 0 95px;
margin: 105px 0 20px 0;
@media screen and (max-width: $m-grid-min-vp) {
margin: 80px 0 0;
......@@ -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,7 +61,7 @@ m-marketing__footer {
width: 60%;
margin: 0 auto;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
width: 60%;
grid-row: 999;
margin: 32px 0 0;
......@@ -75,7 +83,12 @@ m-marketing__footer {
}
&.m-marketingFooter__sloganText {
font-weight: 400;
margin: 0 0 21px;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
......
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);
}
}
......@@ -40,12 +40,17 @@
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (min-width: $m-grid-max-tablet) and (max-width: $m-grid-min-vp) {
font-size: 32px;
line-height: 34px;
line-height: 43px;
margin: 0 0 18px;
}
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
font-size: 38px;
line-height: 39px;
}
&.m-marketing__subtitle--asTitle {
font-size: 48px;
line-height: 53px;
......@@ -87,6 +92,16 @@
font-size: 18px;
line-height: 27px;
margin: 0 0 36px;
@media screen and(min-width: 700px) and (max-width: $m-grid-min-vp) {
font-size: 16px;
line-height: 24px;
}
@media screen and(min-width: $m-grid-max-mobile) and (max-width: 700px) {
font-size: 15px;
line-height: 22px;
}
}
ul.m-marketing__points {
......@@ -119,6 +134,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 +191,114 @@
}
}
}
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-max-tablet) {
width: 730px;
}
@media screen and(max-width: $m-grid-max-tablet) {
width: auto;
}
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%
);
}
}
......@@ -18,7 +18,12 @@
.m-marketing__body {
position: relative;
@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) {
padding: 0 60px 30px;
margin-left: 30px;
}
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0 30px 30px;
}
......@@ -33,6 +38,10 @@
clip-path: polygon(0% 0%, 0% 100%, 100% 92%, 100% 0%);
z-index: -1;
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
transform: translate(0, -56px);
}
@include m-theme() {
background: linear-gradient(
180deg,
......@@ -41,7 +50,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 +63,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 +74,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;
......@@ -74,9 +83,31 @@
}
}
ul.m-marketing__points {
li {
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #b7b7b7;
}
}
}
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 +117,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 +141,11 @@
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(min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
width: 338px;
}
@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 +171,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 +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;
}
......
......@@ -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;
}
}
......@@ -22,10 +22,10 @@
.m-marketing__body {
position: relative;
margin: auto 0;
padding: 0;
padding: 0 60px;
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,7 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
padding: 0;
@include m-theme() {
color: themed($m-grey-300);
......@@ -82,7 +82,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;
......@@ -120,13 +120,22 @@
grid-column-start: 1;
grid-row: 1;
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
margin-left: 60px;
}
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 518px;
clip-path: polygon(0% 1%, 0% 100%, 100% 96%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@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 +151,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 +166,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 +191,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;
}
......
......@@ -33,11 +33,19 @@
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
font-size: 40px !important;
line-height: 39px !important;
text-align: left !important;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
}
@media screen and(max-width: $m-grid-max-mobile) {
text-align: center;
}
}
......@@ -45,8 +53,37 @@
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;
}
@media screen and (max-width: $m-grid-max-mobile) {
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 +96,7 @@
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
line-height: 27px;
}
}
......@@ -91,7 +127,11 @@
position: relative;
z-index: 0;
@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) {
margin-right: 60px;
}
@media screen and (max-width: $m-grid-max-mobile) {
grid-row: 1;
}
......@@ -101,7 +141,12 @@
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 +161,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 +178,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();
......
<div
class="m-poster-date-selector__input"
<m-date-selector
[date]="date"
[hideInput]="true"
[tooltipIcon]="'date_range'"
[tooltipText]="'Schedule Post'"
[i18n]="'@@SCHEDULE__POST__DATE__SELECTOR__TOOLTIP'"
[hideInput]="true"
[calendarType]="'both'"
[class.selected]="hasDateSelected()"
mdl-datetime-picker
[date]="getDate()"
(dateChange)="onDateChange($event)"
>
<input
type="text"
[ngModel]="date | date: dateFormat"
(ngModelChange)="onDateChange($event)"
[hidden]="true"
/>
<m-tooltip icon="date_range">
{{ getDate() || 'Post Immediately' }}
</m-tooltip>
<span></span>
</div>
></m-date-selector>
......@@ -6,7 +6,6 @@ import {
tick,
} from '@angular/core/testing';
import { PosterDateSelectorComponent } from './selector.component';
import { MaterialDateTimePickerDirective } from '../../directives/material/datetimepicker.directive';
import { FormsModule } from '@angular/forms';
import { MockComponent } from '../../../utils/mock';
......@@ -18,11 +17,17 @@ describe('PosterDateSelectorComponent', () => {
TestBed.configureTestingModule({
declarations: [
PosterDateSelectorComponent,
MaterialDateTimePickerDirective,
MockComponent({
selector: 'm-tooltip',
selector: 'm-date-selector',
template: '<ng-content></ng-content>',
inputs: ['icon'],
inputs: [
'date',
'hideInput',
'tooltipIcon',
'tooltipText',
'i18n',
'calendarType',
],
}),
],
imports: [FormsModule],
......
......@@ -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: [
......
......@@ -6,6 +6,17 @@ m-toggle {
margin: 0 0.35em;
cursor: pointer;
&.disabled {
.m-toggle__switch {
.m-toggle__switch--left,
.m-toggle__switch--right {
@include m-theme() {
background: themed($m-grey-800);
}
}
}
}
.m-toggle__track {
display: inline-block;
position: absolute;
......
......@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostBinding,
HostListener,
Input,
Output,
......@@ -19,11 +20,16 @@ export class ToggleComponent {
@Input('mModel') mModel: any;
@Input('disabled') @HostBinding('class.disabled') disabled: boolean = false;
@Output('mModelChange') mModelChange: EventEmitter<any> = new EventEmitter<
any
>();
@HostListener('click') toggle() {
if (this.disabled) {
return;
}
if (this.mModel === this.leftValue) {
this.mModelChange.emit(this.rightValue);
} else {
......
......@@ -4,7 +4,6 @@ import { Material as MaterialService } from '../../services/ui';
import { MaterialTextfield } from './material/text-field';
import { MaterialUpload } from './material/upload';
import { MaterialSwitch } from './material/switch';
import { MaterialDateTimePickerDirective } from './material/datetimepicker.directive';
import { MaterialSlider } from './material/slider';
import { MaterialRadio } from './material/radio/radio';
......@@ -30,7 +29,6 @@ export const MDL_DIRECTIVES: any[] = [
MaterialTextfield,
MaterialUpload,
MaterialSwitch,
MaterialDateTimePickerDirective,
MaterialSlider,
MaterialRadio,
];
import {
Directive,
EventEmitter,
HostListener,
Input,
Output,
} from '@angular/core';
import { DatePipe } from '@angular/common';
import { default as DateTimePicker } from 'material-datetime-picker';
@Directive({
selector: '[mdl-datetime-picker]',
providers: [DatePipe],
})
export class MaterialDateTimePickerDirective {
@Input() date;
@Output() dateChange: EventEmitter<any> = new EventEmitter<any>();
private open: boolean = false;
private picker;
constructor(public datePipe: DatePipe) {}
@HostListener('click')
onHostClick() {
if (!this.open) {
let options = {};
if (this.date) {
options = { default: new Date(this.date).toString() };
}
this.picker = new DateTimePicker(options)
.on('submit', this.submitCallback.bind(this))
.on('close', this.close.bind(this));
this.open = true;
this.picker.open();
}
}
private submitCallback(value) {
this.dateChange.emit(this.datePipe.transform(value.format(), 'short'));
this.close();
}
private close() {
this.picker.off('submit', this.submitCallback);
this.picker.off('close', this.close);
this.open = false;
}
}
......@@ -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);
}
}
......@@ -36,7 +36,12 @@
</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">
......@@ -78,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()">
......@@ -93,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 {
......@@ -276,6 +313,35 @@
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);
}
}
......@@ -11,6 +11,7 @@ type EntityObservables = Map<string, EntityObservable>;
export class EntitiesService {
entities: EntityObservables = new Map<string, EntityObservable>();
castToActivites: boolean = false;
exportUserCounts: boolean = false;
constructor(
protected client: Client,
......@@ -110,6 +111,16 @@ export class EntitiesService {
return this;
}
/**
* Cast to activities or not
* @param cast boolean
* @return EntitiesService
*/
setExportUserCounts(value: boolean): EntitiesService {
this.exportUserCounts = value;
return this;
}
/**
* Fetch entities
* @param urns string[]
......@@ -120,6 +131,7 @@ export class EntitiesService {
const response: any = await this.client.get('api/v2/entities/', {
urns,
as_activities: this.castToActivites ? 1 : 0,
export_user_counts: this.exportUserCounts,
});
if (!response.entities.length) {
......
......@@ -24,6 +24,7 @@ export class FeedsService {
endpoint: string = '';
params: any = { sync: 1 };
castToActivities: boolean = false;
exportUserCounts: boolean = false;
rawFeed: BehaviorSubject<Object[]> = new BehaviorSubject([]);
feed: Observable<BehaviorSubject<Object>[]>;
......@@ -50,6 +51,7 @@ export class FeedsService {
switchMap(feed =>
this.entitiesService
.setCastToActivities(this.castToActivities)
.setExportUserCounts(this.exportUserCounts)
.getFromFeed(feed)
),
tap(feed => {
......@@ -137,6 +139,15 @@ export class FeedsService {
return this;
}
/**
* Sets exportUserCounts
* @param { boolean } export - whether or not to export user's subscribers_count and subscriptions_count.
*/
setExportUserCounts(value: boolean): FeedsService {
this.exportUserCounts = value;
return this;
}
/**
* Fetches the data.
*/
......@@ -150,6 +161,7 @@ export class FeedsService {
...{
limit: 150, // Over 12 scrolls
as_activities: this.castToActivities ? 1 : 0,
export_user_counts: this.exportUserCounts ? 1 : 0,
from_timestamp: this.pagingToken,
},
})
......
import { Injectable } from '@angular/core';
import { Client } from '../../services/api/client';
import { EntitiesService } from './entities.service';
export type HorizontalFeedContext = 'container';
/**
* This service allow retrieving entities to navigate through a horizontal feed whose entities will be loaded
* one by one in specialized components.
*
* @todo: RxJS cursor-like get()
* @todo: Support other kind of context
*/
@Injectable()
export class HorizontalFeedService {
constructor(protected client: Client, protected entities: EntitiesService) {}
/**
* Get the previous and next entity based on an ID and a context.
*
* Contexts:
* - container: Fetch adjacent entities from a container (user/group) sorted chronologically.
*
* @param context
* @param entity
* @return {Promise}
*/
async get(
context: HorizontalFeedContext,
entity: any
): Promise<{ prev; next }> {
if (!entity) {
return {
prev: void 0,
next: void 0,
};
}
switch (context) {
case 'container':
const endpoint = `api/v2/feeds/container/${entity.container_guid ||
entity.owner_guid}/all`;
const params = {
sync: 1,
as_activities: 1,
force_public: 1,
limit: 1,
};
return {
prev: await this.fetch(endpoint, {
...params,
reverse_sort: 1,
from_timestamp: entity.time_created * 1000 + 1,
}),
next: await this.fetch(endpoint, {
...params,
from_timestamp: entity.time_created * 1000 - 1,
}),
};
default:
throw new Error('Unknown Horizontal Feed context');
}
}
/**
* Fetch an the first FeedSyncEntity from a /feed endpoint and hydrates it, if necessary.
* @param endpoint
* @param params
* @return {Promise}
*/
async fetch(endpoint: string, params: any): Promise<any> {
const response = (await this.client.get(endpoint, params, {
cache: true,
})) as any;
if (!response || !response.entities || !response.entities.length) {
return void 0;
}
const feedSyncEntity = response.entities[0];
if (feedSyncEntity.entity) {
return feedSyncEntity.entity;
}
return this.entities
.setCastToActivities(true)
.single(feedSyncEntity.urn)
.toPromise();
}
}
import { TestBed } from '@angular/core/testing';
import { PagesService } from './pages.service';
describe('PagesService', () => {
let service: PagesService;
beforeEach(() => {
jasmine.clock().uninstall();
jasmine.clock().install();
TestBed.configureTestingModule({
providers: [],
});
service = new PagesService();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should be instantiated', () => {
expect(service).toBeTruthy();
});
it('should discern the difference between internal and external URLs', () => {
expect(
service.isInternalLink(
'itunes.apple.com/us/app/minds-com/id961771928%3Fmt%3D8'
)
).toBeFalsy();
// testing for RegExp collision issues.
expect(
!service.isInternalLink(
'itunes.apple.com/us/app/minds-com/id961771928%3Fmt%3D8'
)
).toBeTruthy();
});
it('should still find external links starting https:// or http://', () => {
expect(
service.isInternalLink(
'http://itunes.apple.com/us/app/minds-com/id961771928%3Fmt%3D8'
)
).toBeFalsy();
expect(
!service.isInternalLink(
'http://itunes.apple.com/us/app/minds-com/id961771928%3Fmt%3D8'
)
).toBeTruthy();
expect(
service.isInternalLink(
'https://itunes.apple.com/us/app/minds-com/id961771928%3Fmt%3D8'
)
).toBeFalsy();
expect(
!service.isInternalLink(
'https://itunes.apple.com/us/app/minds-com/id961771928%3Fmt%3D8'
)
).toBeTruthy();
});
it('should discern a URL is not internal', () => {
expect(
service.isInternalLink('https://www.testlinkdonotclick.com/test/')
).toBeFalsy();
expect(
!service.isInternalLink('https://www.testlinkdonotclick.com/test/')
).toBeTruthy();
});
it('should discern an internal URL', () => {
expect(service.isInternalLink('p/terms-of-service')).toBeTruthy();
expect(service.isInternalLink('p/terms-of-service')).not.toBeFalsy();
});
it('should not flag a URL as internal when p/ not in first position', () => {
expect(service.isInternalLink('app/terms-of-service')).toBeFalsy();
expect(service.isInternalLink('app/terms-of-service')).not.toBeTruthy();
});
});
import { Injectable } from '@angular/core';
/**
* Shared logic between internal 'p/' page loading components.
* e.g. header, footer.
*
* @author Ben Hayward
*/
@Injectable()
export class PagesService {
private internalPageRegex: RegExp = /^\/?p\/./; // matches 'p/' in first and second position.
constructor() {}
/**
* Determines whether or not the link given is an internal page or external link.
* @param { string } path - the path to be checked.
*
* @returns {boolean } true if regex matches that of an internal page 'p/[name]'.
*/
public isInternalLink(path: string): boolean {
return new RegExp(this.internalPageRegex).test(path);
}
}
......@@ -21,6 +21,10 @@ export class SiteService {
return this.isProDomain ? this.pro.one_line_headline || '' : '';
}
get baseUrl(): string {
return window.Minds.site_url; // TODO: use SSR once merged in
}
private router$: Subscription;
constructor(private router: Router) {
......
......@@ -7,14 +7,14 @@
<ul class="m-content-sidebar-nav">
<li *ngFor="let page of pages" class="m-content-sidebar-nav-item">
<a
*ngIf="page.path && page.path.indexOf('p/') === 0"
*ngIf="pagesService.isInternalLink(page.path)"
[routerLink]="['/' + page.path]"
[ngClass]="{'is-active': path == page.path }"
>
{{page.title}}
</a>
<a
*ngIf="page.path.indexOf('p/') !== 0"
*ngIf="!pagesService.isInternalLink(page.path)"
[href]="page.path"
target="_blank"
>
......
......@@ -6,6 +6,7 @@ import { Subscription } from 'rxjs';
import { Client } from '../../services/api';
import { MindsTitle } from '../../services/ux/title';
import { Navigation as NavigationService } from '../../services/navigation';
import { PagesService } from '../../common/services/pages.service';
@Component({
moduleId: module.id,
......@@ -29,7 +30,8 @@ export class Pages {
public titleService: MindsTitle,
public client: Client,
public navigation: NavigationService,
public route: ActivatedRoute
public route: ActivatedRoute,
public pagesService: PagesService
) {}
ngOnInit() {
......
@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;
}
}
}
}
export default function isSafari() {
export function isSafari() {
return !!(
(<any>window).safari ||
/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
);
}
export function iOSVersion() {
if (/iP(hone|od|ad)/.test(navigator.platform)) {
// supports iOS 2.0 and later: <http://bit.ly/TJjs1V>
const v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || '0', 10)];
} else {
return null;
}
}
......@@ -29,6 +29,7 @@ export interface MindsBlogEntity {
access_id?: number;
license?: string;
allow_comments: boolean;
perma_url?: string;
}
export interface Message {}
......
export let pagesServiceMock = new (function() {
this.isInternalLink = () => {
return true;
};
})();
<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-8 m-grid__column__skip-3 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,242 @@
m-login {
display: block;
@include m-theme() {
background-color: themed($m-white);
}
}
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
position: relative;
.m-login {
margin: 100px auto;
display: flex;
max-width: 990px;
flex-direction: row;
min-height: 100%;
&:not(.m-login__newDesign) {
@include m-theme() {
background-color: themed($m-white);
}
@media screen and (max-width: $max-mobile) {
flex-direction: column;
}
.m-login {
margin: 100px auto;
display: flex;
max-width: 990px;
flex-direction: row;
min-height: 100%;
> div {
margin: 16px;
flex: 1;
@media screen and (max-width: $max-mobile) {
flex-direction: column;
}
> 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;
&.m-login__iosFallback {
.m-grid {
.m-login__wrapper {
filter: none;
box-shadow: -1px 0px 8px rgba(50, 50, 0, 0.5);
minds-form-login {
clip-path: none;
}
}
}
}
&::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 0 50px;
padding: 0 26px;
&::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 {
position: relative;
padding: 15vh 0 0;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 10vh 0 0;
}
& > * {
margin: 0 auto;
}
}
.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;
}
a {
@include m-theme() {
color: themed($m-blue);
}
&.m-login__joinTheConversation {
font-size: 14px;
line-height: 20px;
font-weight: 500;
}
}
.mdl-card__actions {
flex-direction: column;
align-items: flex-start;
margin-top: 55px;
padding: 0;
& > *:not(button) {
color: #4a4a4a !important;
}
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,26 @@ 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';
import { iOSVersion } from '../../helpers/is-safari';
@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;
@HostBinding('class.m-login__iosFallback')
iosFallback: boolean = false;
flags = {
canPlayInlineVideos: true,
......@@ -29,6 +35,8 @@ export class LoginComponent {
paramsSubscription: Subscription;
private redirectTo: string;
constructor(
public client: Client,
public router: Router,
......@@ -36,8 +44,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 +67,34 @@ 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);
this.iosFallback = iOSVersion() !== null;
}
}
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-8 m-grid__column__skip-3 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="pagesService.isInternalLink(page.path)"
[routerLink]="[page.path]"
>{{ page.title }}</a
>
<a
*ngIf="!pagesService.isInternalLink(page.path)"
[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,12 @@ 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 { PagesService } from '../../common/services/pages.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';
import { iOSVersion } from '../../helpers/is-safari';
@Component({
selector: 'm-register',
......@@ -22,6 +28,12 @@ export class RegisterComponent implements OnInit, OnDestroy {
inProgress: boolean = false;
videoError: boolean = false;
referrer: string;
@HostBinding('class.m-register__newDesign')
newDesign: boolean = false;
@HostBinding('class.m-register__iosFallback')
iosFallback: boolean = false;
private redirectTo: string;
flags = {
canPlayInlineVideos: true,
......@@ -37,15 +49,33 @@ export class RegisterComponent implements OnInit, OnDestroy {
private loginReferrer: LoginReferrerService,
public session: Session,
private onboarding: OnboardingService,
public navigation: NavigationService
public navigation: NavigationService,
public pagesService: PagesService,
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);
this.iosFallback = iOSVersion() !== null;
}
}
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 +83,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 +114,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);
}
}
......@@ -152,6 +152,8 @@
<m-comments__tree [entity]="blog"> </m-comments__tree>
</div>
<m-social-icons [url]="blog.perma_url" [title]="blog.title"> </m-social-icons>
<m-modal-confirm
*ngIf="deleteToggle"
[open]="true"
......
......@@ -14,6 +14,10 @@
}
}
.m-blog--footer {
margin-bottom: 50px;
}
.m-share-buttons {
cursor: pointer;
padding-right: 8px;
......
<div class="m-channel--explicit-overlay--container" *ngIf="!hidden">
<div class="m-channel--explicit-overlay--content">
<h3>
This channel contains mature content
This channel contains content that is NSFW
</h3>
<div
class="m-btn m-btn--slim m-btn--action m-channel--explicit-overlay--action"
......
......@@ -156,6 +156,7 @@ describe('ChannelSidebar', () => {
featuresServiceMock.mock('permissions', true);
featuresServiceMock.mock('pro', true);
featuresServiceMock.mock('purchase-pro', true);
featuresServiceMock.mock('onboarding-december-2019', true);
clientMock.response = {};
uploadMock.response = {};
comp = fixture.componentInstance;
......
import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
import {
Component,
EventEmitter,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { Client, Upload } from '../../../services/api';
import { Session } from '../../../services/session';
import { MindsUser } from '../../../interfaces/entities';
......@@ -15,7 +21,7 @@ import { FeaturesService } from '../../../services/features.service';
inputs: ['user', 'editing'],
templateUrl: 'sidebar.html',
})
export class ChannelSidebar {
export class ChannelSidebar implements OnInit {
minds = window.Minds;
filter: any = 'feed';
isLocked: boolean = false;
......@@ -65,6 +71,7 @@ export class ChannelSidebar {
shouldShowOnboardingProgress() {
return (
!this.featuresService.has('onboarding-december-2019') &&
this.session.isLoggedIn() &&
this.session.getLoggedInUser().guid === this.user.guid &&
!this.storage.get('onboarding_hide') &&
......
......@@ -27,10 +27,21 @@
></m-channel--badges>
</a>
</div>
<a
class="m-mature-text-toggle"
(click)="toggleMatureVisibility()"
*ngIf="comment.mature"
[ngClass]="{ 'm-redButton': showMature }"
>
<i class="material-icons m-material-icons-inline">explicit</i>
</a>
<p
[hidden]="editing"
class="m-commentBubble__message"
class="m-commentBubble__message m-mature-message-content"
[innerHtml]="comment.description || '' | tags"
[ngClass]="{ 'm-mature-text': comment.mature && !showMature }"
(click)="!showMature ? toggleMatureVisibility() : null"
></p>
<p class="m-comment--error-label" *ngIf="error">
......
......@@ -22,6 +22,13 @@
@include m-theme() {
background-color: rgba(themed($m-grey-50), 0.7);
}
.m-mature-text-toggle {
i {
vertical-align: middle;
padding-right: 8px;
}
}
}
.m-commentBubble__username {
......@@ -261,3 +268,9 @@
white-space: nowrap;
}
}
.m-redButton {
@include m-theme() {
color: themed($m-red);
}
}
......@@ -64,6 +64,7 @@ export class CommentComponentV2
error: string = '';
@Input() showReplies: boolean = false;
changesDetected: boolean = false;
showMature: boolean = false;
_delete: EventEmitter<any> = new EventEmitter();
_saved: EventEmitter<any> = new EventEmitter();
......@@ -112,6 +113,10 @@ export class CommentComponentV2
return (this.comment.time_created - secondsElapsed * 0.01) * 1000;
})
);
if (this.minds.user.guid === this.comment.ownerObj.guid) {
this.showMature = true;
}
}
ngAfterViewInit() {
......@@ -384,4 +389,11 @@ export class CommentComponentV2
)
.present();
}
/**
* Toggles mature visibility.
*/
toggleMatureVisibility() {
this.showMature = !this.showMature;
}
}
......@@ -444,4 +444,5 @@ m-comments__tree,
.m-comment--poster .minds-body {
overflow: visible;
min-height: 50px;
max-width: calc(100% - 60px);
}
......@@ -113,6 +113,29 @@
</div>
</div>
<span
*ngIf="sockets.error$ | async"
class="m-commentsThread__connectionLost"
i18n="@@MINDS__COMMENTS__SOCKET_DISCONNECTED"
>
Could not load live comments<br />
<a
*ngIf="!inProgress"
(click)="retry()"
class="m-commentsThread__connectionLost--retry"
i18n="@@MINDS__COMMENTS__SOCKET_RETRY"
>
Click to retry
</a>
<span
[hidden]="!inProgress"
class="m-commentsThread__connectionLost--loading"
i18n="@@MINDS__COMMENTS__SOCKET_LOADING"
>
Loading...
</span>
</span>
<m-comment__poster
*ngIf="activityService.allowComment$ | async"
[guid]="guid"
......
......@@ -8,3 +8,16 @@
opacity: 0.75;
text-align: center;
}
.m-commentsThread__connectionLost {
display: block;
text-align: center;
padding: 4px 8px;
margin: 4px 0;
font-size: 9pt;
cursor: pointer;
@include m-theme() {
color: themed($m-blue-grey-500);
}
}
This diff is collapsed.
......@@ -109,17 +109,24 @@ export class CommentsThreadComponent implements OnInit {
let el = this.scrollView.nativeElement;
const previousScrollHeightMinusTop = el.scrollHeight - el.scrollTop;
let response: any = <
{ comments; 'load-next'; 'load-previous'; socketRoomName }
>await this.commentsService.get({
entity_guid: this.guid,
parent_path,
level: this.level,
limit: 12,
loadNext: descending ? null : this.loadNext,
loadPrevious: descending ? this.loadPrevious : null,
descending,
});
let response: any = null;
try {
response = <{ comments; 'load-next'; 'load-previous'; socketRoomName }>(
await this.commentsService.get({
entity_guid: this.guid,
parent_path,
level: this.level,
limit: 12,
loadNext: descending ? null : this.loadNext,
loadPrevious: descending ? this.loadPrevious : null,
descending,
})
);
} catch (e) {}
if (!response || !response.comments) {
return;
}
let comments = response.comments;
......@@ -287,6 +294,17 @@ export class CommentsThreadComponent implements OnInit {
}
}
/**
* Retries connection to sockets manually.
*/
retry() {
this.inProgress = true;
this.listen();
setTimeout(() => {
this.inProgress = false;
}, 2000);
}
onOptimisticPost(comment) {
this.comments.push(comment);
this.detectChanges();
......@@ -320,6 +338,15 @@ export class CommentsThreadComponent implements OnInit {
}
ngOnChanges(changes) {
// console.log('[comment:list]: on changes', changes);
// console.log('[comment:thread]: on changes', changes);
// reload on entity change.
if (
changes.entity &&
changes.entity.previousValue &&
changes.entity.previousValue.guid !== changes.entity.currentValue.guid
) {
this.load(true);
}
}
}
......@@ -13,6 +13,7 @@ import { OnboardingCategoriesSelector } from './categories-selector/categories-s
import { Tutorial } from './tutorial/tutorial';
import { CaptchaModule } from '../captcha/captcha.module';
import { ExperimentsModule } from '../experiments/experiments.module';
import { PopoverComponent } from './popover-validation/popover.component';
@NgModule({
imports: [
......@@ -31,6 +32,7 @@ import { ExperimentsModule } from '../experiments/experiments.module';
OnboardingForm,
OnboardingCategoriesSelector,
Tutorial,
PopoverComponent,
],
exports: [
LoginForm,
......
This diff is collapsed.
@import 'defaults';
m-login {
.m-login__title {
display: flex;
align-items: center;
justify-content: space-between;
margin-right: 16px;
a {
@include m-theme() {
color: themed($m-blue);
}
}
@media screen and(max-width: 680px) {
flex-direction: column;
align-items: flex-start;
margin-right: 0;
margin-bottom: 24px;
h3 {
margin-bottom: 0;
}
}
}
.m-login__error,
.m-login__formError {
font-size: 14px;
line-height: 19px;
margin-top: 3px;
@include m-theme() {
color: themed($m-red) !important;
}
}
.m-login__error {
text-align: right;
}
.m-login__spacer {
flex-grow: 1;
}
}
.m-login-box {
margin-top: $minds-margin * 2;
width: 100% !important;
......
This diff is collapsed.
<div class="m-popover__wrapper">
<ng-content></ng-content>
<div class="m-popover__content" #content>
<ul class="m-popover__rules">
<li [class.m-popover__rule--checked]="lengthCheck" i18n>
8 or more characters
</li>
<li [class.m-popover__rule--checked]="specialCharCheck" i18n>
At least 1 special character
</li>
<li [class.m-popover__rule--checked]="mixedCaseCheck" i18n>
Mixed case
</li>
<li [class.m-popover__rule--checked]="numbersCheck" i18n>
At least 1 number
</li>
<li [class.m-popover__rule--checked]="spacesCheck" i18n>
Doesn't have spaces
</li>
</ul>
</div>
</div>
This diff is collapsed.
This diff is collapsed.
......@@ -37,7 +37,8 @@ export class GroupsCreator {
public session: Session,
public service: GroupsService,
public router: Router,
public title: MindsTitle
public title: MindsTitle,
private groupsService: GroupsService
) {
this.title.setTitle('Create Group');
}
......@@ -101,6 +102,7 @@ export class GroupsCreator {
}
)
.then(() => {
this.groupsService.updateMembership(true, guid);
this.router.navigate(['/groups/profile', guid]);
});
})
......
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.