...
 
Commits (19)
......@@ -294,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
This source diff could not be displayed because it is too large. You can view the blob instead.
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,14 @@ 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';
import {
RedirectService,
BrowserRedirectService,
} from './common/services/redirect.service';
PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [MindsModule, PlotlyModule, CookieModule],
imports: [MindsModule, CookieModule],
bootstrap: [Minds],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
......
......@@ -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';
......@@ -80,6 +70,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 {
......@@ -93,12 +85,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,
......@@ -108,14 +95,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,
......@@ -157,16 +139,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(),
......
......@@ -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';
......@@ -160,7 +157,6 @@ const routes: Routes = [
RouterModule,
FormsModule,
ReactiveFormsModule,
PlotlyModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
RouterModule.forChild(routes),
......@@ -260,7 +256,6 @@ const routes: Routes = [
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
......@@ -364,7 +359,6 @@ const routes: Routes = [
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
......@@ -442,24 +436,7 @@ const routes: Routes = [
),
deps: [Client, Injector, RedirectService, Location],
},
{
provide: MetaService,
useFactory: (
titleService,
metaService,
siteService,
location,
configsService
) =>
new MetaService(
titleService,
metaService,
siteService,
location,
configsService
),
deps: [Title, Meta, SiteService, Location, ConfigsService],
},
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' };
......
......@@ -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>
......
......@@ -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 {
......
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;
}
}
<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,
];
......@@ -116,6 +116,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),
};
import { NgModule } from '@angular/core';
//import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { AdminAnalyticsComponent } from './pages/admin/admin.component';
import { CommonModule } from '../../common/common.module';
import { CommonModule as NgCommonModule } from '@angular/common';
......@@ -63,11 +60,14 @@ import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsBenchmarkComponent } from './v2/components/benchmark/benchmark.component';
//PlotlyModule.plotlyjs = PlotlyJS;
import * as PlotlyJS from 'plotly.js/dist/plotly-basic.min.js';
import { PlotlyModule } from 'angular-plotly.js';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
PlotlyModule.plotlyjs = PlotlyJS;
const routes: Routes = [
{
path: 'analytics',
path: '',
component: AnalyticsComponent,
children: [
{ path: '', redirectTo: 'dashboard/traffic', pathMatch: 'full' },
......@@ -159,6 +159,7 @@ const routes: Routes = [
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsBenchmarkComponent,
ChartV2Component,
],
providers: [AnalyticsDashboardService],
})
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { UtcDatePipe } from '../../pipes/utcdate';
import { AbbrPipe } from '../../pipes/abbr';
import { MockService } from '../../../utils/mock';
import { ThemeService } from '../../services/theme.service';
import { UtcDatePipe } from '../../../../common/pipes/utcdate';
import { AbbrPipe } from '../../../../common/pipes/abbr';
import { MockService } from '../../../../utils/mock';
import { ThemeService } from '../../../../common/services/theme.service';
import { ChartV2Component } from './chart-v2.component';
describe('ChartV2Component', () => {
......
......@@ -11,9 +11,9 @@ import {
HostBinding,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { ThemeService } from '../../services/theme.service';
import { ThemeService } from '../../../../common/services/theme.service';
import chartPalette from './chart-palette.default';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import isMobileOrTablet from '../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-chartV2',
......
......@@ -21,7 +21,8 @@ import { WireModule } from '../wire/wire.module';
import { CommentsModule } from '../comments/comments.module';
import { HashtagsModule } from '../hashtags/hashtags.module';
import { CanDeactivateGuardService } from '../../services/can-deactivate-guard';
const CKEditorModule = require('@ckeditor/ckeditor5-angular');
import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
import { BlogEditComponent } from './ckeditor/edit/edit.component';
import { BlogEditorComponent } from './ckeditor/editor/editor.component';
......
......@@ -11,9 +11,9 @@ import {
Inject,
PLATFORM_ID,
} from '@angular/core';
const CKEditorModule = require('@ckeditor/ckeditor5-angular');
import { isPlatformBrowser } from '@angular/common';
import { isPlatformServer } from '@angular/common';
declare var require: any;
@Component({
selector: 'm-blog__editor',
......@@ -56,9 +56,9 @@ export class BlogEditorComponent {
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// only executed on the browser side
const BalloonEditor = require('@ckeditor/ckeditor5-build-balloon');
this.Editor = BalloonEditor;
this.Editor.defaultConfig = this.editorConfig;
const ClassicEditor = require('@ckeditor/ckeditor5-build-classic');
this.Editor = ClassicEditor;
// this.Editor.defaultConfig = this.editorConfig;
}
if (isPlatformServer(this.platformId)) {
// only executed on the server side
......
......@@ -240,50 +240,50 @@ export class BlogEdit {
this.error = '';
this.inlineEditor.prepareForSave().then(() => {
const blog = Object.assign({}, this.blog);
// only allowed props
blog.nsfw = this.blog.nsfw;
blog.mature = blog.mature ? 1 : 0;
blog.monetization = blog.monetization ? 1 : 0;
blog.monetized = blog.monetized ? 1 : 0;
blog.time_created = blog.time_created || Math.floor(Date.now() / 1000);
this.editing = false;
this.inProgress = true;
this.canSave = false;
this.check_for_banner()
.then(() => {
this.upload
.post('api/v1/blog/' + this.guid, [this.banner], blog)
.then((response: any) => {
this.inProgress = false;
this.canSave = true;
this.blog.time_created = null;
if (response.status !== 'success') {
this.error = response.message;
return;
}
this.router.navigate(
response.route
? ['/' + response.route]
: ['/blog/view', response.guid]
);
})
.catch(e => {
this.error = e;
this.canSave = true;
this.inProgress = false;
});
})
.catch(() => {
this.error = 'error:no-banner';
this.inProgress = false;
this.canSave = true;
});
});
// this.inlineEditor.prepareForSave().then(() => {
const blog = Object.assign({}, this.blog);
// only allowed props
blog.nsfw = this.blog.nsfw;
blog.mature = blog.mature ? 1 : 0;
blog.monetization = blog.monetization ? 1 : 0;
blog.monetized = blog.monetized ? 1 : 0;
blog.time_created = blog.time_created || Math.floor(Date.now() / 1000);
this.editing = false;
this.inProgress = true;
this.canSave = false;
this.check_for_banner()
.then(() => {
this.upload
.post('api/v1/blog/' + this.guid, [this.banner], blog)
.then((response: any) => {
this.inProgress = false;
this.canSave = true;
this.blog.time_created = null;
if (response.status !== 'success') {
this.error = response.message;
return;
}
this.router.navigate(
response.route
? ['/' + response.route]
: ['/blog/view', response.guid]
);
})
.catch(e => {
this.error = e;
this.canSave = true;
this.inProgress = false;
});
})
.catch(() => {
this.error = 'error:no-banner';
this.inProgress = false;
this.canSave = true;
});
// });
}
add_banner(banner: any) {
......
......@@ -10,7 +10,10 @@ import { AnalyticsService } from '../../../services/analytics';
import { MindsBlogResponse } from '../../../interfaces/responses';
import { MindsBlogEntity } from '../../../interfaces/entities';
import { ConfigsService } from '../../../common/services/configs.service';
import { MetaService } from '../../../common/services/meta.service';
import {
MetaService,
MIN_METRIC_FOR_ROBOTS,
} from '../../../common/services/meta.service';
@Component({
selector: 'm-blog-view-infinite',
......@@ -124,7 +127,16 @@ export class BlogViewInfinite {
.setTitle(blog.custom_meta['title'] || blog.title)
.setDescription(description)
//.setAuthor(this.blog.custom_meta['author'] || `@${this.blog.ownerObj.username}`)
.setOgType('article')
.setCanonicalUrl(blog.perma_url)
.setOgUrl(blog.perma_url)
.setOgImage(blog.thumbnail_src);
.setOgImage(blog.thumbnail_src)
.setRobots(
blog['thumbs:up:count'] >= MIN_METRIC_FOR_ROBOTS ? 'all' : 'noindex'
);
if (blog.nsfw.length) {
this.metaService.setNsfw(true);
}
}
}
import { Component, Input } from '@angular/core';
import { BoostService } from '../../boost.service';
import {
Reason,
rejectionReasons,
} from '../../../../controllers/admin/boosts/rejection-reasons';
import { Reason, rejectionReasons } from '../../rejection-reasons';
@Component({
moduleId: module.id,
......
......@@ -96,7 +96,8 @@ describe('ChannelComponent', () => {
{ provide: Client, useValue: clientMock },
{ provide: Upload, useValue: uploadMock },
{ provide: Session, useValue: sessionMock },
{ provide: MetaService, useValue: MockService(MetaService) },
MetaService,
SiteService,
{ provide: ScrollService, useValue: scrollServiceMock },
{ provide: RecentService, useValue: recentServiceMock },
{ provide: ContextService, useValue: contextServiceMock },
......@@ -141,6 +142,7 @@ describe('ChannelComponent', () => {
large: 'thumbs',
master: 'thumbs',
},
nsfw: [],
};
comp.editing = false;
fixture.detectChanges();
......
......@@ -18,7 +18,10 @@ import { DialogService } from '../../common/services/confirm-leave-dialog.servic
import { BlockListService } from '../../common/services/block-list.service';
import { ChannelSortedComponent } from './sorted/sorted.component';
import { ClientMetaService } from '../../common/services/client-meta.service';
import { MetaService } from '../../common/services/meta.service';
import {
MetaService,
MIN_METRIC_FOR_ROBOTS,
} from '../../common/services/meta.service';
import { ConfigsService } from '../../common/services/configs.service';
@Component({
......@@ -124,16 +127,26 @@ export class ChannelComponent {
private updateMeta(): void {
if (this.user) {
this.metaService.setTitle(`${this.user.name} (@${this.user.username})`);
this.metaService.setDescription(
this.user.briefdescription || `Subscribe to @${this.user.username}`
);
this.metaService.setOgUrl(`/${this.user.username.toLowerCase()}`);
this.metaService.setOgImage(this.user.avatar_url.master, {
width: 2000,
height: 1000,
});
this.metaService.setRobots(this.user.is_mature ? 'noindex' : 'all');
const url = `/${this.user.username.toLowerCase()}`;
this.metaService
.setTitle(`${this.user.name} (@${this.user.username})`)
.setDescription(
this.user.briefdescription || `Subscribe to @${this.user.username}`
)
.setOgUrl(url)
.setCanonicalUrl(url)
.setOgImage(this.user.avatar_url.master, {
width: 2000,
height: 1000,
})
.setRobots(
this.user['subscribers_count'] < MIN_METRIC_FOR_ROBOTS
? 'noindex'
: 'all'
);
if (this.user.is_mature || this.user.nsfw.length) {
this.metaService.setNsfw(true);
}
} else if (this.username) {
this.metaService.setTitle(this.username);
} else {
......
......@@ -91,6 +91,7 @@ describe('ChannelFeed', () => {
impressions: 18200,
pinned_posts: ['a', 'b', 'c'],
mode: ChannelMode.PUBLIC,
nsfw: [],
};
comp.feed = [
{ guid: 'aaaa' },
......
......@@ -191,6 +191,7 @@ describe('ChannelSidebar', () => {
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
nsfw: [],
};
comp.editing = false;
uploadMock.response[`api/v1/channel/avatar`] = {
......
......@@ -48,6 +48,8 @@ export class GroupsSidebarMarkersComponent implements OnInit, OnDestroy {
await this.load(true);
this.listenForMarkers();
this.listenForMembershipUpdates();
} else {
this.inProgress = true; // Server side should start in loading spinner state
}
}
......
......@@ -5,8 +5,6 @@
[showBottombar]="false"
[forceBackground]="false"
[class.m-homepage__formExperiment]="!!registerForm"
pageTitle="Minds Social Network"
i18n-pageTitle
>
<div class="m-marketing__main m-marketing__section--style-2">
<div class="m-grid m-marketing__wrapper">
......@@ -18,13 +16,11 @@
</h1>
<h2 ngPreserveWhitespaces i18n>
Take back control of your social media
{{ headline }}
</h2>
<p class="m-marketing__description" i18n>
A place to have open conversations and bring people together. Free
your mind and get paid for creating content, driving traffic and
referring friends.
{{ description }}
</p>
<button
......
......@@ -8,6 +8,7 @@ import { RegisterForm } from '../forms/register/register';
import { FeaturesService } from '../../services/features.service';
import { ConfigsService } from '../../common/services/configs.service';
import { OnboardingV2Service } from '../onboarding-v2/service/onboarding.service';
import { MetaService } from '../../common/services/meta.service';
@Component({
selector: 'm-homepage__v2',
......@@ -18,16 +19,19 @@ export class HomepageV2Component {
readonly cdnAssetsUrl: string;
readonly siteUrl: string;
readonly headline = 'Take back control of your social media';
readonly description =
'A place to have open conversations and bring people together. Free your mind and get paid for creating content, driving traffic and referring friends.';
constructor(
public client: Client,
public router: Router,
public navigation: NavigationService,
public session: Session,
private loginReferrer: LoginReferrerService,
private featuresService: FeaturesService,
configs: ConfigsService,
private onboardingService: OnboardingV2Service
private onboardingService: OnboardingV2Service,
private metaService: MetaService
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
this.siteUrl = configs.get('site_url');
......@@ -38,6 +42,11 @@ export class HomepageV2Component {
this.router.navigate(['/newsfeed']);
return;
}
this.metaService
.setTitle(`Minds - ${this.headline}`, false)
.setDescription(this.description)
.setCanonicalUrl('/')
.setOgUrl('/');
}
registered() {
......
......@@ -21,6 +21,7 @@
>{{activity.ownerObj.name}}</strong
>
<m-channel--badges
data-nosnippet
class="m-channel--badges-activity"
[user]="activity.ownerObj"
badges="[ 'admin', 'verified' ]"
......
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.