...
 
Commits (47)
......@@ -202,6 +202,8 @@ prepare:production:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID -f containers/server/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID
- docker tag $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID $CI_REGISTRY_IMAGE/server:latest
- docker push $CI_REGISTRY_IMAGE/server:latest
only:
refs:
- master
......@@ -292,7 +294,7 @@ review:stop:
- docker:dind
script:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
- aws s3 sync dist $S3_REPOSITORY_URL --cache-control max-age=31536000
- $(aws ecr get-login --no-include-email --region us-east-1)
## Update docker server container
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
......
#!/bin/sh
export NODE_OPTIONS="--max-old-space-size=3584"
ng build --prod --vendor-chunk --output-path="$1/en/" --deploy-url="$2/en/" --build-optimizer=true --stats-json
ng build --prod --output-path="$1/en/" --deploy-url="$2/en/" --build-optimizer=true --stats-json
FROM node:13-alpine
COPY monitor.sh /monitor.sh
COPY build.sh /build.sh
ENTRYPOINT ["sh", "/monitor.sh"]
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
echo Building...
npx ng run minds:server:production
echo Compiling...
npm run compile:server
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
npx nodemon --delay 3 --watch server.ts --watch dist/en/ --ext js,css,jpg,png,svg,mp4,webp,webm --exec "/usr/bin/env sh" /build.sh
FROM node:13-alpine
COPY monitor.sh /monitor.sh
COPY serve.sh /serve.sh
ENTRYPOINT ["sh", "/monitor.sh"]
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
npx nodemon --delay 3 --watch dist/server.js --watch dist/server --ext js,mjs --exec "/usr/bin/env sh" /serve.sh
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
echo Running...
npm run serve:ssr
......@@ -5,6 +5,10 @@ FROM node:13-alpine
COPY . /dist
CMD node /dist/server
RUN npm install pm2 -g
CMD pm2-runtime /dist/server \
--max-memory-restart 512M \
--instances 2
VOLUME ["/dist"]
\ No newline at end of file
import generateRandomId from "../../support/utilities";
context('Onboarding', () => {
const remindText = 'remind test text';
const username = generateRandomId();
const password = `${generateRandomId()}0oA!`;
const email = 'test@minds.com';
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 checkbox = '[data-cy=data-minds-accept-tos-input]';
const submitButton = 'minds-form-register .mdl-card__actions button';
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
cy.visit('/register');
cy.location('pathname').should('eq', '/register');
cy.server();
cy.route("POST", "**/api/v1/register").as("register");
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);
}
});
cy.visit(`/onboarding`);
);
cy.wait(500);
cy.location('pathname').should('eq', '/onboarding/notice');
});
// create two test groups
after(() => {
cy.deleteUser(username, password);
cy.clearCookies();
});
beforeEach(() => {
......@@ -21,7 +65,7 @@ context('Onboarding', () => {
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}`);
cy.get('h2.m-onboarding__noticeTitle').contains(username);
// should redirect to /hashtags
cy.get('.m-onboarding__form button.mf-button').contains("Let's Get Setup").click();
......@@ -73,8 +117,8 @@ context('Onboarding', () => {
// 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');
cy.get('ul.m-onboarding__cities > li:first-child').click();
// cy.get('.m-onboarding__controls > .m-onboarding__control input[data-minds=locationInput]').type('London');
// cy.get('ul.m-onboarding__cities > li:first-child').click();
// should have Date of Birth inputs
......@@ -91,7 +135,11 @@ context('Onboarding', () => {
// 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();
cy.get('button.mf-button--alt').contains('Finish').click();
// TODO: disable the following line and uncomment the rest when we re-enable the screens
// should be in the newsfeed
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
// should be in the Groups step
......@@ -105,8 +153,8 @@ context('Onboarding', () => {
// 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();
// cy.get('button.mf-button--hollow').contains('Skip');
// cy.get('button.mf-button--alt').contains('Continue').click();
// should be in the Channels step
......@@ -120,10 +168,10 @@ context('Onboarding', () => {
// 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();
// 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');
// cy.location('pathname').should('eq', '/newsfeed/subscriptions');
});
});
......@@ -60,7 +60,7 @@ context('Registration', () => {
});
cy.wait(500);
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
cy.location('pathname').should('eq', '/onboarding/notice');
});
it('should display an error if passwords do not match', () => {
......
......@@ -7,7 +7,6 @@ import * as template from 'gulp-template';
import { join } from 'path';
import { argv } from 'yargs';
const AUTOPREFIXER_BROWSERS = [
'ie >= 11',
'ie_mob >= 11',
......@@ -17,32 +16,44 @@ const AUTOPREFIXER_BROWSERS = [
'opera >= 23',
'ios >= 7',
'android >= 4.4',
'bb >= 10'
'bb >= 10',
];
// --------------
// Build SASS
gulp.task('build.sass', done => {
const app_cdn = argv.deployUrl ? argv.deployUrl: '';
gulp.src(join('./src', '**', '*.scss'))
.pipe(cssGlobbing({ extensions: ['.scss'] }))
.pipe(sass({
includePaths: [join('./src', 'stylesheets')],
style: 'compressed'
}).on('error', sass.logError))
const app_cdn = argv.deployUrl ? argv.deployUrl : '';
gulp
.src(join(__dirname, 'src', '**', '*.scss'))
.pipe(cssGlobbing({ extensions: ['.scss'] }))
.pipe(
sass({
includePaths: [join(__dirname, 'src', 'stylesheets')],
style: 'compressed',
}).on('error', sass.logError)
)
.pipe(autoprefixer(AUTOPREFIXER_BROWSERS))
.pipe(template({
'APP_CDN': app_cdn,
}))
.pipe(gulp.dest('./.styles'))
.pipe(
template({
APP_CDN: app_cdn,
})
)
.pipe(gulp.dest(join(__dirname, '.styles')))
.on('end', () => {
gulp.src('./.styles/stylesheets/main.css')
.pipe(gulp.dest('./src'))
gulp
.src(join(__dirname, '.styles', 'stylesheets', 'main.css'))
.pipe(gulp.dest(join(__dirname, 'src')))
.on('end', done);
});
});
// --------------
// i18n
gulp.task('extract.i18n', require(join(__dirname, 'tasks', 'extract.i18n.xlf'))(gulp));
gulp.task('import.i18n', require(join(__dirname, 'tasks', 'import.i18n.xlf'))(gulp));
gulp.task(
'extract.i18n',
require(join(__dirname, 'tasks', 'extract.i18n.xlf'))(gulp)
);
gulp.task(
'import.i18n',
require(join(__dirname, 'tasks', 'import.i18n.xlf'))(gulp)
);
This diff is collapsed.
......@@ -8,11 +8,12 @@
"preinstall": "git config core.hooksPath .git/hooks/",
"prebuild": "gulp build.sass",
"build": "sh build/base-locale.sh dist",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost",
"build-dev": "npm run build:dev",
"serve-dev": "npm run server:ssr",
"build:dev": "ng build --output-path dist --deploy-url=/ --watch=true --poll=800",
"serve:dev": "ng serve --host=0.0.0.0 --deploy-url=/ --configuration=hmr --hmr --poll=800 --progress --disableHostCheck=true",
"build-dev": "echo 'Deprecated, please use build:dev'; npm run build:dev --",
"serve-dev": "echo 'Deprecated, please use serve:ssr'; npm run serve:ssr --",
"prebuild:dev": "gulp build.sass --deploy-url=http://localhost:8080/",
"build:dev": "ng build --output-path=dist/en --deploy-url=http://localhost:8080/ --watch=true --poll=800 --aot",
"preserve:dev": "gulp build.sass --deploy-url=http://localhost:4200/",
"serve:dev": "ng serve --deploy-url=http://localhost:4200/ --watch=true --poll=800 --aot --progress --proxy-config proxy.conf.js --host=0.0.0.0 --disableHostCheck=true",
"test": "ng test",
"lint": "ng lint",
"e2e": "cypress run --debug",
......@@ -22,6 +23,7 @@
"serve:ssr": "node dist/server",
"build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
"build:client-and-server-bundles": "npm run build && ng run minds:server:production",
"build:ssr:dev": "ng run minds:server:production && npm run compile:server",
"bundle-report": "webpack-bundle-analyzer dist/en/stats.json"
},
"private": true,
......@@ -110,7 +112,8 @@
"tslint": "~5.12.0",
"typescript": "~3.4.5",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10"
"webpack-cli": "^3.3.10",
"nodemon": "^2.0.2"
},
"husky": {
"hooks": {
......
const engineSecure = Boolean(parseInt(process.env['ENGINE_SECURE']) || 0);
const engineHost = process.env['ENGINE_HOST'] || 'localhost';
const enginePort = process.env['ENGINE_PORT'] || (engineSecure ? 443 : 80);
const PROXY_CONFIG = [
{
context: [
'/api',
'/fs',
'/icon',
'/carousel',
],
target: {
protocol: engineSecure ? 'https:' : 'http:',
host: engineHost,
port: enginePort,
},
secure: false,
changeOrigin: true,
withCredentials: true,
logLevel: process.env['PROXY_LOG_LEVEL'] || 'info',
}
];
module.exports = PROXY_CONFIG;
......@@ -103,7 +103,10 @@ app.get('/undefined', (req, res) => {
// cache
const NodeCache = require('node-cache');
const myCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 120 });
const myCache = new NodeCache({
stdTTL: 2 * 60, // 2 minute cache
checkperiod: 60, // Check every minute
});
const cache = () => {
return (req, res, next) => {
......@@ -112,18 +115,18 @@ const cache = () => {
.filter(kv => kv[0] !== 'mwa' && kv[0] !== 'XSRF-TOKEN')
.join(':') || 'loggedout';
const key =
`__express__/${sessKey}/` +
`__express__/${req.headers.host}/${sessKey}/` +
(req.originalUrl || req.url) +
(isMobileOrTablet() ? '/mobile' : '/desktop');
const exists = myCache.has(key);
if (exists) {
console.log(`from cache: ${key}`);
const cachedBody = myCache.get(key);
res.send(cachedBody);
return;
} else {
res.sendResponse = res.send;
res.send = body => {
if (res.finished) return;
myCache.set(key, body);
res.sendResponse(body);
};
......@@ -132,6 +135,10 @@ const cache = () => {
};
};
app.get('/node-cache-stats', (req, res) => {
res.send(myCache.getStats());
});
// All regular routes use the Universal engine
app.get('*', cache(), (req, res) => {
const http =
......@@ -198,3 +205,5 @@ app.get('*', cache(), (req, res) => {
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
app.keepAliveTimeout = 65000;
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
import { AnalyticsModuleLazyRoutes } from './modules/analytics/analytics.lazy';
import { AdminModuleLazyRoutes } from './modules/admin/admin.lazy';
import { Pages } from './controllers/pages/pages';
import { ChannelContainerComponent } from './modules/channel-container/channel-container.component';
import { CanDeactivateGuardService } from './services/can-deactivate-guard';
const routes: Routes = [
{ path: 'about', redirectTo: 'p/about' },
{ path: 'p/:page', component: Pages },
AnalyticsModuleLazyRoutes,
AdminModuleLazyRoutes,
// TODO: Find a way to move channel routes onto its own Module. They take priority and groups/blogs cannot be accessed
{ path: ':username', redirectTo: ':username/', pathMatch: 'full' },
{
path: ':username/:filter',
component: ChannelContainerComponent,
canDeactivate: [CanDeactivateGuardService],
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
onSameUrlNavigation: 'reload',
}),
],
exports: [RouterModule],
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
})
export class AppRoutingModule {}
......@@ -3,18 +3,22 @@ import { NgModule } from '@angular/core';
import { MindsModule } from './app.module';
import { Minds } from './app.component';
import * as PlotlyJS from 'plotly.js/dist/plotly-basic.min.js';
import { PlotlyModule } from 'angular-plotly.js';
import { CookieModule } from '@gorniv/ngx-universal';
PlotlyModule.plotlyjs = PlotlyJS;
import {
RedirectService,
BrowserRedirectService,
} from './common/services/redirect.service';
@NgModule({
imports: [MindsModule, PlotlyModule, CookieModule],
imports: [MindsModule, CookieModule],
bootstrap: [Minds],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
{ provide: 'QUERY_STRING', useValue: location.search || '' },
{
provide: RedirectService,
useClass: BrowserRedirectService,
},
],
})
export class AppBrowserModule {}
......@@ -60,6 +60,4 @@
></m-modal-signup>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"> </m-cookies-notice>
</ng-container>
......@@ -16,7 +16,6 @@ import { ScrollToTopService } from './services/scroll-to-top.service';
import { ContextService } from './services/context.service';
import { Web3WalletService } from './modules/blockchain/web3-wallet.service';
import { Client } from './services/api/client';
import { WebtorrentService } from './modules/webtorrent/webtorrent.service';
import { ActivatedRoute, NavigationEnd, Router, Route } from '@angular/router';
import { ChannelOnboardingService } from './modules/onboarding/channel/onboarding.service';
import { BlockListService } from './common/services/block-list.service';
......@@ -61,7 +60,6 @@ export class Minds {
public context: ContextService,
public web3Wallet: Web3WalletService,
public client: Client,
public webtorrent: WebtorrentService,
public onboardingService: ChannelOnboardingService,
public router: Router,
public blockListService: BlockListService,
......@@ -174,8 +172,6 @@ export class Minds {
this.web3Wallet.setUp();
this.webtorrent.setUp();
this.themeService.setUp();
this.socketsService.setUp();
......
......@@ -18,16 +18,7 @@ import { CaptchaModule } from './modules/captcha/captcha.module';
import { Minds } from './app.component';
import {
MINDS_APP_ROUTING_DECLARATIONS,
MindsAppRoutes,
MindsAppRoutingProviders,
} from './router/app';
import { MINDS_DECLARATIONS } from './declarations';
import { MINDS_PLUGIN_DECLARATIONS } from './plugin-declarations';
import { MINDS_PROVIDERS } from './services/providers';
import { MINDS_PLUGIN_PROVIDERS } from './plugin-providers';
import { CommonModule } from './common/common.module';
import { MonetizationModule } from './modules/monetization/monetization.module';
......@@ -71,7 +62,6 @@ import { MobileModule } from './modules/mobile/mobile.module';
import { IssuesModule } from './modules/issues/issues.module';
import { CanaryModule } from './modules/canary/canary.module';
import { HttpClientModule } from '@angular/common/http';
import { AnalyticsModule } from './modules/analytics/analytics.module';
import { ProModule } from './modules/pro/pro.module';
import { ChannelContainerModule } from './modules/channel-container/channel-container.module';
import { UpgradesModule } from './modules/upgrades/upgrades.module';
......@@ -81,6 +71,8 @@ import { CookieModule } from '@gorniv/ngx-universal';
import { HomepageModule } from './modules/homepage/homepage.module';
import { OnboardingV2Module } from './modules/onboarding-v2/onboarding.module';
import { ConfigsService } from './common/services/configs.service';
import { AppRoutingModule } from './app-routing.module';
import { Pages } from './controllers/pages/pages';
@Injectable()
export class SentryErrorHandler implements ErrorHandler {
......@@ -94,12 +86,7 @@ export class SentryErrorHandler implements ErrorHandler {
@NgModule({
bootstrap: [Minds],
declarations: [
Minds,
MINDS_APP_ROUTING_DECLARATIONS,
MINDS_DECLARATIONS,
MINDS_PLUGIN_DECLARATIONS,
],
declarations: [Minds, Pages],
imports: [
BrowserModule.withServerTransition({ appId: 'm-app' }),
BrowserTransferStateModule,
......@@ -109,14 +96,9 @@ export class SentryErrorHandler implements ErrorHandler {
ReactiveFormsModule,
FormsModule,
HttpClientModule,
RouterModule.forRoot(MindsAppRoutes, {
// initialNavigation: 'enabled',
onSameUrlNavigation: 'reload',
}),
CaptchaModule,
CommonModule,
ProModule, // NOTE: Pro Module should be declared _BEFORE_ anything else
AnalyticsModule,
WalletModule,
//CheckoutModule,
MonetizationModule,
......@@ -158,16 +140,14 @@ export class SentryErrorHandler implements ErrorHandler {
CanaryModule,
ChannelsModule,
UpgradesModule,
//PlotlyModule,
//last due to :username route
AppRoutingModule,
ChannelContainerModule,
],
providers: [
{ provide: ErrorHandler, useClass: SentryErrorHandler },
MindsAppRoutingProviders,
MINDS_PROVIDERS,
MINDS_PLUGIN_PROVIDERS,
{
provide: APP_INITIALIZER,
useFactory: configs => () => configs.loadFromRemote(),
......
......@@ -9,6 +9,10 @@ import { MindsModule } from './app.module';
import { Minds } from './app.component';
import { PlotlyModule } from 'angular-plotly.js';
import { CookieService, CookieBackendService } from '@gorniv/ngx-universal';
import {
ServerRedirectService,
RedirectService,
} from './common/services/redirect.service';
PlotlyModule.plotlyjs = {
plot: () => {
......@@ -38,6 +42,10 @@ export class ServerXhr implements XhrFactory {
provide: CookieService,
useClass: CookieBackendService,
},
{
provide: RedirectService,
useClass: ServerRedirectService,
},
],
bootstrap: [Minds],
})
......
......@@ -118,9 +118,6 @@ import { MarketingFooterComponent } from './components/marketing/footer.componen
import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
//import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { PageLayoutComponent } from './components/page-layout/page-layout.component';
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
......@@ -144,6 +141,7 @@ import { MediaProxyService } from './services/media-proxy.service';
import { HorizontalFeedService } from './services/horizontal-feed.service';
import { FormInputCheckboxComponent } from './components/forms/checkbox/checkbox.component';
import { AttachmentPasteDirective } from './directives/paste/attachment-paste.directive';
import { RedirectService } from './services/redirect.service';
const routes: Routes = [
{
......@@ -159,7 +157,6 @@ const routes: Routes = [
RouterModule,
FormsModule,
ReactiveFormsModule,
PlotlyModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
RouterModule.forChild(routes),
......@@ -259,7 +256,6 @@ const routes: Routes = [
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
......@@ -363,7 +359,6 @@ const routes: Routes = [
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
......@@ -432,28 +427,16 @@ const routes: Routes = [
},
{
provide: ConfigsService,
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
},
{
provide: MetaService,
useFactory: (
titleService,
metaService,
siteService,
location,
configsService
) =>
new MetaService(
titleService,
metaService,
siteService,
location,
configsService
useFactory: (client, injector, redirect, location) =>
new ConfigsService(
client,
injector.get('QUERY_STRING'),
redirect,
location
),
deps: [Title, Meta, SiteService, Location, ConfigsService],
deps: [Client, Injector, RedirectService, Location],
},
MetaService,
MediaProxyService,
V2TopbarService,
{
......
......@@ -39,6 +39,7 @@ describe('ChannelModeSelector', () => {
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
nsfw: [],
};
clientMock.response['api/v1/channel/info'] = { status: 'success' };
......
......@@ -2,27 +2,32 @@ 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;
.m-dateDropdowns__selectWrapper {
position: relative;
overflow: hidden;
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;
border-radius: 3px;
margin: 0 10px 0 0;
@include m-theme() {
color: themed($m-grey-800);
background-color: themed($m-white);
border: 1px solid #e2e2e2;
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.1);
}
&::after {
content: '\25bc';
font-size: 10px;
padding: 10px;
position: absolute;
right: 0;
top: 0;
text-align: center;
pointer-events: none;
@include m-theme() {
color: themed($m-grey-200);
}
}
// month
......@@ -40,4 +45,28 @@ m-date__dropdowns {
min-width: 77px;
}
}
select {
display: inline-block;
background-color: #fff;
box-sizing: border-box;
padding: 8px 10px;
height: 36px;
width: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 16px;
line-height: 21px;
border-radius: 2px;
appearance: none;
@include m-theme() {
color: themed($m-grey-800);
background-color: themed($m-white);
border: 1px solid #e2e2e2;
}
}
}
......@@ -3,27 +3,33 @@ 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>
<div class="m-dateDropdowns__selectWrapper">
<select
data-minds="monthDropdown"
[ngModel]="selectedMonth"
(ngModelChange)="selectMonth($event)"
>
<option *ngFor="let month of monthNames">{{ month }}</option>
</select>
</div>
<div class="m-dateDropdowns__selectWrapper">
<select
data-minds="dayDropdown"
[ngModel]="selectedDay"
(ngModelChange)="selectDay($event)"
>
<option *ngFor="let day of days">{{ day }}</option>
</select>
</div>
<div class="m-dateDropdowns__selectWrapper">
<select
data-minds="yearDropdown"
[ngModel]="selectedYear"
(ngModelChange)="selectYear($event)"
>
<option *ngFor="let year of years">{{ year }}</option>
</select>
</div>
`,
})
export class DateDropdownsComponent implements OnInit {
......@@ -56,30 +62,42 @@ export class DateDropdownsComponent implements OnInit {
ngOnInit() {
this.years = this.range(100, this.selectedYear, false);
this.selectedYear = this.years[0];
this.selectMonth('January');
this.selectMonth('January', false);
}
selectMonth(month: string) {
selectMonth(month: string, emit: boolean = true) {
this.selectedMonth = month;
this.populateDays(
this.getDaysInMonth(this.getMonthNumber(month), this.selectedYear)
);
this.selectedDateChange.emit(this.buildDate());
if (emit) {
this.emitChanges();
}
}
selectDay(day: string) {
selectDay(day: string, emit: boolean = true) {
this.selectedDay = day;
this.selectedDateChange.emit(this.buildDate());
if (emit) {
this.emitChanges();
}
}
selectYear(year) {
selectYear(year, emit: boolean = true) {
this.selectedYear = year;
this.populateDays(
this.getDaysInMonth(this.getMonthNumber(this.selectedMonth), year)
);
if (emit) {
this.emitChanges();
}
}
emitChanges() {
this.selectedDateChange.emit(this.buildDate());
}
......
......@@ -54,8 +54,8 @@
</li>
<li>
<a href="https://irl.minds.com/" target="_blank" i18n>
Minds IRL
<a href="https://change.minds.com/" target="_blank" i18n>
Events
</a>
</li>
</ul>
......
......@@ -9,7 +9,7 @@
[ngClass]="{ 'm-inline-embed': hasInlineContentLoaded() }"
(click)="action($event)"
target="_blank"
rel="noopener noreferrer"
rel="noopener nofollow ugc"
*ngIf="src.thumbnail_src || inlineEmbed"
>
<div
......
......@@ -56,7 +56,6 @@
}
.m-v2-topbar__Container__LoginWrapper > a {
margin-right: 40px;
@include m-theme() {
background: transparent;
border: 1px solid themed($m-black-always);
......@@ -330,9 +329,7 @@
> a.m-v2-topbarLoginWrapper__login {
padding: 0;
border: none !important;
@media screen and(max-width: $max-mobile) {
margin-right: 10px;
}
margin-right: $minds-margin * 2;
}
> a.m-v2-topbarLoginWrapper__joinMindsNow {
......
......@@ -20,13 +20,13 @@ export class TagsPipe implements PipeTransform {
url: {
rule: /(\b(https?|ftp|file):\/\/[^\s\]]+)/gim,
replace: m => {
return `<a href="${m.match[1]}" target="_blank" rel="noopener noreferrer">${m.match[1]}</a>`;
return `<a href="${m.match[1]}" target="_blank" rel="noopener nofollow ugc">${m.match[1]}</a>`;
},
},
mail: {
rule: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/gim,
replace: m => {
return `<a href="mailto:${m.match[0]}" target="_blank" rel="noopener noreferrer">${m.match[0]}</a>`;
return `<a href="mailto:${m.match[0]}" target="_blank" rel="noopener nofollow ugc">${m.match[0]}</a>`;
},
},
hash: {
......
import { Client } from '../api/client.service';
import { Injectable, Inject, Optional } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { Injectable, Inject, Optional, Injector } from '@angular/core';
import { RedirectService } from './redirect.service';
import { Location } from '@angular/common';
@Injectable()
export class ConfigsService {
......@@ -9,7 +9,9 @@ export class ConfigsService {
constructor(
private client: Client,
@Inject('QUERY_STRING') private queryString: string
@Inject('QUERY_STRING') private queryString: string,
private redirectService: RedirectService,
private location: Location
) {}
async loadFromRemote() {
......@@ -17,6 +19,7 @@ export class ConfigsService {
this.configs = await this.client.get(
`api/v1/minds/config${this.queryString}`
);
this.redirectToRootIfInvalidDomain();
} catch (err) {
console.error(err);
}
......@@ -29,4 +32,17 @@ export class ConfigsService {
set(key, value): void {
this.configs[key] = value;
}
/**
* Redirect to the root domain if we have an invalid domain response from configs
* @return void
*/
private redirectToRootIfInvalidDomain(): void {
if (this.get('redirect_to_root_on_init') === true) {
const redirectTo: string =
this.get('site_url') + this.location.path().substr(1);
this.redirectService.redirect(redirectTo);
throw `Invalid domain. Redirecting to ${redirectTo}`;
}
}
}
import { Injectable, Optional } from '@angular/core';
import { Injectable, Optional, Inject } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { SiteService } from './site.service';
import { Location } from '@angular/common';
import { ConfigsService } from './configs.service';
import { DOCUMENT } from '@angular/common';
const DEFAULT_META_TITLE = 'Minds';
const DEFAULT_META_DESCRIPTION = '...';
export const MIN_METRIC_FOR_ROBOTS = 5;
@Injectable()
export class MetaService {
......@@ -18,7 +20,8 @@ export class MetaService {
private metaService: Meta,
private site: SiteService,
private location: Location,
private configs: ConfigsService
private configs: ConfigsService,
@Inject(DOCUMENT) private dom
) {
this.reset();
}
......@@ -29,6 +32,12 @@ export class MetaService {
? this.site.title + ' - ' + this.site.oneLineHeadline
: DEFAULT_META_TITLE;
value = this.stripHtml(value);
if (value.length > 60) {
value = value.substr(0, 57) + '...';
}
if (value && join) {
title = [value, defaultTitle]
.filter(fragment => Boolean(fragment))
......@@ -38,12 +47,17 @@ export class MetaService {
} else {
title = defaultTitle;
}
this.title = title;
this.applyTitle();
return this;
}
setDescription(value: string): MetaService {
value = this.stripHtml(value);
if (value.length > 160) {
value = value.substr(0, 157) + '...';
}
this.metaService.updateTag({ name: 'description', content: value });
return this;
}
......@@ -54,6 +68,33 @@ export class MetaService {
return this;
}
setCanonicalUrl(value: string): MetaService {
// Find and clear or canonical links
const links: HTMLLinkElement[] = this.dom.head.querySelectorAll(
'[rel="canonical"]'
);
if (links.length) {
for (const link of links) {
this.dom.head.removeChild(link);
}
}
if (value) {
// TODO: fix duplicated code with ogUrl here...
if (value && value.indexOf('/') === 0) {
// Relative path
value = this.site.baseUrl + value.substr(1);
}
let link: HTMLLinkElement;
link = this.dom.createElement('link');
link.setAttribute('rel', 'canonical');
link.setAttribute('href', value);
this.dom.head.appendChild(link);
}
return this;
}
setOgUrl(value: string): MetaService {
if (value && value.indexOf('/') === 0) {
// Relative path
......@@ -100,11 +141,24 @@ export class MetaService {
return this;
}
setLanguage(language: string): MetaService {
return this;
}
setRobots(value: string): MetaService {
this.metaService.updateTag({ name: 'robots', content: value });
return this;
}
setNsfw(value: boolean): MetaService {
if (value) {
this.metaService.updateTag({ name: 'rating', content: 'adult' });
} else {
this.metaService.removeTag("name='rating'");
}
return this;
}
reset(
data: {
title?: string;
......@@ -119,7 +173,9 @@ export class MetaService {
.setOgType('website')
.setOgUrl(data.ogUrl || this.location.path())
.setOgImage(data.ogImage || null, { width: 0, height: 0 })
.setRobots(data.robots || 'all');
.setCanonicalUrl('') // Only user canonical when required
.setRobots(data.robots || 'all')
.setNsfw(false);
}
private applyTitle(): void {
......@@ -133,4 +189,16 @@ export class MetaService {
content: this.title,
});
}
/**
* Removes any html found and returns on text
* @param value
* @return string
*/
private stripHtml(value: string): string {
if (!value) return '';
const fakeEl = this.dom.createElement('span');
fakeEl.innerHTML = value;
return fakeEl.textContent || fakeEl.innerText;
}
}
import { Inject, Injectable } from '@angular/core';
import { RESPONSE } from '@nguniversal/express-engine/tokens';
export class RedirectService {
public redirect(url: string): void {}
}
export class BrowserRedirectService extends RedirectService {
redirect(url: string): void {
window.location.href = url;
}
}
export class ServerRedirectService extends RedirectService {
constructor(@Inject(RESPONSE) private res) {
super();
}
redirect(url: string, permanent: boolean = false): void {
const code = permanent ? 301 : 302;
this.res.redirect(code, url);
this.res.end();
}
}
<div
class="drag-animation mdl-color--blue-grey-600 mdl-color-text--blue-grey-50"
[hidden]="!dragging"
>
<div class="drop">
<i class="material-icons">file_upload</i>
<p i18n="@@MINDS__CAPTURE__DROP_AREA">Drop your files here</p>
</div>
</div>
<div class="mdl-grid capture-grid" style="max-width:900px">
<div class="mdl-cell mdl-cell--4-col">
<div class="mdl-card m-albums-selector" #toggle>
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">
<ng-container i18n="@@MINDS__CAPTURE__ALBUM_LABEL"
>Album</ng-container
>
</h2>
<button
class="mdl-button mdl-button--fab mdl-button--colored m-album-add"
(click)="toggle.value = !toggle.value"
>
<i class="material-icons">add</i>
</button>
</div>
<div
class="mdl-card m-album m-album-create mdl-color--blue-grey-500"
*ngIf="toggle.value"
>
<div class="mdl-card__title">
<input type="text" #newalbum />
<button
class="mdl-button mdl-button--colored mdl-color-text--white"
(click)="createAlbum(newalbum); toggle.value = false"
i18n="@@M__ACTION__CREATE"
>
Create
</button>
</div>
</div>
<div
class="mdl-progress mdl-js-progress mdl-progress__indeterminate"
[hidden]="albums.length > 0 && !inProgress"
[mdl]
></div>
<div
*ngFor="let album of albums"
class="mdl-card m-album mdl-color--blue-grey-50 mdl-color-text--blue-grey-500"
[ngClass]="{'mdl-color--blue-grey-500': postMeta.album_guid == album.guid, 'mdl-color-text--blue-grey-50': postMeta.album_guid == album.guid}"
(click)="selectAlbum(album)"
>
<div
class="mdl-card__title"
[ngClass]="{'mdl-color-text--blue-grey-50': postMeta.album_guid == album.guid}"
>
<h2>{{album.title}}</h2>
</div>
<div class="mdl-card__menu">
<i class="material-icons" (click)="deleteAlbum(album)">delete</i>
</div>
</div>
</div>
</div>
<!-- Upload output -->
<div class="mdl-cell mdl-cell--8-col">
<form class="mdl-card">
<div class="mdl-card__actions" style="display:flex;">
<div class="upload-button">
<button class="mdl-button mdl-button--raised">
<i class="material-icons">attachment</i>
<span i18n="@@MINDS__CAPTURE__ADD_FILE_ACTION">Add file</span>
</button>
<input
type="file"
id="file"
#file
(change)="add(file); file.value = '';"
multiple
accept="image/*"
/>
</div>
<div class="mdl-layout-spacer"></div>
<a
class="mdl-color-text--blue-grey-300 m-capture-default-maturity"
(click)="default_maturity = default_maturity ? 0 : 1"
>
<i
class="material-icons"
[ngClass]="{ 'mdl-color-text--red-500': default_maturity }"
title="Mature content"
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
<span
*ngIf="default_maturity"
class="mdl-color-text--red-500"
i18n="@@M__COMMON__MATURE_CONTENT"
>Mature content</span
>
</a>
<select
name="defaultLicense"
[(ngModel)]="default_license"
class="mdl-color-text--blue-grey-300 m-form-select"
>
<option *ngFor="let l of licenses" [value]="l.value"
>{{l.text}}</option
>
</select>
<button
class="mdl-button mdl-button--raised mdl-button--colored m-capture-save-to-album-button"
(click)="publish()"
[disabled]="!uploads"
>
<span i18n="@@MINDS__CAPTURE__SAVE_TO_ALBUM">Save to album</span>
</button>
</div>
</form>
<div
class="mdl-card mdl-shadow--2dp m-upload"
*ngFor="let upload of uploads; let i = index"
>
<div class="mdl-card__title m-capture-edit-container">
<input
type="text"
name="title"
[(ngModel)]="upload.title"
(change)="modify(i)"
/>
<a
class="mdl-color-text--blue-grey-300 m-capture-mature"
(click)="upload.mature = upload.mature ? 0 : 1"
>
<i
class="material-icons"
[ngClass]="{ 'mdl-color-text--red-500': upload.mature }"
title="Mature content"
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
</a>
<select
name="license"
[(ngModel)]="upload.license"
(change)="modify(i)"
class="mdl-color-text--blue-grey-300 m-form-select"
>
<option *ngFor="let l of licenses" [value]="l.value"
>{{l.text}}</option
>
</select>
</div>
<div
class="mdl-progress mdl-js-progress"
[mdlUpload]
[progress]="upload.progress"
[ngClass]="{'failed': upload.state == 'failed', 'complete': upload.state == 'complete'}"
></div>
</div>
<div class="m-splash">
<i class="material-icons mdl-color-text--blue-grey-400">file_upload</i>
<h3
class="mdl-color-text--blue-grey-300"
i18n="@@MINDS__CAPTURE__DRAG_TO_UPLOAD"
>
Drag to upload
</h3>
</div>
</div>
</div>
@import 'defaults';
minds-capture {
height: calc(100vh - 80px);
display: block;
}
.drag-animation {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
-webkit-align-items: center;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.9;
z-index: 100;
animation: fade-animation 2s infinite alternate;
.drop {
display: flex;
flex-wrap: wrap;
text-align: center;
margin: auto;
width: 50%;
height: 50%;
i {
font-size: 12em;
width: 100%;
}
p {
width: 100%;
}
}
}
@keyframes fade-animation {
from {
opacity: 0.9;
}
to {
opacity: 0.8;
}
}
.capture-grid {
.mdl-card {
min-height: 0;
width: 100%;
}
.m-capture-default-maturity {
cursor: pointer;
padding-top: $minds-padding;
i {
cursor: pointer;
vertical-align: middle;
}
span {
font-size: 11px;
text-transform: uppercase;
}
}
.m-albums-selector {
.mdl-button.m-album-add {
width: 24px;
min-width: 0;
height: 24px;
margin-left: $minds-margin;
i {
font-size: 12px;
}
}
.m-album {
cursor: pointer;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-100);
}
h2 {
margin: 0;
font-size: 14px;
line-height: 16px;
}
.mdl-card__menu {
visibility: hidden;
}
&:hover {
.mdl-card__menu {
visibility: visible;
}
}
}
.m-album-create {
input {
outline: none;
border: 0;
font-size: 14px;
@include m-theme() {
color: themed($m-white);
background-color: rgba(themed($m-white), 0.2);
border-right: 1px solid themed($m-grey-100);
}
}
button {
float: left;
margin-left: $minds-margin;
}
}
}
.upload-button {
display: inline-block;
vertical-align: middle;
position: relative;
cursor: pointer;
button {
cursor: pointer;
}
input {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
cursor: pointer;
opacity: 0;
@include m-theme() {
color: themed($m-grey-400);
}
}
input::-webkit-file-upload-button {
cursor: pointer;
}
}
.m-upload {
input {
outline: none;
border: 0;
font-size: 14px;
@include m-theme() {
border-right: 1px solid themed($m-grey-100);
}
}
.mdl-progress {
width: 100%;
}
}
.m-splash {
text-align: center;
padding: 10vh;
i {
font-size: 152px;
text-align: center;
width: 100%;
}
h3 {
text-transform: uppercase;
font-weight: 100;
}
}
}
.m-capture-edit-container {
input {
flex: 2;
}
select {
width: auto;
flex: 1;
}
.m-capture-mature {
margin-left: $minds-padding;
cursor: pointer;
i {
cursor: pointer;
vertical-align: middle;
}
span {
font-size: 11px;
text-transform: uppercase;
}
}
}
.m-capture-save-to-album-button {
margin-left: 8px;
}
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LICENSES, ACCESS } from '../../services/list-options';
import { Session } from '../../services/session';
import { Upload } from '../../services/api/upload';
import { Client } from '../../services/api/client';
@Component({
selector: 'minds-capture',
host: {
'(dragover)': 'dragover($event)',
'(dragleave)': 'dragleave($event)',
'(drop)': 'drop($event)',
},
templateUrl: 'capture.html',
})
export class Capture {
uploads: Array<any> = [];
postMeta: any = {}; //TODO: make this object
albums: Array<any> = [];
offset: string = '';
inProgress: boolean = false;
dragging: boolean = false;
control;
default_maturity: number = 0;
default_license: string = 'all-rights-reserved';
licenses = LICENSES;
access = ACCESS;
constructor(
public session: Session,
public _upload: Upload,
public client: Client,
public router: Router
) {}
ngOnInit() {
if (!this.session.isLoggedIn()) {
this.router.navigate(['/login']);
} else {
this.getAlbums();
}
}
getAlbums() {
var self = this;
this.client
.get('api/v1/media/albums/list', { limit: 5, offset: this.offset })
.then((response: any) => {
if (!response.entities) return;
console.log(response);
self.albums = response.entities;
});
}
createAlbum(album) {
var self = this;
this.inProgress = true;
this.client
.post('api/v1/media/albums', { title: album.value })
.then((response: any) => {
self.albums.unshift(response.album);
self.postMeta.album_guid = response.album.guid;
self.inProgress = false;
album.value = '';
});
}
selectAlbum(album) {
this.postMeta.album_guid = album.guid;
}
deleteAlbum(album) {
if (confirm('Are you sure?')) {
let i: any;
for (i in this.albums) {
if (album.guid === this.albums[i].guid) this.albums.splice(i, 1);
}
this.client.delete('api/v1/media/albums/' + album.guid);
}
}
/**
* Add a file to the upload queue
*/
add(file: any) {
var self = this;
for (var i = 0; i < file.files.length; i++) {
var data: any = {
guid: null,
state: 'created',
progress: 0,
license: this.default_license || 'all-rights-reserved',
mature: this.default_maturity || 0,
};
var fileInfo = file.files[i];
if (fileInfo.type && fileInfo.type.indexOf('image') > -1) {
data.type = 'image';
} else if (fileInfo.type && fileInfo.type.indexOf('video') > -1) {
data.type = 'video';
} else if (fileInfo.type && fileInfo.type.indexOf('audio') > -1) {
data.type = 'audio';
} else {
data.type = 'unknown';
}
data.name = fileInfo.name;
data.title = data.name;
var upload_i = this.uploads.push(data) - 1;
this.uploads[upload_i].index = upload_i;
this.upload(this.uploads[upload_i], fileInfo);
}
}
upload(data, fileInfo) {
var self = this;
this._upload
.post('api/v1/media', [fileInfo], this.uploads[data.index], progress => {
self.uploads[data.index].progress = progress;
if (progress === 100) {
self.uploads[data.index].state = 'uploaded';
}
})
.then((response: any) => {
self.uploads[data.index].guid = response.guid;
self.uploads[data.index].state = 'complete';
self.uploads[data.index].progress = 100;
})
.catch(function(e) {
self.uploads[data.index].state = 'failed';
console.error(e);
});
}
modify(index) {
this.uploads[index].state = 'uploaded';
//we don't always have a guid ready, so keep checking for one
var promise = new Promise((resolve, reject) => {
if (this.uploads[index].guid) {
setTimeout(() => {
resolve();
}, 300);
return;
}
var interval = setInterval(() => {
if (this.uploads[index].guid) {
resolve();
clearInterval(interval);
}
}, 1000);
});
promise.then(() => {
this.client
.post('api/v1/media/' + this.uploads[index].guid, this.uploads[index])
.then((response: any) => {
console.log('response from modify', response);
this.uploads[index].state = 'complete';
});
});
}
/**
* Publish our uploads to an album
*/
publish() {
if (!this.postMeta.album_guid)
return alert('You must select an album first');
var self = this;
var guids = this.uploads.map(upload => {
if (upload.guid !== null || upload.guid !== 'null' || !upload.guid)
return upload.guid;
});
this.client
.post('api/v1/media/albums/' + this.postMeta.album_guid, { guids: guids })
.then((response: any) => {
self.router.navigate(['/media', this.postMeta.album_guid]);
})
.catch(e => {
alert('there was a problem.');
});
}
/**
* Make sure the browser doesn't freak
*/
dragover(e) {
e.preventDefault();
this.dragging = true;
}
/**
* Tell the app we have stopped dragging
*/
dragleave(e) {
e.preventDefault();
console.log(e);
if (e.layerX < 0) this.dragging = false;
}
drop(e) {
e.preventDefault();
this.dragging = false;
this.add(e.dataTransfer);
}
}
<div class="mdl-tabs__tab-bar">
<a
[routerLink]="['/discovery', _filter, 'albums']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'albums' || _type == 'all'}"
[hidden]="_filter != 'owner'"
i18n="@@MINDS__DISCOVERY__ALBUMS_TAB"
>Albums</a
>
<a
[routerLink]="['/discovery', _filter, 'videos']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'videos' || _type == 'all'}"
i18n="@@M__FEATURE__VIDEO_PLURAL"
>Videos</a
>
<a
[routerLink]="['/discovery', _filter, 'images']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'images' || _type == 'all'}"
i18n="@@M__FEATURE__IMAGE_PLURAL"
>Images</a
>
<a
[routerLink]="['/discovery', _filter, 'channels']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'channels' || _type == 'all'}"
[hidden]="_filter == 'owner' || _owner"
i18n="@@M__FEATURE__CHANNEL__PLURAL"
>Channels</a
>
</div>
<div
class="mdl-grid m-discovery-{{_filter}} m-discovery-{{_filter}}-{{_type}}"
style="max-width:900px"
>
<a
class="mdl-cell mdl-cell--4-col"
[routerLink]="['/capture']"
style="text-decoration: none;"
[hidden]="_type == 'channels' || _filter == 'suggested'"
>
<div
class="minds-add-card mdl-card mdl-shadow--2dp mdl-color--blue-grey-400 mdl-color-text--blue-grey-100"
>
<i class="material-icons">file_upload</i>
<h3 i18n="@@M__ACTION__UPLOAD">Upload</h3>
</div>
</a>
<div
*ngIf="_filter == 'suggested' && _type == 'channels'"
class="m-discovery-suggested-location-bar"
>
<div class="m-discovery-suggested-location-inner">
<span
*ngIf="!nearby && hasNearby"
(click)="setNearby(true)"
class="m-discovery-suggested-location-toggle mdl-color--blue-grey-400 mdl-color-text--white"
i18n="@@MINDS__DISCOVERY__LOCATION_OFF_TOGGLE"
>Location off</span
>
<span
*ngIf="!nearby && !hasNearby"
(click)="setNearby(true)"
class="m-discovery-suggested-location-toggle mdl-color--grey-400 mdl-color-text--white"
i18n="@@MINDS__DISCOVERY__NO_RESULTS_LABEL"
>No results</span
>
<span
*ngIf="nearby"
(click)="setNearby(false)"
class="m-discovery-suggested-location-toggle mdl-color--blue-grey-400 mdl-color-text--white"
i18n="@@MINDS__DISCOVERY__LOCATING_LABEL"
>Locating</span
>
<!-- only show if locating is on -->
<span
*ngIf="nearby || (!nearby && !hasNearby)"
class="m-discovery-suggested-location-options"
>
<select
(change)="distance = dist.value; load(true)"
#dist
class="mdl-color-text--blue-grey-600"
>
<option value="5"
>5
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
<option value="25"
>25
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
<option value="50"
>50
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
<option value="100"
>100
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
</select>
<m-18n i18n="Distance from a city@@MINDS__DISCOVERY__OF_WORD">of</m-18n>
<input
(keyup)="findCity(city)"
name="city"
[(ngModel)]="city"
class="mdl-color--blue-grey-300 mdl-color-text--white"
placeholder="Enter your city..."
i18n-placeholder="@@M__COMMON__ENTER_CITY"
/>
</span>
<div
class="m-discovery-cities mdl-card mdl-shadow--4dp"
*ngIf="cities.length > 0"
>
<p
class="mdl-color-text--blue-grey-300"
i18n="@@M__COMMON__SELECT_CITY"
>
Select your city:
</p>
<li
(click)="setCity(c)"
*ngFor="let c of cities"
[hidden]="!(c.address.town || c.address.city)"
>
{{c.address.town}}{{c.address.city}}, {{c.address.state}}
</li>
</div>
</div>
</div>
<div
class="mdl-cell mdl-cell--4-col m-discovery-wrapper"
*ngFor="let entity of entities; let i = index"
[hidden]="i != 0 && _filter == 'suggested'"
>
<minds-card-video
[object]="entity"
*ngIf="entity.subtype == 'video'"
class="mdl-card mdl-shadow--2dp"
[ngClass]="{'mdl-shadow--6dp': _filter == 'suggested'}"
></minds-card-video>
<minds-card-image
[object]="entity"
*ngIf="entity.subtype == 'image'"
class="mdl-card mdl-shadow--2dp"
[ngClass]="{'mdl-shadow--6dp': _filter == 'suggested'}"
></minds-card-image>
<minds-card-album
[object]="entity"
*ngIf="entity.subtype == 'album'"
class="mdl-card mdl-shadow--2dp"
[ngClass]="{'mdl-shadow--6dp': _filter == 'suggested'}"
></minds-card-album>
<!-- START: User Only -->
<minds-card-user
[object]="entity"
[avatarSize]="_filter == 'suggested' ? 'large' : 'medium'"
*ngIf="entity.type == 'user'"
class="mdl-card mdl-shadow--2dp"
></minds-card-user>
<!-- END: User Only -->
<div
class="m-discovery-suggested-actions mdl-card mdl-shadow--6dp mdl-color--blue-grey-900"
*ngIf="_filter == 'suggested'"
>
<span class="minds-button-edit m-button" (click)="pass(i)">
<button i18n="@@MINDS__DISCOVERY__PASS_ACTION">Pass</button>
</span>
<minds-button-subscribe
class="m-button"
[user]="entity"
(click)="pop(i)"
[hidden]="_type != 'channels'"
></minds-button-subscribe>
<minds-button-thumbs-up
class="m-button"
[object]="entity"
(click)="pop(i)"
[hidden]="_type == 'channels'"
></minds-button-thumbs-up>
<minds-button-thumbs-down
class="m-button"
[object]="entity"
(click)="pop(i)"
[hidden]="_type == 'channels'"
></minds-button-thumbs-down>
<minds-button-remind
class="m-button"
[object]="entity"
(click)="pop(i)"
[hidden]="_type == 'channels'"
></minds-button-remind>
</div>
</div>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
*ngIf="_filter != 'suggested'"
>
</infinite-scroll>
<div
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
[hidden]="!inProgress"
*ngIf="_filter == 'suggested'"
></div>
</div>
.m-discovery-suggested {
margin-top: 16px;
max-width: 100%;
.m-discovery-wrapper {
overflow: hidden;
}
.mdl-cell {
min-width: 100%;
margin: auto;
.mdl-card {
z-index: 1;
display: block;
}
minds-card-user {
width: 400px;
margin: 0 auto;
.minds-usercard-banner {
display: none !important;
}
.avatar {
margin: 0 auto;
width: 100%;
height: 368px;
box-sizing: border-box;
text-align: center;
vertical-align: middle;
padding: 8px;
@include m-theme() {
background-color: themed($m-grey-50);
}
img {
height: 100%;
}
}
.minds-tabs {
//margin: -48px 12px 0 112px;
margin-bottom: 8px;
}
.minds-usercard-block {
display: block;
margin-top: 0;
width: 100%;
box-sizing: border-box;
.body {
h3 {
@include m-theme() {
color: themed($m-grey-950);
}
}
> span {
@include m-theme() {
color: themed($m-blue-grey-400);
}
}
}
@media screen and (min-width: 400px) {
h3 {
font-size: 32px;
line-height: normal;
}
}
}
@media screen and (max-width: 400px) {
.minds-usercard-block {
display: block;
@include m-theme() {
color: themed($m-grey-950);
}
}
.avatar {
width: 100%;
}
.body {
margin: 16px 0 0;
}
.m-usercard-bio {
margin-top: 0 !important;
}
}
.m-usercard-bio {
display: flex;
padding: 8px;
margin-top: 16px;
@include m-theme() {
color: themed($m-grey-950);
}
> div {
font-size: 12px;
//flex:1;
margin-right: 16px;
display: flex;
flex-direction: row;
align-items: center;
max-height: 40px;
overflow: hidden;
&.m-usercard-bio-brief {
align-items: flex-start;
}
> i {
vertical-align: middle;
margin-right: 8px;
}
}
}
minds-button-subscribe {
display: none;
}
}
minds-card-video,
minds-card-image {
.minds-video-thumbnail {
min-height: 300px;
background-repeat: no-repeat !important;
background-size: contain !important;
}
}
.m-action-tabs {
display: none;
}
}
.m-discovery-suggested-actions {
display: flex !important;
flex-flow: row nowrap;
align-items: center;
margin: -8px 12px;
padding: 16px 0;
z-index: 0 !important;
.m-button {
flex: 1;
text-align: center;
> * {
padding: 12px 32px;
> i {
vertical-align: middle;
}
}
}
}
.m-discovery-suggested-location-bar {
width: 100%;
margin: 0 auto 16px auto;
text-align: center;
.m-discovery-suggested-location-inner {
min-width: 50%;
margin: auto;
}
.m-discovery-suggested-location-toggle {
border-radius: 3px;
padding: 8px;
cursor: pointer;
}
.m-discovery-suggested-location-options {
select {
-webkit-appearance: none;
font-size: inherit;
font-weight: bold;
padding: 8px;
font-family: inherit;
border: 0;
}
input {
padding: 8px;
margin-left: 12px;
border: 0;
border-radius: 3px;
background: transparent;
font-size: inherit;
font-family: inherit;
color: inherit;
width: 132px;
@media screen and (max-width: 330px) {
width: 80px;
}
&::-webkit-input-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
&::-moz-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
&:-moz-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
&:-ms-input-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
}
}
.m-discovery-cities {
width: 246px;
text-align: left;
margin: 16px auto;
padding: 16px;
p {
margin: 0;
padding: 0;
font-size: 12px;
}
li {
list-style: none;
padding: 12px 0;
cursor: pointer;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
}
}
}
}
}
.m-discovery-suggested-channels {
minds-button-feature {
@include m-theme() {
background-color: themed($m-white);
}
}
.m-discovery-suggested-actions {
width: 400px !important;
padding: 24px 0 16px;
margin: -8px auto 0;
border-radius: 2px;
button {
> i.material-icons {
font-size: 20px;
}
}
.m-button-pass {
@include m-theme() {
color: themed($m-white);
border-color: themed($m-white);
background-color: themed($m-red);
}
&:hover {
@include m-theme() {
background-color: themed($m-red-dark);
}
}
}
.minds-subscribe-button {
@include m-theme() {
color: themed($m-white);
border-color: themed($m-white);
background-color: themed($m-green);
}
&:hover {
@include m-theme() {
background-color: themed($m-green-dark);
}
}
}
}
}
.m-discovery-channels-note {
max-width: 400px;
margin: 16px auto 0;
text-align: center;
}
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { ContextService } from '../../services/context.service';
@Component({
moduleId: module.id,
selector: 'minds-discovery',
templateUrl: 'discovery.html',
})
export class Discovery {
_filter: string = 'featured';
_owner: string = '';
_type: string = 'all';
entities: Array<Object> = [];
moreData: boolean = true;
offset: string | number = '';
inProgress: boolean = false;
city: string = '';
cities: Array<any> = [];
nearby: boolean = false;
hasNearby: boolean = false;
distance: number = 5;
paramsSubscription: Subscription;
searching;
constructor(
public session: Session,
public client: Client,
public router: Router,
public route: ActivatedRoute,
private context: ContextService
) {}
ngOnInit() {
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['filter']) {
this._filter = params['filter'];
switch (this._filter) {
case 'all':
break;
case 'suggested':
if (!this.session.isLoggedIn()) {
this.router.navigate(['/discovery/featured/channels']);
return;
}
this._type = 'channels';
if (this.session.getLoggedInUser().city) {
this.city = this.session.getLoggedInUser().city;
this.nearby = true;
this.hasNearby = false;
}
break;
case 'trending':
this._type = 'images';
break;
case 'featured':
this._type = 'channels';
break;
case 'owner':
break;
default:
this._owner = this._filter;
this._filter = this._filter;
}
}
if (params['type']) {
this._type = params['type'];
}
switch (this._type) {
case 'videos':
this.context.set('object:video');
break;
case 'images':
this.context.set('object:image');
break;
case 'channels':
this.context.set('user');
break;
default:
this.context.reset();
}
this.inProgress = false;
this.entities = [];
this.load(true);
});
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
load(refresh: boolean = false) {
if (this.inProgress) return false;
if (refresh) this.offset = '';
this.inProgress = true;
var filter = this._filter;
if (this._owner) filter = 'owner';
this.client
.get('api/v1/entities/' + filter + '/' + this._type + '/' + this._owner, {
limit: 24,
offset: this.offset,
skip: 0,
nearby: this.nearby,
distance: this.distance,
})
.then((data: any) => {
if (!data.entities) {
if (this.nearby) {
this.hasNearby = false;
return this.setNearby(false);
}
this.moreData = false;
this.inProgress = false;
return false;
}
if (this.nearby) {
this.hasNearby = true;
}
if (refresh) {
this.entities = data.entities;
} else {
if (this.offset && filter != 'trending') data.entities.shift();
this.entities = this.entities.concat(data.entities);
}
this.offset = data['load-next'];
this.inProgress = false;
if (!this.offset) this.moreData = false;
})
.catch(e => {
this.inProgress = false;
if (this.nearby) {
this.setNearby(false);
}
});
}
pass(index: number) {
var entity: any = this.entities[index];
this.client.post('api/v1/entities/suggested/pass/' + entity.guid);
this.pop(index);
}
pop(index: number) {
this.entities.splice(index, 1);
if (this.entities.length < 3) {
this.offset = 3;
this.load(true);
}
}
findCity(q: string) {
if (this.searching) {
clearTimeout(this.searching);
}
this.searching = setTimeout(() => {
this.client
.get('api/v1/geolocation/list', { q: q })
.then((response: any) => {
this.cities = response.results;
});
}, 100);
}
setCity(row: any) {
// Deprecated
}
setNearby(nearby: boolean) {
this.nearby = nearby;
this.entities = [];
this.load(true);
}
}
<div class="m-rewards">
<div class="m-rewards--marketing-header">
<h1>{{name}}</h1>
<h3 i18n="M__REWARDS_MKT__TITLE_THANKS">Thank you for investing!</h3>
<div class="m-rewards--overlay"></div>
</div>
<div class="m-rewards--rewards mdl-grid mdl-grid--no-spacing" *ngIf="rewards">
<div class="mdl-cell mdl-cell--12-col">
<h2 i18n="M__REWARDS_MKT__YOUR_REWARDS">Your rewards</h2>
</div>
<div *ngIf="rewards.indexOf('10,000 points') != -1">
<i class="material-icons mdl-color-text--blue-grey-800"
>account_balance</i
>
<label i18n="M__REWARDS_MKT__10K_POINTS">10,000 points</label>
</div>
<div *ngIf="rewards.indexOf('an investor badge') != -1">
<i class="material-icons mdl-color-text--blue-grey-800"
><i class="material-icons">flight_takeoff</i></i
>
<label i18n="M__REWARDS_MKT__INVESTOR_BADGE">investor badge</label>
</div>
<div *ngIf="rewards.indexOf('an official founders page') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">account_box</i>
<label i18n="M__REWARDS_MKT__FOUNDERS_PAGE">official founders page</label>
</div>
<div *ngIf="rewards.indexOf('Minds video chat') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">video_call</i>
<label i18n="M__REWARDS_MKT__VIDEO_CHAT">Minds video chat</label>
</div>
<div
*ngIf="rewards.indexOf('a t-shirt') != -1"
i
style="align-items: center;"
>
<svg style="width:46px;height:46px" viewBox="0 0 24 24">
<path
fill="#37474f"
d="M16,21H8A1,1 0 0,1 7,20V12.07L5.7,13.12C5.31,13.5 4.68,13.5 4.29,13.12L1.46,10.29C1.07,9.9 1.07,9.27 1.46,8.88L7.34,3H9C9,4.1 10.34,5 12,5C13.66,5 15,4.1 15,3H16.66L22.54,8.88C22.93,9.27 22.93,9.9 22.54,10.29L19.71,13.12C19.32,13.5 18.69,13.5 18.3,13.12L17,12.07V20A1,1 0 0,1 16,21M20.42,9.58L16.11,5.28C15.8,5.63 15.43,5.94 15,6.2C14.16,6.7 13.13,7 12,7C10.3,7 8.79,6.32 7.89,5.28L3.58,9.58L5,11L8,9H9V19H15V9H16L19,11L20.42,9.58Z"
></path>
</svg>
<label>t-shirt</label>
</div>
<div *ngIf="rewards.indexOf('a coffee cup') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">free_breakfast</i>
<label i18n="M__REWARDS_MKT__COFFEE_CUP">coffee cup</label>
</div>
<div *ngIf="rewards.indexOf('a town hall meeting') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">group</i>
<label i18n="M__REWARDS_MKT__TOWN_HALL_MEETING">town hall meeting</label>
</div>
<div
*ngIf="rewards.indexOf('a dinner with bill') != -1 && !(rewards.indexOf('a wine & dine with the team'))"
>
<i class="material-icons mdl-color-text--blue-grey-800">local_dining</i>
<label i18n="M__REWARDS_MKT__DINNER_WITH_BILL">dinner with bill</label>
</div>
<div *ngIf="rewards.indexOf('a wine & dine with the team') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">local_dining</i>
<label i18n="M__REWARDS_MKT__WINE_AND_DINE_W_TEAM"
>wine & dine with the team</label
>
</div>
<div *ngIf="rewards.indexOf('quarterly video chats') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">video_call</i>
<label i18n="M__REWARDS_MKT__QT_VIDEO_CHATS">quarterly video chats</label>
</div>
<div *ngIf="rewards.indexOf('feature sponsorship') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">highlight</i>
<label i18n="M__REWARDS_MKT__FEATURES_SPONSORSHIP"
>feature sponsorship</label
>
</div>
</div>
<div class="m-rewards--form">
<form
#claimForm="ngForm"
(submit)="onClaim()"
class="mdl-grid"
*ngIf="loggedIn"
>
<h6
class="mdl-cell mdl-cell--12-col"
*ngIf="requiresCellPhone"
i18n="M__REWARDS_MKT__NEED_MORE_DATA"
>
We need more data before we can proceed
</h6>
<div class="mdl-cell mdl-cell--6-col" *ngIf="requiresTShirtSize">
<label i18n="M__REWARDS_MKT__TSHIRT_SIZE">T-shirt Size</label>
<select name="tshirtSize" [(ngModel)]="tshirtSize" required>
<option *ngFor="let size of tshirtSizes" [ngValue]="size"
>{{ size }}</option
>
</select>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="requiresTShirtSize">
<label i18n="M__REWARDS_MKT__ADDRESS">Address</label>
<textarea name="address" [(ngModel)]="address"></textarea>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="requiresCellPhone">
<p i18n="M__REWARDS_MKT__EMAIL_BILL">
Please email bill@minds.com to arrange your Dinner reward
</p>
</div>
<div
class="m-rewards--form-action mdl-cell"
[class.mdl-cell--6-col]="requiresCellPhone"
[class.mdl-cell--12-col]="!requiresCellPhone"
>
<button
type="submit"
class="mdl-button mdl-button--colored mdl-color--green"
[disabled]="!claimForm.form.valid || inProgress"
[ngStyle]="{'margin': !requiresCellPhone ? '16px auto': '16px'}"
i18n="M__REWARDS_MKT__CLAIM_MY_REWARDS"
>
Claim my rewards
</button>
</div>
</form>
<button
class="mdl-button mdl-button--colored mdl-color--green"
*ngIf="!loggedIn"
routerLink="/login"
i18n="M__REWARDS_MKT__LOGIN_TO_CLAIM"
>
Login to claim your rewards
</button>
</div>
</div>
@import 'defaults';
.m-rewards--marketing-header {
background: url('<%= APP_CDN %>/assets/photos/galaxy.jpg');
width: 100%;
text-align: center;
display: flex;
align-content: center;
flex-direction: column;
padding: 224px 36px;
box-sizing: border-box;
position: relative;
@media only screen and (max-width: 400px) {
padding: 110px 0;
}
h1,
h3 {
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-white);
text-shadow: 0 0 3px themed($m-grey-900);
}
@media only screen and (max-width: 400px) {
margin: 0;
}
z-index: 1;
}
h1 {
word-spacing: 25px;
letter-spacing: 4px;
text-transform: uppercase;
@media only screen and (max-width: 400px) {
font-size: 30px;
}
}
h3 {
letter-spacing: 2px;
word-spacing: 3px;
font-weight: 300;
@media only screen and (max-width: 400px) {
font-size: 14px;
}
}
.m-rewards--marketing-action-button {
margin-bottom: -80px;
margin-top: 32px;
button {
letter-spacing: 3px;
font-size: 18px;
line-height: 35px;
height: 53px;
padding: 0 24px;
font-weight: 300;
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-white);
}
}
z-index: 2;
}
.m-rewards--overlay {
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 0;
@include m-theme() {
background-color: rgba(themed($m-black), 0.15);
}
}
}
.m-rewards--rewards {
max-width: 990px;
text-align: center;
h2 {
text-transform: uppercase;
font-size: 32px;
font-weight: 400;
letter-spacing: 4px;
}
> div {
flex: auto;
display: flex;
flex-direction: column;
padding: 24px 12px;
i {
font-size: 46px;
}
label {
padding-top: 8px;
font-size: 16px;
text-transform: capitalize;
letter-spacing: 1.25px;
font-family: 'Roboto', Helvetica, sans-serif;
}
}
}
.m-rewards--form {
max-width: 990px;
margin: 32px auto;
text-align: center;
.mdl-cell {
text-align: left;
display: flex;
align-items: center;
label {
display: block;
font-size: 16px;
text-transform: capitalize;
letter-spacing: 1.25px;
font-family: 'Roboto', Helvetica, sans-serif;
padding: 8px;
}
p {
padding: 0;
margin: 0;
}
}
textarea {
padding: 16px;
width: 100%;
@include m-theme() {
border: 1px solid themed($m-grey-50);
}
}
select {
padding: 8px;
/* -webkit-appearance: none; */
font-size: 16px;
margin-left: 8px;
}
button {
padding: 8px 16px;
max-height: none;
height: auto;
font-family: 'Roboto', Helvetica, sans-serif;
letter-spacing: 2.5px;
font-size: 16px;
margin: 16px;
@include m-theme() {
color: themed($m-white) !important;
}
}
}
import { Client } from '../../services/api';
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Title } from '@angular/platform-browser';
import { Session } from '../../services/session';
import { CookieService } from '../../common/services/cookie.service';
@Component({
moduleId: module.id,
selector: 'minds-rewards-component',
templateUrl: 'rewards.html',
})
export class RewardsComponent {
paramsSubscription: Subscription;
uuid: string;
requiresTShirtSize: boolean;
requiresCellPhone: boolean;
rewards: any;
name: string;
loggedIn: boolean;
tshirtSize: string;
private cellPhoneNumber: string;
address: string;
loading: boolean = true;
inProgress: boolean = false;
tshirtSizes: Array<string> = ['Small', 'Medium', 'Large', 'Extra Large'];
constructor(
private session: Session,
private client: Client,
private route: ActivatedRoute,
private router: Router,
cookieService: CookieService
) {
if (cookieService.get('redirect')) cookieService.remove('redirect');
this.loggedIn = this.session.isLoggedIn();
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['uuid']) {
this.uuid = params['uuid'];
}
});
this.client
.get('api/v1/rewards/data', { uuid: this.uuid })
.then((res: any) => {
this.loading = false;
if (res.hasOwnProperty('valid') && !res.valid) {
this.router.navigate(['/']);
} else {
this.requiresTShirtSize = res.requiresTShirtSize;
this.requiresCellPhone = res.requiresCellPhone;
this.rewards = res.rewards;
this.name = res.name;
}
});
}
ngOnInit() {}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
onClaim() {
if (this.inProgress) return;
this.inProgress = true;
const options = {
uuid: this.uuid,
user_guid: this.session.getLoggedInUser().guid,
tshirtSize: this.tshirtSize,
address: this.address,
};
this.client
.post('api/v1/rewards/claim', options)
.then(res => {
alert('Thank you. Your rewards have been claimed.');
this.router.navigate(['/newsfeed']);
})
.catch(error => {
this.inProgress = false;
console.error('error! ', error);
});
}
onLogin() {
localStorage.setItem('redirect', '/claim-rewards/' + this.uuid);
this.router.navigate(['/login']);
}
}
import { AdminReportsDownload } from './controllers/admin/reports-download/reports-download';
import { AdminBoosts } from './controllers/admin/boosts/boosts';
import { AdminFirehoseComponent } from './controllers/admin/firehose/firehose.component';
import { AdminPages } from './controllers/admin/pages/pages';
import { AdminReports } from './controllers/admin/reports/reports';
import { AdminMonetization } from './controllers/admin/monetization/monetization';
import { AdminPrograms } from './controllers/admin/programs/programs.component';
import { AdminPayouts } from './controllers/admin/payouts/payouts.component';
import { AdminFeatured } from './controllers/admin/featured/featured';
import { AdminTagcloud } from './controllers/admin/tagcloud/tagcloud.component';
import { AdminVerify } from './controllers/admin/verify/verify.component';
import { RejectionReasonModalComponent } from './controllers/admin/boosts/modal/rejection-reason-modal.component';
import { AdminInteractions } from './controllers/admin/interactions/interactions.component';
import { InteractionsTableComponent } from './controllers/admin/interactions/table/table.component';
import { AdminPurchasesComponent } from './controllers/admin/purchases/purchases.component';
import { AdminWithdrawals } from './controllers/admin/withdrawals/withdrawals.component';
import { AdminFeaturesComponent } from './controllers/admin/features/admin-features.component';
export const MINDS_DECLARATIONS: any[] = [
// Components
InteractionsTableComponent,
// Controllers; Controller-based directives
AdminInteractions,
RejectionReasonModalComponent,
AdminBoosts,
AdminFirehoseComponent,
AdminPages,
AdminReports,
AdminMonetization,
AdminPrograms,
AdminPayouts,
AdminFeatured,
AdminTagcloud,
AdminVerify,
AdminPurchasesComponent,
AdminWithdrawals,
AdminReportsDownload,
AdminFeaturesComponent,
];
......@@ -115,6 +115,7 @@ export interface MindsUser {
has_custom_background?: boolean;
};
mode: ChannelMode;
nsfw: Array<number>;
}
export interface MindsGroup {
......
......@@ -8,10 +8,10 @@ import { ActivityService } from '../../common/services/activity.service';
@Component({
selector: 'minds-admin',
templateUrl: 'admin.html',
templateUrl: 'admin.component.html',
providers: [ActivityService],
})
export class Admin {
export class AdminComponent {
filter: string = '';
paramsSubscription: Subscription;
......
export const AdminModuleLazyRoutes = {
path: 'admin',
loadChildren: () => import('./admin.module').then(m => m.AdminModule),
};
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { AdminReportsDownload } from './reports-download/reports-download';
import { AdminBoosts } from './boosts/boosts';
import { AdminFirehoseComponent } from './firehose/firehose.component';
import { AdminPages } from './pages/pages';
import { AdminReports } from './reports/reports';
import { AdminMonetization } from './monetization/monetization';
import { AdminPrograms } from './programs/programs.component';
import { AdminPayouts } from './payouts/payouts.component';
import { AdminFeatured } from './featured/featured';
import { AdminTagcloud } from './tagcloud/tagcloud.component';
import { AdminVerify } from './verify/verify.component';
import { RejectionReasonModalComponent } from './boosts/modal/rejection-reason-modal.component';
import { AdminInteractions } from './interactions/interactions.component';
import { InteractionsTableComponent } from './interactions/table/table.component';
import { AdminPurchasesComponent } from './purchases/purchases.component';
import { AdminWithdrawals } from './withdrawals/withdrawals.component';
import { AdminFeaturesComponent } from './features/admin-features.component';
import { CommonModule } from '../../common/common.module';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { LegacyModule } from '../legacy/legacy.module';
import { BlogModule } from '../blogs/blog.module';
import { GroupsModule } from '../groups/groups.module';
import { FormsModule } from '@angular/forms';
import { CommentsModule } from '../comments/comments.module';
const routes: Routes = [
{
path: ':filter/:type',
component: AdminComponent,
data: { title: 'Admin' },
},
{ path: ':filter', component: AdminComponent, data: { title: 'Admin' } },
];
@NgModule({
imports: [
NgCommonModule,
CommonModule,
RouterModule.forChild(routes),
FormsModule,
LegacyModule,
BlogModule,
GroupsModule,
CommentsModule,
],
declarations: [
AdminComponent,
InteractionsTableComponent,
AdminInteractions,
RejectionReasonModalComponent,
AdminBoosts,
AdminFirehoseComponent,
AdminPages,
AdminReports,
AdminMonetization,
AdminPrograms,
AdminPayouts,
AdminFeatured,
AdminTagcloud,
AdminVerify,
AdminPurchasesComponent,
AdminWithdrawals,
AdminReportsDownload,
AdminFeaturesComponent,
],
})
export class AdminModule {}
......@@ -5,8 +5,8 @@ import { Subscription } from 'rxjs';
import { Client } from '../../../services/api';
import { RejectionReasonModalComponent } from './modal/rejection-reason-modal.component';
import { Reason, rejectionReasons } from './rejection-reasons';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { Reason, rejectionReasons } from '../../boost/rejection-reasons';
import { ReportCreatorComponent } from '../../report/creator/creator.component';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivityService } from '../../../common/services/activity.service';
......
......@@ -5,7 +5,7 @@ import {
Input,
Output,
} from '@angular/core';
import { Reason, rejectionReasons } from '../rejection-reasons';
import { Reason, rejectionReasons } from '../../../boost/rejection-reasons';
@Component({
moduleId: module.id,
......
......@@ -13,7 +13,7 @@ import { clientMock } from '../../../../tests/client-mock.spec';
import { AdminFirehoseComponent } from './firehose.component';
import { Session } from '../../../services/session';
import { RouterTestingModule } from '@angular/router/testing';
import { NewsfeedHashtagSelectorService } from '../../../modules/newsfeed/services/newsfeed-hashtag-selector.service';
import { NewsfeedHashtagSelectorService } from '../../newsfeed/services/newsfeed-hashtag-selector.service';
import { newsfeedHashtagSelectorServiceMock } from '../../../../tests/newsfeed-hashtag-selector-service-mock.spec';
import { overlayModalServiceMock } from '../../../../tests/overlay-modal-service-mock.spec';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
......
......@@ -4,8 +4,8 @@ import { Session } from '../../../services/session';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { NewsfeedHashtagSelectorService } from '../../../modules/newsfeed/services/newsfeed-hashtag-selector.service';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { NewsfeedHashtagSelectorService } from '../../newsfeed/services/newsfeed-hashtag-selector.service';
import { ReportCreatorComponent } from '../../report/creator/creator.component';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......
export const AnalyticsModuleLazyRoutes = {
path: 'analytics',
loadChildren: () => import('./analytics.module').then(m => m.AnalyticsModule),
};
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.