...
 
Commits (10)
......@@ -41,14 +41,19 @@ context('Groups', () => {
cy.get('.m-groups-save > button').contains('Create').click();
cy.route("POST", "**/api/v1/groups/group/*/banner*").as("postBanner");
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
}).wait('@postBanner').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
// get current groups count of sidebar
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
}).wait('@postBanner').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
//check count changed.
cy.get('.m-groupSidebarMarkers__list').children().should('have.length', size + 1);
});
cy.get('.m-groupInfo__name').contains('test');
cy.get('.m-groupInfo__description').contains('This is a test');
......@@ -73,11 +78,9 @@ context('Groups', () => {
})
it('should be able to toggle conversation and comment on it', () => {
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.get(`m-group--sidebar-markers li:nth-child(${size - 1})`).click();
});
// toggle the conversation
cy.get('.m-groupGrid__right').should('be.visible');
......@@ -98,9 +101,9 @@ context('Groups', () => {
})
it('should post an activity inside the group and record the view when scrolling', () => {
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.get(`m-group--sidebar-markers li:nth-child(${size - 1})`).click();
});
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
......@@ -128,14 +131,18 @@ context('Groups', () => {
});
it('should delete a group', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.get('.m-groupSidebarMarkers__list').children().its('length').then((size) => {
cy.get(`m-group--sidebar-markers li:nth-child(${size - 1})`).click();
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.get('minds-groups-settings-button ul.minds-dropdown-menu > li:nth-child(8)').contains('Delete Group').click();
cy.get('minds-groups-settings-button m-modal .mdl-button--raised').contains('Confirm').click();
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.contains('Delete Group').click();
cy.contains('Confirm').click();
cy.location('pathname').should('eq', '/groups/member');
cy.location('pathname').should('eq', '/groups/member');
cy.get('.m-groupSidebarMarkers__list').children().should('have.length', size - 1);
});
})
})
......@@ -5,36 +5,36 @@ context('Login', () => {
})
it('should login', () => {
cy.get('.m-v2-topbar__Container__LoginWrapper > a').click();
cy.get('.m-v2-topbar__Container__LoginWrapper > a').contains('Login').click();
cy.location('pathname').should('eq', '/login');
// it should have a login form
cy.get('.m-login').should('be.visible');
cy.get('.m-login__wrapper').should('be.visible');
cy.get('minds-form-login .m-login-box .mdl-cell:first-child input').type(Cypress.env().username);
cy.get('minds-form-login .m-login-box .mdl-cell:last-child input').type(Cypress.env().password);
cy.get('minds-form-login .m-btn--login').click();
cy.get('minds-form-login .mf-button--alt').click();
cy.location('pathname')
.should('eq', '/newsfeed/subscriptions');
})
it('should fail to login because of incorrect password', () => {
cy.get('.m-v2-topbar__Container__LoginWrapper > a').click();
cy.get('.m-v2-topbar__Container__LoginWrapper > a').contains('Login').click();
cy.location('pathname').should('eq', '/login');
// it should have a login form
cy.get('.m-login').should('be.visible');
cy.get('.m-login__wrapper').should('be.visible');
cy.get('minds-form-login .m-login-box .mdl-cell:first-child input').type(Cypress.env().username);
cy.get('minds-form-login .m-login-box .mdl-cell:last-child input').type(Cypress.env().password + '1');
cy.get('minds-form-login .m-btn--login').click();
cy.get('minds-form-login .mf-button--alt').click();
cy.wait(500);
......
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');
});
});
......@@ -5,11 +5,6 @@ context('Registration', () => {
const username = generateRandomId();
const password = `${generateRandomId()}0oA!`;
const email = 'test@minds.com';
const noSymbolPass = 'Passw0rd';
const welcomeText = "Welcome to Minds!";
const passwordDontMatch = "Passwords must match.";
const passwordInvalid = " Password must have more than 8 characters. Including uppercase, numbers, special characters (ie. !,#,@), and cannot have spaces. ";
const usernameField = 'minds-form-register #username';
const emailField = 'minds-form-register #email';
......@@ -20,8 +15,8 @@ context('Registration', () => {
beforeEach(() => {
cy.clearCookies();
cy.visit('/login');
cy.location('pathname').should('eq', '/login');
cy.visit('/register');
cy.location('pathname').should('eq', '/register');
cy.server();
cy.route("POST", "**/api/v1/register").as("register");
});
......@@ -39,55 +34,21 @@ context('Registration', () => {
cy.get(usernameField)
.focus()
.type(username);
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password);
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton)
.click()
.wait('@register').then((xhr) => {
expect(xhr.status).to.equal(200);
});
//onboarding modal shown
cy.contains(welcomeText);
});
it('should display an error if password is invalid', () => {
cy.get(usernameField)
.focus()
.type(generateRandomId());
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(noSymbolPass);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(noSymbolPass);
cy.get(checkbox)
.click({force: true});
......@@ -98,37 +59,22 @@ context('Registration', () => {
expect(xhr.status).to.equal(200);
});
cy.scrollTo('top');
cy.contains(passwordInvalid);
cy.wait(500);
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
});
it('should display an error if passwords do not match', () => {
cy.get(usernameField)
.focus()
.type(generateRandomId());
cy.get(emailField)
.focus()
.type(email);
cy.get('minds-form-register #password')
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password + '!');
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton).click();
cy.scrollTo('top');
cy.contains(passwordDontMatch);
cy.get('.m-register__error').contains('Passwords must match');
});
})
import 'cypress-file-upload';
/**
* @author Marcelo, Ben and Brian
* @author Marcelo, Ben and Brian
* @create date 2019-08-09 22:54:02
* @modify date 2019-08-09 22:54:02
* @desc Custom commands for access through cy.[cmd]();
*
*
* For more comprehensive examples of custom
* commands please read more here:
* https://on.cypress.io/custom-commands
*
* -- This is a parent command --
* Cypress.Commands.add('login', (email, password) => { ... })
* -- This is a child command --
* Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
* -- This is a dual command --
* Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
* -- This is will overwrite an existing command --
* Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
*/
......@@ -56,7 +56,7 @@ const defaults = {
const loginForm = {
password: 'minds-form-login .m-login-box .mdl-cell:last-child input',
username: 'minds-form-login .m-login-box .mdl-cell:first-child input',
submit: 'minds-form-login .m-btn--login',
submit: 'minds-form-login .mf-button',
}
const poster = {
......@@ -84,7 +84,7 @@ Cypress.Commands.add('login', (canary = false, username, password) => {
cy.get(loginForm.username).focus().type(username);
cy.get(loginForm.password).focus().type(password);
cy.get(loginForm.submit)
.focus()
.click({force: true})
......@@ -104,21 +104,21 @@ Cypress.Commands.add('logout', () => {
/**
* Register a user, be sure to delete the user following this.
*
*
* ! LOG-OUT PRIOR TO CALLING !
*
*
* @param { string } username - The username. Note that the requested username will NOT be freed up upon deletion
* @param { string } password - The users password.
* @returns void
*/
Cypress.Commands.add('newUser', (username = '', password = '') => {
cy.visit('/login')
cy.visit('/register')
.location('pathname')
.should('eq', `/login`);
.should('eq', `/register`);
cy.server();
cy.route("POST", '**/api/v1/register').as('registerPOST');
cy.get(registerForm.username).focus().type(username);
cy.get(registerForm.email).focus().type(defaults.email);
cy.get(registerForm.password).focus().type(password);
......@@ -137,7 +137,7 @@ Cypress.Commands.add('newUser', (username = '', password = '') => {
//onboarding modal shown.
cy.get(onboarding.welcomeTextContainer)
.contains(onboarding.welcomeText);
//skip onboarding.
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
......@@ -151,14 +151,14 @@ Cypress.Commands.add('preserveCookies', () => {
/**
* Deletes a user. Use carefully on sandbox or you may lose your favorite test account.
*
*
* ! LOG-IN PRIOR TO CALLING !
*
*
* @param { string } username - The username. TODO: when both params provided log the user in too
* @param { string } password - The password.
* @returns void
*/
Cypress.Commands.add('deleteUser', (username, password) => {
Cypress.Commands.add('deleteUser', (username, password) => {
cy.server();
cy.route("POST", '**/api/v2/settings/password/validate').as('validatePost');
cy.route("POST", '**/api/v2/settings/delete').as('deletePOST');
......@@ -170,7 +170,7 @@ Cypress.Commands.add('deleteUser', (username, password) => {
cy.get(settings.deleteAccountButton).click({ force: true });
cy.get('#password').focus().type(password);
cy.get(settings.deleteSubmitButton).click({ force: true })
cy.get(settings.deleteSubmitButton).click({ force: true })
.wait('@validatePost').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
......@@ -193,9 +193,9 @@ Cypress.Commands.add('uploadFile', (selector, fileName, type = '') => {
cy.fixture(fileName).then((content) => {
cy.log("Content", fileName);
cy.get(selector).upload({
fileContent: content,
fileName: fileName,
mimeType: type
fileContent: content,
fileName: fileName,
mimeType: type
});
});
});
......@@ -218,7 +218,7 @@ Cypress.Commands.add('post', (message) => {
/**
* Sets the feature flag cookie.
* @param { Object } flags - JSON object containing flags to turn on
* @param { Object } flags - JSON object containing flags to turn on
* e.g. { dark mode:false, es-feeds: true }
* @returns void
*/
......
......@@ -1980,11 +1980,6 @@
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"@types/video.js": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.3.tgz",
"integrity": "sha512-yAb46+4A0dKFxOQRVLoLyfC/S/BmHLE10MxPXt/t88+7R4GWLHosHelVtYpKBRykjptdkqfQXNRXoQzDeKm6MA=="
},
"@types/webpack-sources": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.5.tgz",
......@@ -12596,6 +12591,13 @@
"requires": {
"moment": "2.10.6",
"rome": "git+https://github.com/joews/rome.git#19f5d3031a922c29c52b9038b2832a827e5e99d6"
},
"dependencies": {
"moment": {
"version": "2.10.6",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.10.6.tgz",
"integrity": "sha1-bLIZZ8ecunsMpeZmRPFzZis++nc="
}
}
},
"material-design-icons": {
......@@ -12968,9 +12970,9 @@
}
},
"moment": {
"version": "2.10.6",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.10.6.tgz",
"integrity": "sha1-bLIZZ8ecunsMpeZmRPFzZis++nc="
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"monotone-convex-hull-2d": {
"version": "1.0.1",
......
......@@ -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,
......
......@@ -127,6 +127,12 @@ import { FormDescriptorComponent } from './components/form-descriptor/form-descr
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { SsoService } from './services/sso.service';
import { PagesService } from './services/pages.service';
<<<<<<< HEAD
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';
=======
>>>>>>> 0eb70a775f10cf0c5b9d8f2b45bde7a237f98372
import { EmailConfirmationComponent } from './components/email-confirmation/email-confirmation.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -252,6 +258,7 @@ const routes: Routes = [
FormToastComponent,
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
],
exports: [
MINDS_PIPES,
......@@ -352,6 +359,7 @@ const routes: Routes = [
FormToastComponent,
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
],
providers: [
SiteService,
......@@ -419,6 +427,14 @@ const routes: Routes = [
useFactory: router => new RouterHistoryService(router),
deps: [Router],
},
{
provide: V2TopbarService,
useFactory: V2TopbarService._,
},
{
provide: SidebarMarkersService,
useFactory: SidebarMarkersService._,
},
],
entryComponents: [
NotificationsToasterComponent,
......
......@@ -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);
}
}
......@@ -8,8 +8,9 @@
<h4 class="m-marketingFooter__sloganText" i18n>
Take back control of your social media
</h4>
<div class="m-marketingFooter__text">&copy; {{ year }} Minds, Inc.</div>
<div class="m-marketingFooter__text" *ngIf="!isMobile">
&copy; {{ year }} Minds, Inc.
</div>
</div>
<div
......@@ -201,6 +202,9 @@
<div
class="m-grid__column-12 m-grid__column-12--mobile m-marketingFooter__column"
>
<div class="m-marketingFooter__text" *ngIf="isMobile">
&copy; {{ year }} Minds, Inc.
</div>
<ul class="m-marketingFooter__inlineList m-marketingFooter__legalLinks">
<li>
<a routerLink="/p/terms" i18n>
......
......@@ -40,6 +40,14 @@ m-marketing__footer {
}
.m-marketingFooter__column {
@media screen and(max-width: $m-grid-max-mobile) {
@for $i from 1 through 5 {
&:nth-child(#{$i}) {
grid-row: $i;
}
}
}
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 32px;
......@@ -53,9 +61,7 @@ m-marketing__footer {
width: 60%;
margin: 0 auto;
@media screen and (max-width: $m-grid-min-vp) {
width: 60%;
grid-row: 999;
@media screen and (max-width: $m-grid-max-mobile) {
margin: 32px 0 0;
}
}
......@@ -75,7 +81,12 @@ m-marketing__footer {
}
&.m-marketingFooter__sloganText {
font-weight: 400;
margin: 0 0 21px;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
......@@ -115,15 +126,6 @@ m-marketing__footer {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
display: inline-block;
margin-right: 1em;
&:last-child {
margin-right: 0;
}
}
a {
color: inherit;
font-weight: normal;
......@@ -149,7 +151,7 @@ m-marketing__footer {
text-align: right;
padding-right: 92px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
text-align: inherit;
padding-right: initial;
}
......
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostListener,
OnInit,
} from '@angular/core';
@Component({
selector: 'm-marketing__footer',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'footer.component.html',
})
export class MarketingFooterComponent {
export class MarketingFooterComponent implements OnInit {
readonly year: number = new Date().getFullYear();
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
isMobile: boolean;
constructor(protected cd: ChangeDetectorRef) {}
ngOnInit() {
this.onResize();
}
@HostListener('window:resize')
onResize() {
this.isMobile = window.innerWidth <= 480;
this.detectChanges();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -3,6 +3,10 @@
m-marketing {
display: block;
font-family: Roboto, sans-serif;
overflow-x: hidden;
min-width: 320px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@include m-theme() {
background: themed($m-white);
......
......@@ -2,23 +2,40 @@ import {
ChangeDetectionStrategy,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { MindsTitle } from '../../../services/ux/title';
import { V2TopbarService } from '../../layout/v2-topbar/v2-topbar.service';
@Component({
selector: 'm-marketing',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'marketing.component.html',
})
export class MarketingComponent implements OnInit {
export class MarketingComponent implements OnInit, OnDestroy {
@Input() pageTitle: string = '';
@Input() showBottombar: boolean = true;
@Input() forceBackground: boolean = true;
constructor(protected title: MindsTitle) {}
constructor(
protected title: MindsTitle,
private topbarService: V2TopbarService
) {}
ngOnInit() {
if (this.pageTitle) {
this.title.setTitle(this.pageTitle);
}
this.topbarService.toggleMarketingPages(
true,
this.showBottombar,
this.forceBackground
);
}
ngOnDestroy() {
this.topbarService.toggleMarketingPages(false);
}
}
......@@ -119,6 +119,13 @@
}
}
a.m-marketing__link {
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
.m-marketing__links {
@media screen and (max-width: $m-grid-min-vp) {
text-align: center;
......@@ -169,4 +176,110 @@
}
}
}
span.m-marketing__imageUX {
span.m-marketing__imageTick {
border-radius: 50%;
background-color: #4fc3a9;
color: white;
width: 63px;
height: 63px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 39px -4px rgba(0, 0, 0, 0.5);
z-index: 3;
@media screen and (max-width: $max-mobile) {
width: 10vw;
height: 10vw;
font-size: 5vw;
}
}
.m-marketing__imageTick--left {
position: absolute;
bottom: 62px;
left: -21px;
@media screen and(min-width: $max-mobile) and (max-width: $m-grid-min-vp) {
bottom: -23%;
left: 8.5%;
}
@media screen and (max-width: $max-mobile) {
//bottom: -116px;
//left: 2px;
bottom: -27%;
left: 2.5%;
}
}
.m-marketing__imageTick--right {
position: absolute;
bottom: 8px;
left: -45px;
@media screen and(min-width: $max-mobile) and (max-width: $m-grid-min-vp) {
bottom: 0;
right: 0;
left: auto;
}
@media screen and (max-width: $max-mobile) {
//bottom: -116px;
//left: 2px;
bottom: -27%;
left: 2.5%;
}
}
img {
box-shadow: 0 0 39px -4px rgba(0, 0, 0, 0.5);
}
}
.m-marketing__quotation {
display: flex;
flex-direction: column;
align-items: flex-end;
margin: 0 auto;
@media screen and(min-width: $m-grid-min-vp) {
width: 730px;
}
h3 {
font-size: 28px;
font-weight: bold;
line-height: 37px;
margin-bottom: 0;
}
h4 {
font-size: 14px;
line-height: 19px;
margin: 0;
@include m-theme() {
color: themed($m-grey-300);
}
}
}
.mf-button.mf-button--alt.mf-button--gradient {
background: #5dbac0; /* Old Browsers */
background: linear-gradient(
45deg,
#4eb69f 0%,
#4fc3aa 49%,
#4eb69f 49%,
#4fc3aa 100%
);
}
}
......@@ -41,7 +41,7 @@
);
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
right: 0;
bottom: -3vw;
transform: none;
......@@ -54,7 +54,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin: 15px 0 15px;
text-align: center;
}
......@@ -65,7 +65,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
......@@ -76,7 +76,18 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@media screen and(min-width: $m-grid-max-tablet) {
padding-right: 200px;
}
@media screen and(min-width: 900px) and (max-width: $m-grid-min-vp) {
padding-right: 100px;
}
@media screen and(max-width: 900px) {
padding-right: 0;
}
@include m-theme() {
color: themed($m-grey-300);
......@@ -86,7 +97,12 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
font-size: 17px;
line-height: 24px;
}
@media screen and (max-width: $m-grid-max-mobile) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
......@@ -105,7 +121,7 @@
height: 547px;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 2%, 0% 97%, 100% 100%, 100% 0%);
......@@ -131,7 +147,7 @@
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......@@ -150,7 +166,7 @@
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......
......@@ -5,7 +5,7 @@
&.m-marketing__section--style-3 {
margin-bottom: 100px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: 80px;
}
......@@ -14,7 +14,7 @@
z-index: 0;
padding: 80px 0 80px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0;
}
}
......@@ -25,7 +25,7 @@
padding: 0;
min-height: 330px;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0 30px 0;
min-height: 0;
}
......@@ -50,7 +50,7 @@
);
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......@@ -61,7 +61,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
font-size: 28px;
line-height: 32px;
margin: 20px 0 17px;
......@@ -72,7 +72,10 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@media screen and(min-width: $min-tablet) {
padding-right: 200px;
}
@include m-theme() {
color: themed($m-grey-300);
......@@ -82,7 +85,7 @@
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
......@@ -126,7 +129,12 @@
height: 518px;
clip-path: polygon(0% 1%, 0% 100%, 100% 96%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
width: 100%;
//height: auto;
}
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 100%, 100% 97%, 100% 0%);
......@@ -142,7 +150,7 @@
bottom: 35px;
transform: translate(15px, 0);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
right: auto;
left: 50%;
bottom: 0;
......@@ -157,7 +165,7 @@
position: relative;
width: 100%;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
......@@ -182,7 +190,7 @@
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
......
......@@ -45,8 +45,34 @@
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@media screen and(min-width: $min-tablet) {
padding-right: 200px;
}
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 30px;
text-align: center;
}
}
ol.m-marketing__description {
padding-left: 16px;
& > li {
margin-bottom: 16px;
font-size: 18px;
line-height: 27px;
}
//@media screen and(min-width: $m-grid-min-vp),
// screen and (max-width: $m-grid-max-mobile) {
// padding-right: 100px;
//}
}
ol.m-marketing__description > li,
p.m-marketing__description {
@include m-theme() {
color: themed($m-grey-300);
}
......@@ -59,8 +85,7 @@
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
line-height: 27px;
}
}
......@@ -91,17 +116,22 @@
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
grid-row: 1;
}
//
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 547px;
clip-path: polygon(0% 1%, 0% 96%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
width: 100%;
//height: auto;
}
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
......@@ -116,8 +146,14 @@
left: 0;
bottom: 35px;
transform: translate(-15px, 0);
z-index: 2;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and(min-width: $m-grid-max-mobile) and (max-width: $m-grid-min-vp) {
width: 100%;
height: auto;
}
@media screen and (max-width: $m-grid-max-mobile) {
right: auto;
left: 50%;
bottom: 0;
......@@ -127,13 +163,13 @@
}
}
span {
span:not(.m-marketing__imageUX):not(.m-marketing__imageTick) {
display: inline-block;
position: relative;
width: 100%;
text-align: right;
@media screen and (max-width: $m-grid-min-vp) {
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
......
@import '../../../../foundation/grid-values';
.m-marketing__main,
.m-marketing__section {
&.m-marketing__section--style-5 {
margin-bottom: 100px;
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: 80px;
}
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 80px 0 80px;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
padding: 95px 0 0;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 0 30px 0;
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-max-mobile) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
@media screen and(min-width: $min-tablet) {
padding-right: 200px;
}
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-max-mobile) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
ul.m-marketing__points {
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
> li em {
font-style: normal;
@include m-theme() {
color: themed($m-black);
}
@include m-on-theme(dark) {
color: #ffffff;
font-weight: bold;
}
}
}
.m-marketing__image {
position: relative;
z-index: 0;
grid-column-start: 1;
grid-row: 1;
img.m-marketing__image--1 {
object-fit: cover;
width: 438px;
height: 518px;
clip-path: polygon(0% 1%, 0% 100%, 100% 96%, 100% 0%);
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
width: 100%;
}
@media screen and (max-width: $m-grid-max-mobile) {
width: 100vw;
height: 100vw;
clip-path: polygon(0% 1%, 0% 100%, 100% 97%, 100% 0%);
}
}
img.m-marketing__image--2 {
object-fit: contain;
width: 358px;
height: 191px;
position: absolute;
right: 0;
bottom: 35px;
transform: translate(15px, 0);
@media screen and (min-width: $m-grid-max-mobile) and (max-width: $m-grid-max-tablet) {
width: 100%;
}
@media screen and (max-width: $m-grid-max-mobile) {
right: auto;
left: 50%;
bottom: 0;
transform: translate(-50%, 50%);
width: 85vw;
height: 45.35vw;
}
}
span {
display: inline-block;
position: relative;
width: 100%;
@media screen and (max-width: $m-grid-max-mobile) {
margin-bottom: calc(
20vw + 40px
); // A little bit less than half UX image + normal margin
&.m-marketing__image--noUxSample {
margin-bottom: 40px;
}
}
// Deco
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 191px;
height: 191px;
transform: translate(-60px, -58px);
background: url('<%= APP_CDN %>/assets/marketing/deco_3.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-max-mobile) {
content: initial;
display: none;
}
}
}
}
}
}
......@@ -23,6 +23,8 @@
<div class="m-phone-input--flag" [ngClass]="country.flagClass"></div>
</div>
<span class="m-phone-input--country-name">{{ country.name }}</span>
<span class="m-phone-input--dial-code">+{{ country.dialCode }}</span>
<span class="m-phone-input--dial-code" [attr.data-minds]="country.dialCode">
+{{ country.dialCode }}
</span>
</li>
</ul>
......@@ -5,6 +5,7 @@ import {
ViewChild,
Output,
EventEmitter,
OnInit,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
......@@ -16,7 +17,7 @@ import { CountryCode } from './countries';
selector: 'm-phone-input--country',
templateUrl: 'country.component.html',
})
export class PhoneInputCountryComponent {
export class PhoneInputCountryComponent implements OnInit {
@Output('country') selectedCountryEvt = new EventEmitter();
countries: Array<Country> = [];
selectedCountry: Country = new Country();
......
......@@ -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 {
......@@ -271,11 +308,41 @@
font-family: 'Roboto', sans-serif;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white);
background-color: themed($m-white);
border: 1px solid themed($m-blue);
color: themed($m-blue);
}
}
> a.m-v2-topbarLoginWrapper__login,
> a.m-v2-topbarLoginWrapper__joinMindsNow {
font-size: 16px;
line-height: 21px;
font-weight: normal;
text-transform: none;
white-space: nowrap;
@include m-theme() {
color: themed($m-grey-800) !important;
}
}
> a.m-v2-topbarLoginWrapper__login {
padding: 0;
border: none !important;
@media screen and(max-width: $max-mobile) {
margin-right: 10px;
}
}
> a.m-v2-topbarLoginWrapper__joinMindsNow {
@include m-theme() {
border: 1px solid themed($m-grey-800) !important;
}
margin-right: 0 !important;
border-radius: 4px;
}
}
.m-v2-topbar__Bottom {
......
......@@ -6,11 +6,16 @@ import {
OnInit,
OnDestroy,
ViewChild,
HostListener,
HostBinding,
} from '@angular/core';
import { Session } from '../../../services/session';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { NotificationsToasterComponent } from '../../../modules/notifications/toaster.component';
import { ThemeService } from '../../../common/services/theme.service';
import { V2TopbarService } from './v2-topbar.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
@Component({
selector: 'm-v2-topbar',
......@@ -21,6 +26,12 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
minds = window.Minds;
timeout;
isTouchScreen = false;
forceBackground: boolean = true;
showBackground: boolean = true;
showSeparateLoginBtns: boolean = false;
marketingPages: boolean = false;
showTopbar: boolean = true;
showBottombar: boolean = true;
@ViewChild(DynamicHostDirective, { static: true })
notificationsToasterHost: DynamicHostDirective;
......@@ -28,16 +39,32 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
componentRef;
componentInstance: NotificationsToasterComponent;
onAuthPages: boolean = false; // sets to false if we're on login or register pages
router$;
constructor(
protected session: Session,
protected cd: ChangeDetectorRef,
private themeService: ThemeService,
protected componentFactoryResolver: ComponentFactoryResolver
protected componentFactoryResolver: ComponentFactoryResolver,
protected topbarService: V2TopbarService,
protected router: Router
) {}
ngOnInit() {
this.loadComponent();
this.session.isLoggedIn(() => this.detectChanges());
this.listen();
this.topbarService.setContainer(this);
}
toggleVisibility(visible: boolean) {
this.showTopbar = visible;
this.showBottombar = visible;
this.detectChanges();
}
getCurrentUser() {
......@@ -56,6 +83,33 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
this.componentInstance = this.componentRef.instance;
}
/**
* Marketing pages set this to true in order to change how the topbar looks
* @param value
* @param showBottombar
*/
toggleMarketingPages(
value: boolean,
showBottombar = true,
forceBackground: boolean = true
) {
this.marketingPages = value;
this.showSeparateLoginBtns = value;
this.showBottombar = value && showBottombar;
this.forceBackground = forceBackground;
this.onScroll();
this.detectChanges();
}
@HostListener('window:scroll')
onScroll() {
this.showBackground = this.forceBackground
? true
: this.marketingPages
? window.document.body.scrollTop > 52
: true;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......@@ -84,4 +138,27 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
clearTimeout(this.timeout);
}
}
private listen() {
this.setOnAuthPages(this.router.url);
this.router$ = this.router.events.subscribe(
(navigationEvent: NavigationEnd) => {
if (navigationEvent instanceof NavigationEnd) {
if (!navigationEvent.urlAfterRedirects) {
return;
}
this.setOnAuthPages(
navigationEvent.urlAfterRedirects || navigationEvent.url
);
}
}
);
}
private setOnAuthPages(url) {
this.onAuthPages = url === '/login' || url === '/register';
this.detectChanges();
}
}
import { V2TopbarComponent } from './v2-topbar.component';
export class V2TopbarService {
private container: V2TopbarComponent;
static _() {
return new V2TopbarService();
}
setContainer(container: V2TopbarComponent) {
this.container = container;
return this;
}
toggleMarketingPages(
value: boolean,
showBottombar: boolean = true,
forceBackground: boolean = true
) {
if (this.container) {
this.container.toggleMarketingPages(
value,
showBottombar,
forceBackground
);
}
}
toggleVisibility(visible: boolean) {
this.container.toggleVisibility(visible);
}
}
@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;
}
}
}
}
<div class="m-login">
<div>
<h3 i18n="@@MINDS__HOME__LOGIN__LOGIN">Login to Minds</h3>
<minds-form-login
(done)="loggedin()"
(doneRegistered)="registered()"
></minds-form-login>
<div class="m-grid" *mIfFeature="'register_pages-december-2019'; else oldLogin">
<div
class="m-grid__column-7 m-grid__column__skip-5 m-grid__column-10--tablet m-grid__column__skip-2--tablet m-grid__column-12--mobile m-grid__column__skip-1--mobile"
>
<div class="m-login__wrapper">
<minds-form-login
[showBigButton]="true"
[showInlineErrors]="true"
[showLabels]="true"
[showTitle]="true"
(done)="loggedin()"
(doneRegistered)="registered()"
>
</minds-form-login>
</div>
</div>
</div>
<ng-template #oldLogin>
<div class="m-login">
<div>
<h3 i18n="@@MINDS__HOME__LOGIN__LOGIN">Login to Minds</h3>
<minds-form-login
(done)="loggedin()"
(doneRegistered)="registered()"
></minds-form-login>
</div>
<div>
<h3 i18n="@@M__COMMON__START_A_CHANNEL_MINDS_REF">
Not on Minds? Start a Minds channel
</h3>
<minds-form-register
[referrer]="referrer"
(done)="registered()"
parentId="/login"
></minds-form-register>
<div>
<h3 i18n="@@M__COMMON__START_A_CHANNEL_MINDS_REF">
Not on Minds? Start a Minds channel
</h3>
<minds-form-register
[referrer]="referrer"
(done)="registered()"
parentId="/login"
></minds-form-register>
</div>
</div>
</div>
</ng-template>
......@@ -2,30 +2,210 @@
m-login {
display: block;
@include m-theme() {
background-color: themed($m-white);
}
}
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
.m-login {
margin: 100px auto;
display: flex;
max-width: 990px;
flex-direction: row;
min-height: 100%;
&:not(.m-login__newDesign) {
.m-login {
margin: 100px auto;
display: flex;
max-width: 990px;
flex-direction: row;
min-height: 100%;
@media screen and (max-width: $max-mobile) {
flex-direction: column;
}
@media screen and (max-width: $max-mobile) {
flex-direction: column;
}
> div {
margin: 16px;
flex: 1;
> div {
margin: 16px;
flex: 1;
}
h3 {
font-weight: 800;
font-size: 18px;
margin: 0 8px;
}
}
}
h3 {
font-weight: 800;
font-size: 18px;
margin: 0 8px;
&.m-login__newDesign {
margin-top: -52px;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 50%;
left: 30%;
clip-path: polygon(55% 0, 100% 0, 100% 11%, 18% 87%);
@include m-theme() {
background: themed($m-amber-medium);
}
//@media screen and (max-width: $max-mobile) {
// right: 0;
// bottom: -3vw;
// clip-path: polygon(83% 0%, 100% 0, 116% 22%, 30% 43%);
//}
}
@media screen and (max-width: $m-grid-max-mobile) {
margin: -52px 26px 50px;
&::before {
position: absolute;
top: 0;
right: 0;
clip-path: polygon(55% 0, 100% 0, 100% 30%, 18% 87%);
background: url(http://localhost/en/assets/marketing/deco_2-straight.svg)
no-repeat;
}
}
.m-grid {
padding: 15vh 0 0;
@media screen and (max-width: $m-grid-max-mobile) {
padding: 10vh 0 0;
}
}
.m-login__wrapper {
display: block;
max-width: 692px;
filter: drop-shadow(-1px 0px 8px rgba(50, 50, 0, 0.5));
}
minds-form-login {
display: block;
background-color: #fcfcfc;
padding: 86px 67px;
clip-path: polygon(0 2%, 100% 0, 100% 97%, 0 95%);
@media screen and (max-width: $m-grid-max-mobile) {
clip-path: polygon(0 2%, 100% 0, 100% 100%, 0 99%);
padding: 55px 26px 47px;
h3,
.m-register__alreadyAUser {
text-align: center;
}
.mdl-card__actions {
margin-top: 35px;
label.mdl-checkbox {
margin-bottom: 50px;
}
}
}
h3 {
font-size: 36px;
line-height: 48px;
font-weight: bold;
@include m-theme() {
color: themed($m-grey-800);
}
}
.mdl-cell {
margin: 0;
padding-bottom: 25px;
}
form {
background: transparent !important;
.mdl-card__supporting-text {
overflow: visible;
}
.mdl-cell {
width: 100%;
}
input:not([type='checkbox']) {
padding: 10px 15px;
height: 37px;
font-weight: normal;
@include m-theme() {
color: themed($m-grey-800);
}
&:active,
&:focus {
@include m-theme() {
outline: 1px solid themed($m-blue);
}
}
}
&.m-loginBox__bigButton {
.mdl-card__actions {
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
button.mf-button {
width: 132px;
height: 60px;
}
}
}
}
span,
label {
font-size: 14px;
line-height: 19px;
}
label:not(.mdl-checkbox) {
display: inline-block;
margin-bottom: 10px;
@include m-theme() {
color: themed($m-grey-300);
}
}
label.mdl-checkbox {
display: flex;
align-items: center;
padding-top: 0;
margin-bottom: 33px;
}
.mdl-card__actions {
flex-direction: column;
align-items: flex-start;
margin-top: 55px;
padding: 0;
& > *:not(button) {
color: #4a4a4a !important;
}
a {
color: #4a90e2;
}
button {
align-self: flex-end;
@include m-theme() {
background-color: themed($m-aqua);
}
}
}
}
}
}
......@@ -27,6 +27,11 @@ import { mindsTitleMock } from '../../mocks/services/ux/minds-title.service.mock
import { signupModalServiceMock } from '../../mocks/modules/modals/signup/signup-modal-service.mock';
import { SignupModalService } from '../modals/signup/service';
import { By } from '@angular/platform-browser';
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { IfFeatureDirective } from '../../common/directives/if-feature.directive';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { MockService } from '../../utils/mock';
@Component({
selector: 'minds-form-login',
......@@ -35,6 +40,10 @@ import { By } from '@angular/platform-browser';
class MindsFormLoginMock {
@Output() done: EventEmitter<any> = new EventEmitter<any>();
@Output() doneRegistered: EventEmitter<any> = new EventEmitter<any>();
@Input() showBigButton: boolean = false;
@Input() showInlineErrors: boolean = false;
@Input() showTitle: boolean = false;
@Input() showLabels: boolean = false;
}
@Component({
......@@ -57,6 +66,7 @@ describe('LoginComponent', () => {
MindsFormLoginMock,
MindsFormRegisterMock,
LoginComponent,
IfFeatureDirective,
],
imports: [
RouterTestingModule,
......@@ -71,6 +81,8 @@ describe('LoginComponent', () => {
{ provide: OnboardingService, useValue: onboardingServiceMock },
{ provide: MindsTitle, useValue: mindsTitleMock },
{ provide: SignupModalService, useValue: signupModalServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: V2TopbarService, useValue: MockService(V2TopbarService) },
],
}).compileComponents();
}));
......@@ -80,6 +92,8 @@ describe('LoginComponent', () => {
jasmine.clock().uninstall();
jasmine.clock().install();
featuresServiceMock.mock('register_pages-december-2019', false);
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance;
......
import { Component } from '@angular/core';
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -8,20 +8,23 @@ import { MindsTitle } from '../../services/ux/title';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { OnboardingService } from '../onboarding/onboarding.service';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
@Component({
selector: 'm-login',
templateUrl: 'login.component.html',
})
export class LoginComponent {
export class LoginComponent implements OnInit, OnDestroy {
errorMessage: string = '';
twofactorToken: string = '';
hideLogin: boolean = false;
inProgress: boolean = false;
referrer: string;
minds = window.Minds;
private redirectTo: string;
@HostBinding('class.m-login__newDesign')
newDesign: boolean = false;
flags = {
canPlayInlineVideos: true,
......@@ -29,6 +32,8 @@ export class LoginComponent {
paramsSubscription: Subscription;
private redirectTo: string;
constructor(
public client: Client,
public router: Router,
......@@ -36,8 +41,9 @@ export class LoginComponent {
public title: MindsTitle,
private modal: SignupModalService,
private loginReferrer: LoginReferrerService,
public session: Session,
private onboarding: OnboardingService
private featuresService: FeaturesService,
private topbarService: V2TopbarService,
public session: Session
) {}
ngOnInit() {
......@@ -58,21 +64,33 @@ export class LoginComponent {
if (/iP(hone|od)/.test(window.navigator.userAgent)) {
this.flags.canPlayInlineVideos = false;
}
this.newDesign = this.featuresService.has('register_pages-december-2019');
if (this.newDesign) {
this.topbarService.toggleVisibility(false);
}
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
this.topbarService.toggleVisibility(true);
}
loggedin() {
if (this.referrer) this.router.navigateByUrl(this.referrer);
else if (this.redirectTo) this.navigateToRedirection();
else this.loginReferrer.navigate();
if (this.referrer) {
this.router.navigateByUrl(this.referrer);
} else if (this.redirectTo) {
this.navigateToRedirection();
} else {
this.loginReferrer.navigate();
}
}
registered() {
if (this.redirectTo) this.navigateToRedirection();
else {
if (this.redirectTo) {
this.navigateToRedirection();
} else {
this.loginReferrer.navigate({
defaultUrl: '/' + this.session.getLoggedInUser().username,
});
......
<section class="m-register--hero">
<div class="m-register--hero--video">
<video autoplay muted loop *ngIf="!videoError; else fallback">
<source
[src]="minds.cdn_assets_url + 'assets/videos/what-1/what-1.mp4'"
type="video/mp4"
(error)="onSourceError()"
/>
</video>
<ng-template #fallback>
<img [src]="minds.cdn_assets_url + 'assets/photos/cover.png'" />
</ng-template>
</div>
<div class="m-register--hero--inner">
<div class="m-register--hero--overlay"></div>
<div class="m-register--hero--slogans">
<h1 i18n="@@M__SOCIAL_NETWORK_SLOGAN">Where minds gather</h1>
<h3 i18n="@@MINDS__HOME__register__LAUNCH_CTA">
The leading open source social network for Internet freedom. Earn crypto
and free promotion for your contributions.
</h3>
</div>
<div class="m-register--signup" [hidden]="session.isLoggedIn()">
<div
class="m-grid"
*mIfFeature="'register_pages-december-2019'; else registerBlock"
>
<div
class="m-grid__column-7 m-grid__column__skip-5 m-grid__column-10--tablet m-grid__column__skip-2--tablet m-grid__column-12--mobile m-grid__column__skip-1--mobile"
>
<div class="m-register__wrapper">
<minds-form-register
[showTitle]="true"
[showBigButton]="true"
[showPromotions]="false"
[showLabels]="true"
[showInlineErrors]="true"
(done)="registered()"
[referrer]="referrer"
parentId="/register"
></minds-form-register>
>
</minds-form-register>
</div>
</div>
</section>
</div>
<div class="mdl-grid mdl-grid--no-spacing m-register--footer">
<section class="mdl-cell mdl-cell--12-col m-footer">
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" />
<ul class="m-footer-nav m-footer-nav-inline">
<li
*ngFor="let page of navigation.getItems('footer')"
class="m-footer-nav-item "
>
<a
*ngIf="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
>
<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';
......@@ -10,6 +10,10 @@ 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';
@Component({
selector: 'm-register',
......@@ -23,6 +27,9 @@ export class RegisterComponent implements OnInit, OnDestroy {
inProgress: boolean = false;
videoError: boolean = false;
referrer: string;
@HostBinding('class.m-register__newDesign')
newDesign: boolean = false;
private redirectTo: string;
flags = {
canPlayInlineVideos: true,
......@@ -39,15 +46,32 @@ export class RegisterComponent implements OnInit, OnDestroy {
public session: Session,
private onboarding: OnboardingService,
public navigation: NavigationService,
public pagesService: PagesService
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);
}
}
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']) {
......@@ -55,12 +79,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]);
}
......@@ -72,5 +110,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);
}
}
......@@ -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') &&
......
......@@ -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,
......
<div
class="mdl-card mdl-color--red-500 mdl-color-text--blue-grey-50 m-error-box mdl-shadow--2dp minds-login-box m-error-box"
style="min-height:0;"
[hidden]="!errorMessage"
[hidden]="showInlineErrors || !errorMessage"
>
<div class="mdl-card__supporting-text mdl-color-text--blue-grey-50">
<ng-container
*ngIf="errorMessage == 'LoginException::AttemptsExceeded'"
i18n="@@MINDS__LOGIN__EXCEPTION__ATTEMPTS_EXCEEDED"
>
You have exceeded your login attempts. Please try again in a few minutes.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::DisabledUser'"
i18n="@@MINDS__LOGIN__EXCEPTION__DISABLED_USER"
>
This account has been disabled
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::AuthenticationFailed'"
i18n="@@MINDS__LOGIN__EXCEPTION__INCORRECT_USERNAME_PASSWORD"
>
Incorrect username/password. Please try again.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::AccountLocked'"
i18n="@@MINDS__LOGIN__EXCEPTION__ACCOUNT_LOCKED"
>
Account locked
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException:BannedUser'"
i18n="@@MINDS__LOGIN__EXCEPTION__BANNED_USER"
>
You are not allowed to login.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::CodeVerificationFailed'"
i18n="@@MINDS__LOGIN__EXCEPTION__CODE_VERIFICATION_FAILED"
>
Sorry, we couldn't verify your two factor code. Please try logging again.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::InvalidToken'"
i18n="@@MINDS__LOGIN__EXCEPTION__INVALID_TOKEN"
>
Invalid token
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::EmailAddress'"
i18n="@@MINDS__LOGIN__EXCEPTION__ENTERED_EMAIL_ADDRESS_NOT_USERNAME"
>
Please enter a username instead of an email address.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::Unknown'"
i18n="@@MINDS__LOGIN__EXCEPTION__SORRY_THERE_WAS_AN_ERROR_PLEASE_TRY_AGAIN"
>
Sorry, there was an error. Please try again.
</ng-container>
<ng-container *ngTemplateOutlet="errorTemplate"></ng-container>
</div>
</div>
<ng-template #errorTemplate>
<ng-container
*ngIf="errorMessage == 'LoginException::AttemptsExceeded'"
i18n="@@MINDS__LOGIN__EXCEPTION__ATTEMPTS_EXCEEDED"
>
You have exceeded your login attempts. Please try again in a few minutes.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::DisabledUser'"
i18n="@@MINDS__LOGIN__EXCEPTION__DISABLED_USER"
>
This account has been disabled
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::AuthenticationFailed'"
i18n="@@MINDS__LOGIN__EXCEPTION__INCORRECT_USERNAME_PASSWORD"
>
Incorrect username/password. Please try again.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::AccountLocked'"
i18n="@@MINDS__LOGIN__EXCEPTION__ACCOUNT_LOCKED"
>
Account locked
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException:BannedUser'"
i18n="@@MINDS__LOGIN__EXCEPTION__BANNED_USER"
>
You are not allowed to login.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::CodeVerificationFailed'"
i18n="@@MINDS__LOGIN__EXCEPTION__CODE_VERIFICATION_FAILED"
>
Sorry, we couldn't verify your two factor code. Please try logging again.
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::InvalidToken'"
i18n="@@MINDS__LOGIN__EXCEPTION__INVALID_TOKEN"
>
Invalid token
</ng-container>
<ng-container
*ngIf="errorMessage == 'LoginException::Unknown'"
i18n="@@MINDS__LOGIN__EXCEPTION__SORRY_THERE_WAS_AN_ERROR_PLEASE_TRY_AGAIN"
>
Sorry, there was an error. Please try again.
</ng-container>
</ng-template>
<div class="m-login__title">
<h3 *ngIf="showTitle">Login to Minds</h3>
<a *ngIf="showTitle" [routerLink]="['/forgot-password']">
Forgot your Password?
</a>
</div>
<!-- START: LOGIN -->
<form
(submit)="$event.preventDefault()"
[formGroup]="form"
class="mdl-card mdl-color-text--blue-grey-400 m-form m-login-box"
[class.m-loginBox__bigButton]="showBigButton"
[hidden]="hideLogin"
>
<div class="mdl-card__supporting-text mdl-grid">
<div class="mdl-cell mdl-cell--12-col">
<label for="username" *ngIf="showLabels" i18n>
Username
</label>
<input
type="text"
id="username"
formControlName="username"
placeholder="Username"
[placeholder]="showLabels ? '' : 'Username'"
i18n-placeholder="@@M__COMMON__USERNAME"
autocomplete="username"
(keydown.enter)="login(); $event.preventDefault();"
autofocus
/>
<div class="m-login__error" *ngIf="!!usernameError">
<ng-container
*ngIf="usernameError == 'LoginException::EmailAddress'"
i18n="@@MINDS__LOGIN__EXCEPTION__ENTERED_EMAIL_ADDRESS_NOT_USERNAME"
>
Please enter a username instead of an email address.
</ng-container>
<ng-container
*ngIf="usernameError == 'LoginException::UsernameRequired'"
i18n="@@MINDS__LOGIN__EXCEPTION__USERNAME_REQUIRED"
>
Username is required.
</ng-container>
</div>
</div>
<div class="mdl-cell mdl-cell--12-col">
<label for="password" *ngIf="showLabels" i18n>
Password
</label>
<input
type="password"
id="password"
formControlName="password"
placeholder="Password"
[placeholder]="showLabels ? '' : 'Password'"
i18n-placeholder="@@M__COMMON__PASSWORD"
autocomplete="current-password"
(keydown.enter)="login(); $event.preventDefault();"
......@@ -95,20 +124,39 @@
</div>
<div class="mdl-card__actions">
<div
class="m-login__formError"
*ngIf="showInlineErrors && showBigButton && errorMessage"
>
<ng-container *ngTemplateOutlet="errorTemplate"></ng-container>
</div>
<div class="m-login__spacer" *ngIf="showBigButton"></div>
<button
class="m-btn m-btn--action m-btn--login"
(click)="login()"
[disabled]="inProgress"
*ngIf="!showBigButton"
>
<ng-container i18n="@@M__ACTION__LOGIN">Login</ng-container>
</button>
<button
class="mf-button mf-button--alt"
(click)="login()"
[disabled]="inProgress"
*ngIf="showBigButton"
>
<ng-container i18n="@@M__ACTION__LOGIN">
Login
</ng-container>
</button>
<a
class="mdl-card__subtitle-text mdl-color-text--blue-grey-300 m-reset-password-link"
[routerLink]="['/forgot-password']"
*ngIf="!showTitle"
>
<ng-container i18n="@@FORMS__LOGIN__FORGOT_PASSWORD_LINK"
>Forgot Password?</ng-container
>
>Forgot Password?
</ng-container>
</a>
</div>
</form>
......@@ -123,10 +171,9 @@
</div>
<div class="mdl-card__supporting-text">
<ng-container i18n="@@FORMS__LOGIN__2FA_LOGIN_SMS_SENT"
>We just sent you a text. Enter the code below to
authenticate.</ng-container
>
<ng-container i18n="@@FORMS__LOGIN__2FA_LOGIN_SMS_SENT">
We just sent you a text. Enter the code below to authenticate.
</ng-container>
</div>
<div
......
@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;
......
import { Component, EventEmitter, NgZone, Output } from '@angular/core';
import { Component, EventEmitter, Input, NgZone, Output } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Client } from '../../../services/api';
......@@ -11,6 +11,10 @@ import { UserAvatarService } from '../../../common/services/user-avatar.service'
templateUrl: 'login.html',
})
export class LoginForm {
@Input() showBigButton: boolean = false;
@Input() showInlineErrors: boolean = false;
@Input() showTitle: boolean = false;
@Input() showLabels: boolean = false;
@Output() done: EventEmitter<any> = new EventEmitter();
@Output() doneRegistered: EventEmitter<any> = new EventEmitter();
......@@ -21,9 +25,11 @@ export class LoginForm {
referrer: string;
minds = window.Minds;
usernameError: string;
form: FormGroup;
//Taken from advice in https://stackoverflow.com/a/1373724
// Taken from advice in https://stackoverflow.com/a/1373724
private emailRegex: RegExp = new RegExp(
"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
);
......@@ -42,21 +48,40 @@ export class LoginForm {
}
login() {
if (this.inProgress) return;
if (this.inProgress) {
return;
}
this.usernameError = null;
const username = this.form.value.username.trim();
if (username === '') {
if (this.showInlineErrors) {
this.usernameError = 'LoginException::UsernameRequired';
} else {
this.errorMessage = 'LoginException::UsernameRequired';
}
return;
}
let username = this.form.value.username.trim();
if (this.emailRegex.test(username)) {
this.errorMessage = 'LoginException::EmailAddress';
if (this.showInlineErrors) {
this.usernameError = 'LoginException::EmailAddress';
} else {
this.errorMessage = 'LoginException::EmailAddress';
}
return;
}
//re-enable cookies
// re-enable cookies
document.cookie =
'disabled_cookies=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
this.errorMessage = '';
this.inProgress = true;
let opts = {
const opts = {
username: username,
password: this.form.value.password,
};
......@@ -77,7 +102,7 @@ export class LoginForm {
this.errorMessage = 'LoginException::Unknown';
this.session.logout();
} else if (e.status === 'failed') {
//incorrect login details
// incorrect login details
this.errorMessage = 'LoginException::AuthenticationFailed';
this.session.logout();
} else if (e.status === 'error') {
......@@ -88,7 +113,7 @@ export class LoginForm {
this.session.logout();
}
//two factor?
// two factor?
this.twofactorToken = e.message;
this.hideLogin = true;
} else {
......
<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>
m-popover {
.m-popover__wrapper {
position: relative;
//margin-top: 1.5rem;
display: block;
.m-popover__content {
margin-top: 2.2rem;
opacity: 0;
visibility: hidden;
position: absolute;
//left: -150px;
transform: translate(0, 10px);
padding: 20px;
filter: drop-shadow(0px 2px 5px rgba(0, 0, 0, 0.26));
width: 90%;
&::before {
position: absolute;
z-index: -1;
content: '';
top: -8px;
border-style: solid;
border-width: 0 10px 10px 10px;
transition-duration: 0.3s;
transition-property: transform;
height: 24px;
@include m-theme() {
border-color: transparent transparent themed($m-white) transparent;
}
}
@include m-theme() {
background-color: themed($m-white);
}
&.m-popover__content--visible {
z-index: 10;
opacity: 1;
visibility: visible;
transform: translate(0, -20px);
transition: all 0.5s cubic-bezier(0.75, -0.02, 0.2, 0.97);
}
ul.m-popover__rules {
list-style-type: none;
li {
@include m-theme() {
color: themed($m-grey-300);
}
&::before {
content: ' ';
margin: 0 5px 0 -15px;
width: 24px;
vertical-align: middle;
line-height: 1;
font-size: 21px;
letter-spacing: normal;
text-transform: none;
display: inline-block;
font-weight: 400;
font-style: normal;
word-wrap: normal;
-moz-font-feature-settings: 'liga';
font-feature-settings: 'liga';
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
&.m-popover__rule--checked::before {
font-family: 'Material Icons';
@include m-theme() {
color: themed($m-green);
}
// check
content: '\e5ca';
}
}
}
}
}
}
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Output,
ViewChild,
} from '@angular/core';
@Component({
selector: 'm-popover',
templateUrl: 'popover.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PopoverComponent {
@ViewChild('content', { static: true }) content: ElementRef;
lengthCheck: boolean = false;
specialCharCheck: boolean = false;
mixedCaseCheck: boolean = false;
numbersCheck: boolean = false;
spacesCheck: boolean = false;
hidden: boolean = false;
@Output() change: EventEmitter<boolean> = new EventEmitter<boolean>();
constructor(protected cd: ChangeDetectorRef) {}
show(): void {
if (!this.hidden) {
this.content.nativeElement.classList.add('m-popover__content--visible');
this.detectChanges();
}
}
hide(keepHidden: boolean = false): void {
this.content.nativeElement.classList.remove('m-popover__content--visible');
this.hidden = keepHidden;
this.detectChanges();
}
checkRules(str: string): void {
this.lengthCheck = str.length >= 8;
this.specialCharCheck = /[^a-zA-Z\d]/.exec(str) !== null;
this.mixedCaseCheck =
/[a-z]/.exec(str) !== null && /[A-Z]/.exec(str) !== null;
this.numbersCheck = /\d/.exec(str) !== null;
this.spacesCheck = /\s/.exec(str) === null;
// if everything is right, wait a bit and hide
if (
this.lengthCheck &&
this.specialCharCheck &&
this.mixedCaseCheck &&
this.numbersCheck &&
this.spacesCheck
) {
this.change.emit(true);
setTimeout(() => this.hide(true), 500);
} else {
this.change.emit(false);
}
this.detectChanges();
}
detectChanges(): void {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<div
class="mdl-card mdl-color--red-500 mdl-color-text--blue-grey-50 mdl-shadow--2dp minds-login-box m-error-box"
style="min-height:0;"
[hidden]="!errorMessage"
[hidden]="showInlineErrors || !errorMessage"
>
<div class="mdl-card__supporting-text mdl-color-text--blue-grey-50">
{{errorMessage}}
<ng-container *ngTemplateOutlet="errorTemplate"></ng-container>
</div>
</div>
<ng-template #errorTemplate>
{{ errorMessage }}
</ng-template>
<h3 *ngIf="showTitle">Join the Minds Revolution</h3>
<span class="m-register__alreadyAUser" *ngIf="showTitle">
Already have an account? <a [routerLink]="'/login'">Login</a>
</span>
<!-- START: Register -->
<form
(submit)="$event.preventDefault()"
......@@ -18,67 +27,109 @@
>
<div class="mdl-card__supporting-text mdl-grid">
<div class="mdl-cell mdl-cell--12-col">
<label for="username" *ngIf="showLabels" i18n>
Username
</label>
<input
type="text"
id="username"
formControlName="username"
placeholder="Username"
[placeholder]="showLabels ? '' : 'Username'"
i18n-placeholder="@@M__COMMON__USERNAME"
(keyup)="validationTimeoutHandler()"
readonly
onfocus="this.removeAttribute('readonly');"
[class.m-input--hide-placeholder]="showLabels"
autofocus
/>
<div
class="m-register__error"
[style.visibility]="showError('username') ? 'visible' : 'hidden'"
>
<ng-container
*ngIf="this.form.get('username').errors?.minlength"
i18n="@@MINDS__REGISTER__EXCEPTION__USERNAME_TOO_SHORT"
>
Must be at least 4 characters long
</ng-container>
<ng-container
*ngIf="this.form.get('username').errors?.maxlength"
i18n="@@MINDS__REGISTER__EXCEPTION__USERNAME_TOO_LONG"
>
Cannot be longer than 128 characters
</ng-container>
</div>
</div>
<div class="mdl-cell mdl-cell--12-col">
<label for="email" *ngIf="showLabels" i18n>
Email
</label>
<input
type="email"
id="email"
formControlName="email"
placeholder="Email"
[placeholder]="showLabels ? '' : 'Email'"
i18n-placeholder="email placeholder|@@FORMS__REGISTER__EMAIL_PLACEHOLDER"
[class.m-input--hide-placeholder]="showLabels"
/>
</div>
<div class="mdl-cell mdl-cell--12-col" style="position: relative">
<input
type="password"
id="password"
formControlName="password"
placeholder="Password"
i18n-placeholder="@@M__COMMON__PASSWORD"
readonly
onfocus="this.removeAttribute('readonly');"
/>
<m-tooltip
class="tooltip-wrapper"
icon="help"
i18n="@@MINDS__PASSWORD_TOOLTIP"
anchor="right"
<div
class="m-register__error"
[style.visibility]="showError('email') ? 'visible' : 'hidden'"
>
Password must have more than 8 characters. Including uppercase, numbers,
special characters (ie. !,#,@), and cannot have spaces.
</m-tooltip>
<ng-container
*ngIf="this.form.get('email').errors?.email"
i18n="@@MINDS__REGISTER__EXCEPTION__INVALID_EMAIL"
>
Invalid email
</ng-container>
</div>
</div>
<div
class="mdl-cell mdl-cell--12-col password-help"
[hidden]="errorMessage || !form.value.password"
>
<span i18n="@@MINDS__PASSWORD_TOOLTIP"
>Password must have more than 8 characters. Including uppercase,
numbers, special characters (ie. !,#,@), and cannot have spaces</span
>
<div class="mdl-cell mdl-cell--12-col" style="position: relative">
<label for="password" *ngIf="showLabels" i18n>
Password
</label>
<m-popover (change)="onPopoverChange($event)" #popover>
<input
type="password"
id="password"
formControlName="password"
[placeholder]="showLabels ? '' : 'Password'"
i18n-placeholder="@@M__COMMON__PASSWORD"
readonly
onfocus="this.removeAttribute('readonly');"
(focus)="onPasswordFocus()"
(blur)="onPasswordBlur()"
(ngModelChange)="popover.show(); popover.checkRules($event)"
[class.m-input--hide-placeholder]="showLabels"
/>
</m-popover>
</div>
<div class="mdl-cell mdl-cell--12-col" [hidden]="!form.value.password">
<label for="password2" *ngIf="showLabels" i18n>
Confirm your password
</label>
<input
type="password"
id="password2"
formControlName="password2"
placeholder="Confirm your password"
[placeholder]="showLabels ? '' : 'Confirm your password'"
i18n-placeholder="@@FORMS__REGISTER__CONFIRM_PASSWORD_PLACEHOLDER"
(keydown.enter)="register($event)"
readonly
onfocus="this.removeAttribute('readonly');"
[class.m-input--hide-placeholder]="showLabels"
/>
<div
class="m-register__error"
*ngIf="this.form.get('password2').touched && this.form.get('password2').dirty"
>
<ng-container
*ngIf="this.form.errors?.passwordConfirming"
i18n="@@MINDS__REGISTER__EXCEPTION__INVALID_EMAIL"
>
Passwords must match
</ng-container>
</div>
</div>
<div
......@@ -95,12 +146,20 @@
</div>
</div>
<div
class="m-register__formError"
*ngIf="showInlineErrors && showBigButton && errorMessage"
>
<ng-container *ngTemplateOutlet="errorTemplate"></ng-container>
</div>
<div class="mdl-card__actions">
<div>
<label
class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect"
[mdlSwitch]
[toggled]="form.value.exclusive_promotions"
*ngIf="showPromotions"
>
<input
type="checkbox"
......@@ -132,8 +191,8 @@
class="m-register-tac"
i18n="@@FORMS__REGISTER__TOS_ACCEPTANCE_LABEL"
>
I accept the
<a routerLink="/p/terms" target="_blank"> Terms & Conditions </a>
I have read and accept the
<a routerLink="/p/terms" target="_blank"> terms of use </a>
</span>
</label>
</div>
......@@ -142,10 +201,22 @@
class="m-btn m-btn--action"
(click)="register($event)"
[disabled]="!this.form.valid || inProgress"
*ngIf="!showBigButton"
>
<ng-container i18n="@@FORMS__REGISTER__SIGNUP_ACTION"
>Signup</ng-container
>
<ng-container i18n="@@FORMS__REGISTER__SIGNUP_ACTION">
Signup
</ng-container>
</button>
<button
class="mf-button mf-button--alt"
(click)="register($event)"
[disabled]="!this.form.valid|| this.passwordFieldValid || inProgress"
*ngIf="showBigButton"
>
<ng-container i18n="@@FORMS__REGISTER__JOIN_MINDS_NOW_ACTION">
Join Minds Now
</ng-container>
</button>
</div>
</form>
......
......@@ -90,4 +90,8 @@ minds-form-register {
.mdl-checkbox {
padding-top: 5px;
}
input.m-input--hide-placeholder::placeholder {
color: transparent;
}
}
......@@ -5,23 +5,37 @@ import {
Input,
Output,
NgZone,
OnInit,
} from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import {
FormGroup,
FormBuilder,
Validators,
AbstractControl,
ValidationErrors,
} from '@angular/forms';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { ReCaptchaComponent } from '../../../modules/captcha/recaptcha/recaptcha.component';
import { ExperimentsService } from '../../experiments/experiments.service';
import { RouterHistoryService } from '../../../common/services/router-history.service';
import { PopoverComponent } from '../popover-validation/popover.component';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
selector: 'minds-form-register',
templateUrl: 'register.html',
})
export class RegisterForm {
export class RegisterForm implements OnInit {
@Input() referrer: string;
@Input() parentId: string = '';
@Input() showTitle: boolean = false;
@Input() showBigButton: boolean = false;
@Input() showPromotions: boolean = true;
@Input() showLabels: boolean = false;
@Input() showInlineErrors: boolean = false;
@Output() done: EventEmitter<any> = new EventEmitter();
......@@ -32,6 +46,7 @@ export class RegisterForm {
captcha: string;
takenUsername: boolean = false;
usernameValidationTimeout: any;
passwordFieldValid: boolean = false;
showFbForm: boolean = false;
......@@ -40,6 +55,7 @@ export class RegisterForm {
minds = window.Minds;
@ViewChild('reCaptcha', { static: false }) reCaptcha: ReCaptchaComponent;
@ViewChild('popover', { static: false }) popover: PopoverComponent;
constructor(
public session: Session,
......@@ -49,16 +65,26 @@ export class RegisterForm {
private experiments: ExperimentsService,
private routerHistoryService: RouterHistoryService
) {
this.form = fb.group({
username: ['', Validators.required],
email: ['', Validators.required],
password: ['', Validators.required],
password2: ['', Validators.required],
tos: [false],
exclusive_promotions: [false],
captcha: [''],
previousUrl: this.routerHistoryService.getPreviousUrl(),
});
this.form = fb.group(
{
username: [
'',
[
Validators.required,
Validators.minLength(4),
Validators.maxLength(128),
],
],
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
password2: ['', [Validators.required]],
tos: [false, Validators.requiredTrue],
exclusive_promotions: [false],
captcha: [''],
previousUrl: this.routerHistoryService.getPreviousUrl(),
},
{ validators: [this.passwordConfirmingValidator] }
);
}
ngOnInit() {
......@@ -67,6 +93,21 @@ export class RegisterForm {
}
}
showError(field: string) {
return (
this.showInlineErrors &&
this.form.get(field).invalid &&
this.form.get(field).touched &&
this.form.get(field).dirty
);
}
passwordConfirmingValidator(c: AbstractControl): ValidationErrors | null {
if (c.get('password').value !== c.get('password2').value) {
return { passwordConfirming: true };
}
}
register(e) {
e.preventDefault();
this.errorMessage = '';
......@@ -76,7 +117,7 @@ export class RegisterForm {
return;
}
//re-enable cookies
// re-enable cookies
document.cookie =
'disabled_cookies=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
......@@ -103,7 +144,6 @@ export class RegisterForm {
this.inProgress = false;
this.session.login(data.user);
this.done.next(data.user);
})
.catch(e => {
......@@ -114,11 +154,11 @@ export class RegisterForm {
}
if (e.status === 'failed') {
//incorrect login details
// incorrect login details
this.errorMessage = 'RegisterException::AuthenticationFailed';
this.session.logout();
} else if (e.status === 'error') {
//two factor?
// two factor?
this.errorMessage = e.message;
this.session.logout();
} else {
......@@ -160,4 +200,18 @@ export class RegisterForm {
500
);
}
onPasswordFocus() {
if (this.form.value.password.length > 0) {
this.popover.show();
}
}
onPasswordBlur() {
this.popover.hide();
}
onPopoverChange(valid: boolean) {
this.passwordFieldValid = !valid;
}
}
......@@ -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]);
});
})
......
import { Component, Inject, EventEmitter } from '@angular/core';
import {
Component,
Inject,
EventEmitter,
HostListener,
HostBinding,
Input,
} from '@angular/core';
import { Router } from '@angular/router';
import { GroupsService } from './groups-service';
......@@ -11,61 +18,90 @@ import { LoginReferrerService } from '../../services/login-referrer.service';
inputs: ['_group: group'],
outputs: ['membership'],
template: `
<button
class="m-btn m-btn--slim m-btn--join-group"
*ngIf="
!group['is:banned'] &&
!group['is:awaiting'] &&
!group['is:invited'] &&
!group['is:member']
"
(click)="join()"
i18n="@@GROUPS__JOIN_BUTTON__JOIN_ACTION"
>
<ng-container *ngIf="!inProgress">Join</ng-container>
<ng-container *ngIf="inProgress">Joining</ng-container>
</button>
<span *ngIf="group['is:invited'] &amp;&amp; !group['is:member']">
<ng-container *ngIf="iconsOnly; else normalView">
<div
class="m-groupsJoin__subscribe"
(click)="join()"
*ngIf="
!group['is:banned'] &&
!group['is:awaiting'] &&
!group['is:invited'] &&
!group['is:member']
"
>
<i class="material-icons">
add
</i>
</div>
<div
class="m-groupsJoin__subscribed"
(click)="leave()"
*ngIf="group['is:member']"
>
<i class="material-icons">
check
</i>
</div>
</ng-container>
<ng-template #normalView>
<button
class="m-btn m-btn--slim m-btn--join-group"
*ngIf="
!group['is:banned'] &&
!group['is:awaiting'] &&
!group['is:invited'] &&
!group['is:member']
"
(click)="join()"
i18n="@@GROUPS__JOIN_BUTTON__JOIN_ACTION"
>
<ng-container *ngIf="!inProgress">Join</ng-container>
<ng-container *ngIf="inProgress">Joining</ng-container>
</button>
<span *ngIf="group['is:invited'] &amp;&amp; !group['is:member']">
<button
class="m-btn m-btn--slim m-btn--action"
(click)="accept()"
i18n="@@M__ACTION__ACCEPT"
>
Accept
</button>
<button
class="m-btn m-btn--slim m-btn--action"
(click)="decline()"
i18n="@@GROUPS__JOIN_BUTTON__DECLINE_ACTION"
>
Decline
</button>
</span>
<button
class="m-btn m-btn--slim m-btn--action"
(click)="accept()"
i18n="@@M__ACTION__ACCEPT"
class="m-btn m-btn--slim subscribed "
*ngIf="group['is:member']"
(click)="leave()"
i18n="@@GROUPS__JOIN_BUTTON__LEAVE_ACTION"
>
Accept
Leave
</button>
<button
class="m-btn m-btn--slim m-btn--action"
(click)="decline()"
i18n="@@GROUPS__JOIN_BUTTON__DECLINE_ACTION"
class="m-btn m-btn--slim awaiting"
*ngIf="group['is:awaiting']"
(click)="cancelRequest()"
i18n="@@GROUPS__JOIN_BUTTON__CANCEL_REQ_ACTION"
>
Decline
Cancel request
</button>
</span>
<button
class="m-btn m-btn--slim subscribed "
*ngIf="group['is:member']"
(click)="leave()"
i18n="@@GROUPS__JOIN_BUTTON__LEAVE_ACTION"
>
Leave
</button>
<button
class="m-btn m-btn--slim awaiting"
*ngIf="group['is:awaiting']"
(click)="cancelRequest()"
i18n="@@GROUPS__JOIN_BUTTON__CANCEL_REQ_ACTION"
>
Cancel request
</button>
<m-modal-signup-on-action
[open]="showModal"
(closed)="join(); showModal = false"
action="join a group"
i18n-action="@@GROUPS__JOIN_BUTTON__JOIN_A_GROUP_TITLE"
[overrideOnboarding]="true"
*ngIf="!session.isLoggedIn()"
>
</m-modal-signup-on-action>
<m-modal-signup-on-action
[open]="showModal"
(closed)="join(); showModal = false"
action="join a group"
i18n-action="@@GROUPS__JOIN_BUTTON__JOIN_A_GROUP_TITLE"
[overrideOnboarding]="true"
*ngIf="!session.isLoggedIn()"
>
</m-modal-signup-on-action>
</ng-template>
`,
})
export class GroupsJoinButton {
......@@ -75,6 +111,10 @@ export class GroupsJoinButton {
membership: EventEmitter<any> = new EventEmitter();
inProgress: boolean = false;
@HostBinding('class.m-groupsJoin--iconsOnly')
@Input()
iconsOnly: boolean = false;
constructor(
public session: Session,
public service: GroupsService,
......
......@@ -3,6 +3,14 @@ import { Client, Upload } from '../../services/api';
import { UpdateMarkersService } from '../../common/services/update-markers.service';
import { BehaviorSubject } from 'rxjs';
export interface MembershipUpdate {
show: boolean;
guid: string;
}
/**
* Service for groups.
*/
export class GroupsService {
private base: string = 'api/v1/groups/';
......@@ -12,6 +20,14 @@ export class GroupsService {
group = new BehaviorSubject(null);
$group = this.group.asObservable();
// Observable handling membership state.
public membershipUpdate$: BehaviorSubject<
MembershipUpdate
> = new BehaviorSubject({
show: null,
guid: null,
});
static _(
client: Client,
upload: Upload,
......@@ -89,6 +105,7 @@ export class GroupsService {
return this.clientService
.delete(`${this.base}group/${group.guid}`)
.then((response: any) => {
this.updateMembership(false, group.guid);
return !!response.done;
})
.catch(e => {
......@@ -96,6 +113,18 @@ export class GroupsService {
});
}
/**
* Emits membership changes to subscribed components.
* @param { boolean } - whether or not observable should be shown or hidden.
* @param { string } - the GUID of the observable.
*/
updateMembership(show: boolean, guid: string): void {
this.membershipUpdate$.next({
show: show,
guid: guid,
});
}
// Membership
join(group: any, target: string = null) {
......@@ -107,6 +136,7 @@ export class GroupsService {
return this.clientService.put(endpoint).then((response: any) => {
if (response.done) {
this.updateMembership(true, group.guid);
return true;
}
......@@ -123,6 +153,7 @@ export class GroupsService {
return this.clientService.delete(endpoint).then((response: any) => {
if (response.done) {
this.updateMembership(false, group.guid);
return true;
}
......
......@@ -11,6 +11,7 @@ import { startWith, map, tap, throttle } from 'rxjs/operators';
import { UpdateMarkersService } from '../../../common/services/update-markers.service';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { GroupsService } from '../groups-service';
@Component({
selector: 'm-group--sidebar-markers',
......@@ -31,6 +32,7 @@ export class GroupsSidebarMarkersComponent {
private client: Client,
public session: Session,
private updateMarkers: UpdateMarkersService,
private groupsService: GroupsService,
private cd: ChangeDetectorRef
) {}
......@@ -38,6 +40,25 @@ export class GroupsSidebarMarkersComponent {
this.onResize();
await this.load(true);
this.listenForMarkers();
this.listenForMembershipUpdates();
}
/**
* Listens and responds to membership updates emited from groupsService.
*/
listenForMembershipUpdates(): void {
this.groupsService.membershipUpdate$.subscribe(update => {
if (!update.guid) {
return;
}
if (update.show) {
this.groupsService.load(update.guid).then(group => {
this.groups.unshift(group);
});
return;
}
this.groups = this.groups.filter(group => group.guid !== update.guid);
});
}
listenForMarkers() {
......
This diff is collapsed.
This diff is collapsed.
import { Component, OnDestroy, ViewChild } from '@angular/core';
import { Client } from '../../services/api/client';
import { MindsTitle } from '../../services/ux/title';
import { Router } from '@angular/router';
import { Navigation as NavigationService } from '../../services/navigation';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { Session } from '../../services/session';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { RegisterForm } from '../forms/register/register';
import { FeaturesService } from '../../services/features.service';
@Component({
selector: 'm-homepage__v2',
templateUrl: 'homepage-v2.component.html',
})
export class HomepageV2Component {
@ViewChild('registerForm', { static: false }) registerForm: RegisterForm;
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
minds = window.Minds;
constructor(
public client: Client,
public title: MindsTitle,
public router: Router,
public navigation: NavigationService,
public session: Session,
private loginReferrer: LoginReferrerService,
private featuresService: FeaturesService
) {
this.title.setTitle('Minds Social Network', false);
if (this.session.isLoggedIn()) {
this.router.navigate(['/newsfeed']);
return;
}
}
navigate() {
if (this.featuresService.has('onboarding-december-2019')) {
this.router.navigate(['/onboarding']);
} else {
this.router.navigate(['/login']);
}
}
isMobile() {
return window.innerWidth <= 540;
}
}
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import {
FormsModule as NgFormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import { CommonModule } from '../../common/common.module';
import { LegacyModule } from '../legacy/legacy.module';
import { ModalsModule } from '../modals/modals.module';
import { MindsFormsModule } from '../forms/forms.module';
import { MarketingModule } from '../marketing/marketing.module';
import { ExperimentsModule } from '../experiments/experiments.module';
import { HomepageV2Component } from './homepage-v2.component';
import { CaptchaModule } from '../captcha/captcha.module';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
NgCommonModule,
NgFormsModule,
ReactiveFormsModule,
CommonModule,
LegacyModule,
ModalsModule,
MindsFormsModule,
MarketingModule,
ExperimentsModule,
CaptchaModule,
RouterModule,
],
declarations: [HomepageV2Component],
entryComponents: [HomepageV2Component],
exports: [HomepageV2Component],
})
export class HomepageV2Module {}
This diff is collapsed.
This diff is collapsed.
......@@ -26,6 +26,7 @@ m-newsfeed__tiles {
margin: 4px;
overflow: hidden;
transform-style: preserve-3d;
@include m-theme() {
background-color: rgba(themed($m-black), 0.65);
}
......
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.