...
 
Commits (67)
......@@ -7,17 +7,14 @@
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"assets": ["assets", "favicon.ico"],
"index": "index.php",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"prefix": "m",
"styles": [
"../node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"../node_modules/material-design-icons/iconfont/material-icons.css",
......
......@@ -16,10 +16,7 @@
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"assets": ["src/assets", "src/favicon.ico"],
"styles": [
"node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"node_modules/material-design-icons/iconfont/material-icons.css",
......@@ -36,10 +33,10 @@
"optimization": true,
"outputHashing": "all",
"sourceMap": {
"hidden": true,
"scripts": true,
"styles": true
},
"hidden": true,
"scripts": true,
"styles": true
},
"extractCss": true,
"namedChunks": false,
"aot": true,
......@@ -52,6 +49,14 @@
"with": "src/environments/environment.prod.ts"
}
]
},
"hmr": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.hmr.ts"
}
]
}
}
},
......@@ -63,6 +68,10 @@
"configurations": {
"production": {
"browserTarget": "v5.x:build:production"
},
"hmr": {
"hmr": true,
"browserTarget": "v5.x:build:hmr"
}
}
},
......@@ -83,22 +92,14 @@
"node_modules/medium-editor/dist/js/medium-editor.min.js",
"src/shims/jitsi-api.min.js"
],
"assets": [
"src/assets",
"src/favicon.ico"
]
"assets": ["src/assets", "src/favicon.ico"]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
}
......@@ -118,12 +119,8 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
"tsConfig": ["e2e/tsconfig.e2e.json"],
"exclude": ["**/node_modules/**"]
}
}
}
......@@ -132,11 +129,11 @@
"defaultProject": "v5.x",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"prefix": "m",
"styleext": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
"prefix": "m"
}
}
}
......@@ -21,7 +21,6 @@ context('Pro Settings', () => {
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
},
logoGuid: '#logo_guid',
}
const hashtags = {
......
This diff is collapsed.
......@@ -10,6 +10,7 @@
"build": "ng build --prod",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost/en",
"build-dev": "ng build --output-path dist/en --deploy-url=/en/ --watch=true --poll=800",
"serve-dev": "ng serve --host=0.0.0.0 --deploy-url=/en/ --configuration=hmr --hmr --poll=800 --progress",
"test": "ng test",
"lint": "ng lint",
"e2e": "cypress run --debug",
......@@ -57,6 +58,7 @@
"@angular/cli": "^7.2.1",
"@angular/compiler-cli": "~8.0.3",
"@angular/language-service": "~8.0.3",
"@angularclass/hmr": "^2.1.3",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.4",
"@types/node": "~10.12.18",
......@@ -88,7 +90,7 @@
},
"husky": {
"hooks": {
"pre-commit": ".githooks/pre-commit && pretty-quick --staged --bail --pattern '**/*.*(ts|html|scss)'"
"pre-commit": "sh .githooks/pre-commit && pretty-quick --staged --bail --pattern \"**/*.*(ts|html|scss)\""
}
}
}
......@@ -35,8 +35,6 @@ export class Minds {
showTOSModal: boolean = false;
paramsSubscription;
protected router$: Subscription;
constructor(
......@@ -125,7 +123,6 @@ export class Minds {
ngOnDestroy() {
this.loginReferrer.unlisten();
this.scrollToTop.unlisten();
this.paramsSubscription.unsubscribe();
}
@HostBinding('class') get cssColorSchemeOverride() {
......
......@@ -70,6 +70,7 @@ 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';
import * as Sentry from '@sentry/browser';
......@@ -147,6 +148,7 @@ export class SentryErrorHandler implements ErrorHandler {
IssuesModule,
CanaryModule,
ChannelsModule,
UpgradesModule,
//last due to :username route
ChannelContainerModule,
......
......@@ -109,6 +109,10 @@ import { RouterHistoryService } from './services/router-history.service';
import { DraggableListComponent } from './components/draggable-list/list.component';
import { DndModule } from 'ngx-drag-drop';
import { SiteService } from './services/site.service';
import { MarketingComponent } from './components/marketing/marketing.component';
import { MarketingFooterComponent } from './components/marketing/footer.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
@NgModule({
imports: [
......@@ -207,6 +211,10 @@ import { SiteService } from './services/site.service';
FeaturedContentComponent,
PosterDateSelectorComponent,
DraggableListComponent,
ToggleComponent,
MarketingComponent,
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
],
exports: [
MINDS_PIPES,
......@@ -294,6 +302,9 @@ import { SiteService } from './services/site.service';
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
DraggableListComponent,
ToggleComponent,
MarketingComponent,
MarketingAsFeaturedInComponent,
],
providers: [
SiteService,
......
<div class="m-grid m-marketing__asFeaturedIn">
<div class="m-grid__column-2 m-marketingAsFeaturedIn__title" i18n>
As featured in
</div>
<ul class="m-grid__column-10">
<li>
<a
href="https://www.independent.co.uk/news/business/indyventure/minds-facebook-alternative-deletefacebook-social-network-data-a8475841.html"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/press-logos/independent.png'"
alt="Independent"
/>
</a>
</li>
<li>
<a
href="https://www.foxnews.com/tech/alternate-social-media-squash-extremist-content-without-violating-first-amendment"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/foxnews.png'"
alt="Fox News"
/>
</a>
</li>
<li *ngIf="false">
<img [src]="cdnAssetsUrl + 'assets/marketing/forbes.png'" alt="Forbes" />
</li>
<li>
<a
href="https://techcrunch.com/2018/04/16/minds-aims-to-decentralize-the-social-network/"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/techcrunch.png'"
alt="TechCrunch"
/>
</a>
</li>
<li>
<a
href="https://mobile.reuters.com/article/amp/idUSKBN1K7147"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/reuters.png'"
alt="Reuters"
/>
</a>
</li>
<li>
<a
href="https://www.wired.com/story/minds-anti-facebook/"
target="_blank"
>
<img [src]="cdnAssetsUrl + 'assets/marketing/wired.png'" alt="Wired" />
</a>
</li>
<li>
<a
href="http://podcasts.joerogan.net/podcasts/bill-ottman"
target="_blank"
>
<img
[src]="cdnAssetsUrl + 'assets/marketing/tjre.png'"
alt="The Joe Rogan Experience"
/>
</a>
</li>
</ul>
</div>
@import '../../../foundation/grid-values';
.m-marketing__asFeaturedIn {
max-width: 1084px;
margin: 40px auto 0;
@media screen and (max-width: $m-grid-min-vp) {
&.m-grid {
display: block;
}
margin: 20px 0 45px;
padding: 0 12px;
}
.m-marketingAsFeaturedIn__title {
display: flex;
flex-direction: column;
justify-content: center;
@include m-theme() {
color: themed($m-grey-400);
}
@media screen and (max-width: $m-grid-min-vp) {
text-align: center;
margin: 0 0 20px;
}
}
ul {
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 0;
list-style: none;
> li {
img {
width: 55px;
height: 55px;
object-fit: contain;
@media screen and (max-width: $m-grid-min-vp) {
width: 40px;
height: 40px;
}
}
&.m-marketingAsFeaturedIn__item--bigger {
img {
width: 96px;
height: 96px;
@media screen and (max-width: $m-grid-min-vp) {
width: 40px;
height: 40px;
}
}
}
}
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-marketing__asFeaturedIn',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'as-featured-in.component.html',
})
export class MarketingAsFeaturedInComponent {
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
}
<div class="m-marketing__footer">
<div class="m-grid m-marketingFooter__columns">
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4 i18n>About</h4>
<ul>
<li hidden>
<a href="#" i18n>
Company
</a>
</li>
<li hidden>
<a href="#" i18n>
Mission
</a>
</li>
<li hidden>
<a href="#" i18n>
Features
</a>
</li>
<li>
<a routerLink="/mobile" i18n>
Mobile
</a>
</li>
<li>
<a routerLink="/jobs" i18n>
Jobs
</a>
</li>
<li hidden>
<a href="https://carta.com/minds" i18n>
Investors
</a>
</li>
<li>
<a routerLink="/blog/minds" i18n>
Blog
</a>
</li>
<li>
<a
href="https://cdn-assets.minds.com/front/dist/assets/whitepapers/03_27_18_Minds%20Whitepaper%20V0.1.pdf"
target="_blank"
i18n
>
Whitepaper
</a>
</li>
<li>
<a href="https://irl.minds.com/" target="_blank" i18n>
Minds IRL
</a>
</li>
</ul>
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4 i18n>Business</h4>
<ul>
<li>
<a routerLink="/upgrades" i18n>
Upgrade
</a>
</li>
<li>
<a routerLink="/token" i18n>
Token
</a>
</li>
<li>
<a routerLink="/plus" i18n>
Plus
</a>
</li>
<li>
<a routerLink="/pro" i18n>
Pro
</a>
</li>
<li>
<a routerLink="/nodes" i18n>
Nodes
</a>
</li>
<li>
<a routerLink="/boost" i18n>
Boost
</a>
</li>
<li>
<a routerLink="/wire" i18n>
Pay
</a>
</li>
<li>
<a href="https://teespring.com/stores/minds" i18n>
Store
</a>
</li>
</ul>
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4>Developers</h4>
<ul>
<li>
<a href="https://developers.minds.com" target="_blank" i18n>
Documentation
</a>
</li>
<li>
<a routerLink="/groups/profile/365903183068794880" i18n>
Community
</a>
</li>
<li>
<a href="https://gitlab.com/minds" target="_blank" i18n>
Code
</a>
</li>
<li>
<a routerLink="/canary" i18n>
Canary
</a>
</li>
<li>
<a routerLink="/branding" i18n>
Branding
</a>
</li>
</ul>
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4>Support</h4>
<ul>
<li>
<a routerLink="/help" i18n>
Help Desk
</a>
</li>
<li>
<a routerLink="/groups/profile/100000000000000681" i18n>
Community
</a>
</li>
<li>
<a routerLink="/localization" i18n>
Languages
</a>
</li>
<li>
<a href="https://status.minds.com" target="_blank" i18n>
Status
</a>
</li>
<li hidden>
<a routerLink="/p/contact" i18n>
Contact
</a>
</li>
</ul>
</div>
</div>
<div class="m-marketing__sep m-marketing__sep--big"></div>
<div class="m-grid m-marketingFooter__columns">
<div
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column m-marketingFooter__column--noMobileSpacing"
i18n
>
<div class="m-marketingFooter__text">&copy; {{ year }} Minds, Inc.</div>
</div>
<div
class="m-grid__column-10 m-grid__column-12--mobile m-marketingFooter__column"
>
<ul class="m-marketingFooter__inlineList">
<li>
<a routerLink="/p/terms" i18n>
Terms of Service
</a>
</li>
<li>
<a routerLink="/p/privacy" i18n>
Privacy Policy
</a>
</li>
<li>
<a routerLink="/content-policy" i18n>
Content Policy
</a>
</li>
<li>
<a routerLink="/p/dmca" i18n>
DMCA
</a>
</li>
</ul>
</div>
</div>
</div>
@import '../../../foundation/grid-values';
m-marketing__footer {
display: block;
margin-top: 105px;
@media screen and (max-width: $m-grid-min-vp) {
margin-top: 80px;
}
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-gradient-start) 0%,
themed($m-white) 100%
);
}
}
.m-marketing__footer {
padding: 60px 0 48px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 32px 0;
}
@include m-theme() {
color: themed($m-grey-800);
}
.m-marketingFooter__columns {
max-width: 1084px;
margin: 0 auto;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 32px;
}
}
.m-marketingFooter__column {
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 32px;
&:last-child,
&.m-marketingFooter__column--noMobileSpacing {
margin-bottom: 0;
}
}
h4 {
font-weight: 500;
font-size: 16px;
line-height: 21px;
margin: 0 0 26px;
@media screen and (max-width: $m-grid-min-vp) {
margin: 0 0 8px;
}
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-marketingFooter__text {
font-size: 14px;
line-height: 26px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
> li {
@extend .m-marketingFooter__text;
@include m-theme() {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
display: inline-block;
margin-right: 1em;
&:last-child {
margin-right: 0;
}
}
a {
color: inherit;
font-weight: 300;
text-decoration: none;
}
}
&.m-marketingFooter__inlineList {
> li {
display: inline-block;
margin-right: 40px;
@media screen and (max-width: $m-grid-min-vp) {
margin-right: 1em;
}
&:last-child {
margin-right: 0;
}
}
}
}
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-marketing__footer',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'footer.component.html',
})
export class MarketingFooterComponent {
readonly year: number = new Date().getFullYear();
}
.m-marketing__main,
.m-marketing__section {
// Common
overflow-x: hidden;
.m-marketing--hideMobile {
@media screen and (max-width: $m-grid-min-vp) {
display: none;
}
}
.m-marketing__title,
h1 {
font-size: 22px;
line-height: 44px;
font-weight: bold;
opacity: 0.7;
margin: 12px 0 4px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-marketing__subtitle,
h2 {
font-weight: 900;
font-size: 42px;
line-height: 44px;
margin: 0 0 23px;
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 32px;
line-height: 34px;
margin: 0 0 18px;
}
&.m-marketing__subtitle--asTitle {
font-size: 48px;
line-height: 53px;
margin: 0 0 26px;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 38px;
line-height: 46px;
margin: 0 0 21px;
}
}
em {
font-style: inherit;
text-decoration: inherit;
white-space: nowrap;
position: relative;
&::after {
content: '';
display: inline-block;
position: absolute;
background: rgba(80, 226, 195, 0.3);
top: 0.36em;
left: -0.03em;
right: -0.03em;
bottom: 0.22em;
pointer-events: none;
z-index: -1;
}
}
em + em::after {
left: -0.25em;
}
}
p.m-marketing__description {
font-size: 18px;
line-height: 27px;
margin: 0 0 36px;
}
ul.m-marketing__points {
list-style: disc;
margin: 0 0 45px;
padding: 0 0 0 1em;
font-size: 16px;
line-height: 21px;
> li {
margin-bottom: 19px;
&:last-child {
margin-bottom: 0;
}
}
}
// Style 1
&.m-marketing__section--style-1 {
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-white) 0%,
themed($m-marketing-bg-gradient-start) 100%
);
}
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 95px 0 85px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 30px 0;
margin: 0 20px;
}
}
.m-marketing__body {
position: relative;
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 383px;
height: 388px;
transform: translate(-290px, 97px);
background: url('<%= APP_CDN %>/assets/marketing/deco_2.svg') no-repeat;
z-index: -1;
opacity: 0.6;
@media screen and (max-width: $m-grid-min-vp) {
transform: translate(170px, -180px);
left: auto;
right: 0;
}
}
}
h2,
.m-marketing__subtitle {
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px;
}
}
p.m-marketing__description {
@include m-theme() {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px;
}
}
ul.m-marketing__points {
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px;
}
}
.m-marketing__image {
position: relative;
align-self: center;
z-index: 0;
span {
display: block;
position: relative;
width: 528px;
height: 415px;
@media screen and (max-width: $m-grid-min-vp) {
width: 313px;
height: 237px;
margin: 0 auto;
}
// Deco
&::before {
content: '';
display: block;
position: absolute;
width: 393px;
height: 193px;
top: 0;
left: 0;
transform: translate(61px, 179px);
background: url('<%= APP_CDN %>/assets/marketing/deco_1.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
top: 0;
left: 0;
width: 377px;
height: 193px;
transform: translate(-32px, 18px);
background-size: cover;
}
}
&::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 383px;
height: 383px;
transform: translate(178px, -95px);
background: url('<%= APP_CDN %>/assets/marketing/deco_2.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
img {
position: absolute;
object-fit: cover;
}
.m-marketing__image--1 {
top: 0;
left: 90px;
width: 327px;
height: 327px;
@media screen and (max-width: $m-grid-min-vp) {
top: 0;
left: 0;
width: 190px;
height: 190px;
}
}
.m-marketing__image--2 {
top: 295px;
left: 0;
width: 181px;
height: 120px;
@media screen and (max-width: $m-grid-min-vp) {
display: none;
}
}
.m-marketing__image--3 {
top: 163px;
left: 358px;
width: 170px;
height: 198px;
@media screen and (max-width: $m-grid-min-vp) {
top: 65px;
left: 165px;
width: 148px;
height: 172px;
}
}
}
}
// Style 2
&.m-marketing__section--style-2 {
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 72px 0 32px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px 30px;
}
&::before {
content: '';
position: absolute;
top: 0;
right: -290px;
bottom: -56px;
left: 0;
transform: translate(-86px, -56px);
clip-path: polygon(0% 0%, 0% 100%, 100% 92%, 100% 0%);
z-index: -1;
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-colored-gradient-start) 0%,
themed($m-marketing-bg-colored-gradient-end) 99.99%
);
}
@media screen and (max-width: $m-grid-min-vp) {
right: 0;
bottom: -3vw;
transform: none;
clip-path: none;
}
}
h1 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 15px 0 15px;
text-align: center;
}
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
.m-marketing__image {
position: relative;
z-index: 0;
img {
object-fit: contain;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
width: 100vw;
height: 100vw;
object-fit: cover;
clip-path: polygon(0% 2%, 0% 97%, 100% 100%, 100% 0%);
}
}
span {
display: inline-block;
position: relative;
// Deco
&::before {
content: '';
display: block;
position: absolute;
width: 393px;
height: 193px;
bottom: 0;
right: 0;
transform: translate(45px, 32px);
background: url('<%= APP_CDN %>/assets/marketing/deco_1.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
&::after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 284px;
height: 262px;
transform: translate(35px, -35px);
background: url('<%= APP_CDN %>/assets/marketing/deco_2-straight.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
}
}
}
<div class="m-marketing">
<div class="m-marketing__mainWrapper">
<ng-content select=".m-marketing__main"></ng-content>
</div>
<ng-content select="[slot=2]"></ng-content>
<div class="m-marketing__sep" *ngIf="bodyWrapper?.children?.length > 0"></div>
<div class="m-marketing__extrasWrapper" #bodyWrapper>
<ng-content select=".m-marketing__extras"></ng-content>
</div>
</div>
<m-marketing__footer></m-marketing__footer>
@import '../../../foundation/grid-values';
m-marketing {
display: block;
font-family: Roboto, sans-serif;
@include m-theme() {
background: themed($m-white);
color: themed($m-grey-800);
}
.m-marketing {
font-family: Roboto, sans-serif;
font-weight: 400;
}
.m-marketing__sep {
border-top: 1px solid;
height: 0;
width: 100%;
margin: 40px 0;
&.m-marketing__sep--big {
margin: 60px 0;
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 20px 0;
&.m-marketing__sep--big {
margin: 30px 0;
}
}
@include m-theme() {
border-color: themed($m-grey-50);
}
}
.m-marketing__wrapper,
.m-marketing__extras > * > * {
max-width: 1084px;
margin: 0 auto;
box-sizing: border-box;
}
}
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from '@angular/core';
import { MindsTitle } from '../../../services/ux/title';
@Component({
selector: 'm-marketing',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'marketing.component.html',
})
export class MarketingComponent implements OnInit {
@Input() pageTitle: string = '';
constructor(protected title: MindsTitle) {}
ngOnInit() {
if (this.pageTitle) {
this.title.setTitle(this.pageTitle);
}
}
}
<div class="m-toggle__track"></div>
<div
class="m-toggle__switch"
[class.m-toggle__switch--left]="mModel === leftValue"
[class.m-toggle__switch--right]="mModel === rightValue"
></div>
m-toggle {
position: relative;
display: inline-block;
width: 27px;
height: 19px;
margin: 0 0.35em;
cursor: pointer;
.m-toggle__track {
display: inline-block;
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 10px;
z-index: 1;
transform: translateY(-50%);
border-radius: 6px;
@include m-theme() {
background: themed($m-grey-100);
}
}
.m-toggle__switch {
display: inline-block;
position: absolute;
top: 0;
left: 50%;
width: 19px;
height: 19px;
z-index: 2;
transform: translateX(-50%);
border-radius: 50%;
@include m-theme() {
background: themed($m-grey-100);
box-shadow: 1px 1px 1px -1px rgba(themed($m-black), 0.3);
}
&.m-toggle__switch--left,
&.m-toggle__switch--right {
transform: none;
@include m-theme() {
background: themed($m-blue);
}
}
&.m-toggle__switch--left {
left: 0;
right: auto;
}
&.m-toggle__switch--right {
right: 0;
left: auto;
}
}
}
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostListener,
Input,
Output,
} from '@angular/core';
@Component({
selector: 'm-toggle',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'toggle.component.html',
})
export class ToggleComponent {
@Input('leftValue') leftValue: any;
@Input('rightValue') rightValue: any;
@Input('mModel') mModel: any;
@Output('mModelChange') mModelChange: EventEmitter<any> = new EventEmitter<
any
>();
@HostListener('click') toggle() {
if (this.mModel === this.leftValue) {
this.mModelChange.emit(this.rightValue);
} else {
this.mModelChange.emit(this.leftValue);
}
}
}
......@@ -42,10 +42,10 @@
<li
class="m-dropdownList__item m-user-menuDropdown__Item"
*ngIf="isAdmin()"
(click)="closeMenu()"
*ngIf="getCurrentUser()?.pro"
>
<a routerLink="/analytics/admin/network">
<a routerLink="/analytics/dashboard/traffic">
<i class="material-icons">timeline</i>
<span i18n>Analytics</span>
</a>
......
......@@ -58,6 +58,15 @@ describe('TagPipe', () => {
);
});
it('should transform uppercase text following # to lower case ', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = 'textString #NaMe';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=name;period=24h'
);
});
it('should correctly parse when duplicates substrings present', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = '#hash #hashlonger';
......
......@@ -32,7 +32,11 @@ export class TagsPipe implements PipeTransform {
rule: /(^|\s||)#(\w+)/gim,
replace: m => {
if (this.featureService.has('top-feeds')) {
return `${m.match[1]}<a href="/newsfeed/global/top;hashtag=${m.match[2]};period=24h">#${m.match[2]}</a>`;
return `${
m.match[1]
}<a href="/newsfeed/global/top;hashtag=${m.match[2].toLowerCase()};period=24h">#${
m.match[2]
}</a>`;
}
return `${m.match[1]}<a href="/newsfeed/tag/${m.match[2]};ref=hashtag">#${m.match[2]}</a>`;
},
......
@import './grid-values';
@import '../../stylesheets/themes';
.mf-button {
display: inline-block;
padding: 14px 32px;
font: inherit;
font-size: 18px;
line-height: 24px;
border: 1px solid;
border-radius: 2px;
font-weight: 400;
cursor: pointer;
user-select: none;
white-space: nowrap;
text-align: center;
appearance: none;
text-decoration: none;
@include m-theme() {
background: themed($m-blue);
color: themed($m-white-always);
border-color: themed($m-blue);
}
&.mf-button--alt {
@include m-theme() {
background: themed($m-aqua);
border-color: themed($m-aqua);
color: themed($m-white-always);
}
}
&.mf-button--destructive {
@include m-theme() {
background: themed($m-red);
border-color: themed($m-red);
color: themed($m-white-always);
}
}
@media screen and (max-width: $m-grid-min-vp) {
display: block;
font-size: 15px;
padding: 12px 15px;
width: 100%;
box-sizing: border-box;
}
&.mf-button--always-inline {
@media screen and (max-width: $m-grid-min-vp) {
display: inline-block;
}
}
&.mf-button--hollow {
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
border-color: themed($m-blue);
}
}
&[disabled] {
cursor: default;
opacity: 0.6;
}
}
$m-grid-min-vp: 1168px;
$m-grid-cols: 12;
$m-grid-gap: 20px;
@import './grid-values';
.m-grid {
display: grid;
grid-template-columns: repeat($m-grid-cols, 1fr);
grid-column-gap: $m-grid-gap;
grid-row-gap: 0;
@for $i from 1 through $m-grid-cols {
.m-grid__column-#{$i} {
grid-column: auto / span $i;
}
}
@media screen and (max-width: $m-grid-min-vp) {
@for $i from 1 through $m-grid-cols {
.m-grid__column-#{$i}--mobile {
grid-column: auto / span $i;
}
}
}
}
.mf-jumpAnchor {
// used for element.scrollIntoView targets
position: relative;
top: 0;
width: 0;
height: 0;
visibility: hidden;
m-body.has-v2-navbar & {
top: -52px;
}
}
export type Currency = 'tokens' | 'usd';
export default function currency(value: number, type: Currency) {
switch (type) {
case 'tokens':
return `${value.toLocaleString()} tokens`;
case 'usd':
return `$ ${value.toLocaleString()}`;
}
}
export interface Category {
id: string;
label: string;
metrics?: string[]; // TODO: remove this
permissions?: string[];
}
export interface Response {
status: string;
dashboard: Dashboard;
}
export interface Dashboard {
category: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
}
export interface Filter {
id: string;
label: string;
options: Option[];
}
export interface Option {
id: string;
label: string;
available?: boolean;
selected?: boolean;
interval?: string;
comparison_interval?: number;
from_ts_ms?: number;
from_ts_iso?: string;
}
export interface Metric {
id: string;
label: string;
permissions: string[];
summary: Summary;
visualisation: Visualisation | null;
}
export interface Summary {
current_value: number;
comparison_value: number;
comparison_interval: number;
comparison_positive_inclination: boolean;
}
export interface Visualisation {
type: string;
segments: Array<Buckets>;
}
export interface Buckets {
buckets: Bucket[];
}
export interface Bucket {
key: number;
date: string;
value: number;
}
export interface Timespan {
id: string;
label: string;
interval: string;
comparison_interval: number;
from_ts_ms: number;
from_ts_iso: string;
}
export interface UserState {
category: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
loading: boolean;
}
......@@ -76,14 +76,15 @@ export interface MindsUser {
subscribed?: boolean;
rating?: number;
eth_wallet?: string;
is_admin?: boolean;
is_mature?: boolean;
mature_lock?: boolean;
tags?: Array<string>;
toaster_notifications?: boolean;
pro?: boolean;
pro_published?: boolean;
pro_settings?: {
logo_image: string;
logo_guid: string;
tag_list?: Tag[];
background_image: string;
title: string;
......@@ -95,6 +96,8 @@ export interface MindsUser {
featured_content?: Array<string>;
tile_ratio?: string;
styles?: { [key: string]: string };
has_custom_logo?: boolean;
has_custom_background?: boolean;
};
mode: ChannelMode;
}
......
......@@ -49,6 +49,21 @@ import { ActiveUsersChartComponent } from './components/charts/active-users/acti
import { Graph } from './graph.component';
import { PageviewsCardComponent } from './components/cards/pageviews/pageviews.component';
import { PageviewsChartComponent } from './components/charts/pageviews/pageviews.component';
import { AnalyticsDashboardComponent } from './v2/dashboard.component';
import { AnalyticsLayoutChartComponent } from './v2/layouts/layout-chart/layout-chart.component';
import { AnalyticsLayoutTableComponent } from './v2/layouts/layout-table/layout-table.component';
import { AnalyticsLayoutSummaryComponent } from './v2/layouts/layout-summary/layout-summary.component';
import { AnalyticsMetricsComponent } from './v2/components/metrics/metrics.component';
import { AnalyticsFiltersComponent } from './v2/components/filters/filters.component';
import { AnalyticsFilterComponent } from './v2/components/filter/filter.component';
import { AnalyticsChartComponent } from './v2/components/chart/chart.component';
import { AnalyticsTableComponent } from './v2/components/table/table.component';
import { AnalyticsDashboardService } from './v2/dashboard.service';
import { SearchModule } from '../search/search.module';
import { AnalyticsSearchComponent } from './v2/components/search/search.component';
import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsMenuComponent } from './v2/components/menu/menu.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -77,6 +92,12 @@ const routes: Routes = [
// { path: 'plus', component: OffChainBoostsCardComponent},
],
},
{
path: 'dashboard/',
redirectTo: 'dashboard/traffic',
pathMatch: 'full',
},
{ path: 'dashboard/:category', component: AnalyticsDashboardComponent },
],
},
];
......@@ -87,6 +108,8 @@ const routes: Routes = [
CommonModule,
RouterModule.forChild(routes),
PlotlyModule,
SearchModule,
FormsModule,
],
exports: [
AdminAnalyticsComponent,
......@@ -138,7 +161,19 @@ const routes: Routes = [
PageviewsChartComponent,
PageviewsCardComponent,
Graph,
AnalyticsDashboardComponent,
AnalyticsLayoutChartComponent,
AnalyticsLayoutTableComponent,
AnalyticsLayoutSummaryComponent,
AnalyticsMetricsComponent,
AnalyticsFiltersComponent,
AnalyticsFilterComponent,
AnalyticsChartComponent,
AnalyticsTableComponent,
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsMenuComponent,
],
providers: [],
providers: [AnalyticsDashboardService],
})
export class AnalyticsModule {}
@import 'defaults';
m-analytics {
display: block;
@include m-theme() {
background-color: themed($m-white);
}
.m-analytics__content {
max-width: 1280px;
margin: 0 auto;
......
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Session } from '../../../../services/session';
@Component({
selector: 'm-analytics__channel',
templateUrl: 'channel.component.html',
})
export class ChannelAnalyticsComponent {}
export class ChannelAnalyticsComponent {
constructor(private router: Router, private session: Session) {}
ngOnInit() {
if (!this.session.isLoggedIn()) {
this.router.navigate(['/login']);
return;
}
}
}
const categories: Array<any> = [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// metrics: [],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
metrics: [
'active_users',
'signups',
'unique_visitors',
'pageviews',
'impressions',
'retention',
],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
metrics: ['total', 'pageviews', 'active_referrals', 'customers'],
},
// {
// id: 'engagement',
// label: 'Engagement',
// permissions: ['admin', 'user'],
// metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
// },
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
metrics: ['top_content', 'top_channels'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// metrics: ['top_referrers'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_tokens'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
];
export default categories;
const chartPalette: Array<any> = [
{
id: 'm-white',
themeMap: ['#fff', '#161616'],
},
{
id: 'm-transparent',
themeMap: ['rgba(0,0,0,0)', 'rgba(0,0,0,0)'],
},
{
id: 'm-grey-50',
themeMap: ['rgba(232,232,232,1)', 'rgba(53,53,53,1)'],
},
{
id: 'm-grey-70',
themeMap: ['#eee', '#333'],
},
{
id: 'm-grey-130',
themeMap: ['#ccc', '#555'],
},
{
id: 'm-grey-160',
themeMap: ['#bbb', '#555'],
},
{
id: 'm-grey-300',
themeMap: ['#999', '#666'],
},
{
id: 'm-blue',
themeMap: ['#4690df', '#44aaff'],
},
{
id: 'm-red-dark',
themeMap: ['#c62828', '#e57373'],
},
{
id: 'm-amber-dark',
themeMap: ['#ffa000', '#ffecb3'],
},
{
id: 'm-green-dark',
themeMap: ['#388e3c', '#8bc34a'],
},
{
id: 'm-blue-grey-500',
themeMap: ['#607d8b', '#607d8b'],
},
];
export default chartPalette;
<!-- TODO: Make this into a different component -->
<!-- <m-chart [buckets]="(vm$.thecurrentvisualisation" | async)></m-chart> -->
<!-- TODO: then all this becomes m-plotlyChart -->
<!-- <div *ngIf="vm$ | async as vm"> -->
<div>
<div #graphDiv id="graphDiv"></div>
<!-- <plotly-plot
id="graphDiv"
[divId]="graphDiv"
[data]="data"
[layout]="layout"
[config]="config"
[useResizeHandler]="true"
[style]="{ position: 'relative' }"
(hover)="onHover($event)"
(unhover)="onUnhover($event)"
(afterPlot)="afterPlot()"
>
</plotly-plot> -->
<!-- <div class="hoverInfo__row">
{{ hoverInfo.date | date: selectedTimespan.datePipe }}
</div> -->
<div #hoverInfoDiv id="hoverInfoDiv" class="hoverInfoDiv">
<div class="hoverInfo__row">
{{ hoverInfo.date | utcDate | date: datePipe }}
</div>
<div [ngSwitch]="selectedMetric.unit" class="hoverInfo__row--primary">
<ng-template ngSwitchCase="number">
{{ hoverInfo.value | number }} {{ selectedMetric.label | lowercase }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.value | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.value | number: '1.1-3' }} {{ selectedMetric.unit }}
</ng-template>
</div>
<div class="hoverInfo__row" *ngIf="isComparison">
vs
<ng-container [ngSwitch]="selectedMetric.unit" class="hoverInfo__row">
<ng-template ngSwitchCase="number">
{{ hoverInfo.comparisonValue | number }}
</ng-template>
<ng-template ngSwitchCase="usd">
{{ hoverInfo.comparisonValue | currency }} USD
</ng-template>
<ng-template ngSwitchDefault>
{{ hoverInfo.comparisonValue | number: '1.1-3' }}
{{ selectedMetric.unit }}
</ng-template>
</ng-container>
on {{ hoverInfo.comparisonDate | utcDate | date: datePipe }}
</div>
</div>
</div>
m-analytics__chart {
display: block;
position: relative;
.js-plotly-plot,
.plot-container {
height: 44vh;
display: block;
}
}
#graphDiv {
position: relative;
g,
g > * {
cursor: default;
}
> * {
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1),
color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.main-svg {
max-width: 100%;
}
}
.hoverInfoDiv {
width: 160px;
padding: 12px;
position: absolute;
pointer-events: none;
border-radius: 3px;
font-size: 12px;
z-index: 9999999999; // TODO fix
opacity: 0;
transition: opacity 0.2s ease-in;
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0 0 4px rgba(themed($m-black), 0.3);
color: themed($m-grey-200);
}
[class*='hoverInfo__row'] {
padding-bottom: 4px;
&:last-of-type {
padding-top: 2px;
}
}
.hoverInfo__row--primary {
font-size: 15px;
// font-weight: bold;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsChartComponent } from './chart.component';
describe('AnalyticsChartComponent', () => {
let component: AnalyticsChartComponent;
let fixture: ComponentFixture<AnalyticsChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsChartComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
<div class="filterLabelWrapper" *ngIf="filter.id !== 'timespan'">
<span>{{ filter.label }}</span>
<m-tooltip icon="help">
<div class="filterDesc">{{ filter?.description }}</div>
<ul class="filterOptions__descContainer">
<ng-container *ngFor="let option of filter.options">
<li class="filterOption__desc">
<span class="filterOption__descLabel">{{ option.label }}</span
><span class="filterOption__desc" *ngIf="option.description"
>: {{ option.description }}</span
>
</li>
</ng-container>
</ul>
</m-tooltip>
</div>
<div
class="filterWrapper"
[ngClass]="{
expanded: expanded,
isMobile: isMobile,
dropUp: dropUp
}"
(focus)="expanded = true"
(blur)="expanded = false"
>
<div class="filterHeader" (click)="expanded = !expanded">
<div class="row">
<span class="option option--selected">
{{ selectedOption.label }}
</span>
<i class="material-icons" *ngIf="!expanded">keyboard_arrow_down</i>
<i class="material-icons" *ngIf="expanded">keyboard_arrow_up</i>
</div>
</div>
<div class="unselectedOptionsContainer">
<ng-container *ngFor="let option of filter.options">
<div
class="option row"
(click)="updateFilter(option)"
[ngClass]="{
unavailable: option.available === false
}"
>
<span>{{ option.label }}</span>
</div>
</ng-container>
</div>
</div>
$rounded-top: 3px 3px 0 0;
$rounded-bottom: 0 0 3px 3px;
m-analytics__filter {
position: relative;
margin: 0 36px 0 0;
z-index: 2;
}
.filterLabelWrapper {
position: absolute;
bottom: 110%;
white-space: nowrap;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.9);
}
m-tooltip {
margin-right: 4px;
}
> * {
display: inline-block;
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.7);
}
}
.m-tooltip--bubble {
letter-spacing: 1.2px;
line-height: 16px;
z-index: 9999;
font-size: 11px;
bottom: 85%;
left: 100%;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
> * {
font-size: 11px;
font-weight: 300;
line-height: inherit;
letter-spacing: inherit;
}
ul {
padding-inline-start: 16px;
margin-block-end: 4px;
li {
padding-bottom: 8px;
.filterOption__descLabel {
// font-weight: bold;
}
}
}
}
}
}
.filterWrapper {
cursor: pointer;
&.expanded {
@include m-theme() {
box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
}
.filterHeader {
@include m-theme() {
border-color: themed($m-blue);
}
}
.unselectedOptionsContainer {
visibility: visible;
// @include m-theme() {
// box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.15);
// }
}
&:not(.dropUp) {
.filterHeader {
@include m-theme() {
border-radius: $rounded-top;
}
}
.unselectedOptionsContainer {
border-top: none;
border-radius: $rounded-bottom;
}
}
&.dropUp {
.filterHeader {
border-radius: $rounded-bottom;
}
.unselectedOptionsContainer {
bottom: 100%;
border-radius: $rounded-top;
border-bottom: none;
}
}
}
.filterHeader {
position: relative;
width: 100%;
padding: 8px 6px 6px 10px;
border-radius: 3px;
transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
background-color: themed($m-white);
color: rgba(themed($m-grey-200), 0.9);
}
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
.filterLabel {
margin-right: 10px;
}
i {
flex-grow: 0;
}
.option--selected {
margin-right: 8px;
@include m-theme() {
color: themed($m-grey-500);
}
}
}
.unselectedOptionsContainer {
position: absolute;
// display: none;
visibility: hidden;
width: 100%;
padding: 8px 6px 6px 10px;
border-radius: 3px;
left: 0px;
transition: box-shadow 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
border: 1px solid themed($m-blue);
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
}
.option {
padding: 5px 0;
}
}
.row {
display: flex;
justify-content: space-between;
}
.option {
border-radius: 3px;
white-space: nowrap;
@include m-theme() {
background-color: themed($m-white);
color: rgba(themed($m-grey-200), 0.9);
}
&.unavailable {
text-decoration: line-through;
@include m-theme() {
color: themed($m-grey-50);
}
}
&:hover:not(.unavailable) {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
}
.filterWrapper.isMobile {
.filterHeader {
i {
display: none;
}
}
.option--selected {
margin-right: 0;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsFilterComponent } from './filter.component';
describe('AnalyticsFilterComponent', () => {
let component: AnalyticsFilterComponent;
let fixture: ComponentFixture<AnalyticsFilterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsFilterComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsFilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ChangeDetectionStrategy,
} from '@angular/core';
import { Observable } from 'rxjs';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from '../../dashboard.service';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
import { Session } from '../../../../../services/session';
@Component({
selector: 'm-analytics__filter',
templateUrl: 'filter.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFilterComponent implements OnInit {
@Input() filter: Filter;
@Input() dropUp: boolean = false;
isMobile: boolean;
expanded = false;
options: Array<any> = [];
selectedOption: Option;
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session
) {}
ngOnInit() {
// this.subscription = this.analyticsService.timespan$.subscribe(timespan => {
// if (this.filter.id === 'timespan') {
// this.selectedOption =
// this.filter.options.find(option => option.id === timespan) ||
// this.filter.options[0];
// // TODO: make selected option at top of array?
// } else {
// this.selectedOption =
// this.filter.options.find(option => option.selected === true) ||
// this.filter.options[0];
// }
// });
this.selectedOption =
this.filter.options.find(option => option.selected === true) ||
this.filter.options[0];
this.isMobile = isMobileOrTablet();
}
updateFilter(option: Option) {
this.expanded = false;
this.selectedOption = option;
if (this.filter.id === 'timespan') {
this.analyticsService.updateTimespan(option.id);
return;
}
if (!this.selectedOption.available) {
return;
}
const selectedFilterStr = `${this.filter.id}::${option.id}`;
this.analyticsService.updateFilter(selectedFilterStr);
}
}
<div class="filtersContainer">
<!-- <ng-container *ngFor="let filter of filters$ | async"> -->
<ng-container *ngFor="let filter of filters">
<m-analytics__filter
class="filter"
[filter]="filter"
[dropUp]="true"
></m-analytics__filter>
</ng-container>
</div>
.filtersContainer {
display: flex;
flex-wrap: wrap;
padding: 16px;
position: relative;
margin-top: 36px;
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsFiltersComponent } from './filters.component';
describe('AnalyticsFiltersComponent', () => {
let component: AnalyticsFiltersComponent;
let fixture: ComponentFixture<AnalyticsFiltersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsFiltersComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsFiltersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { AnalyticsDashboardService, Filter } from '../../dashboard.service';
@Component({
selector: 'm-analytics__filters',
templateUrl: './filters.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFiltersComponent implements OnInit, OnDestroy {
subscription: Subscription;
filters$ = this.analyticsService.filters$;
filters: Filter[];
constructor(
private analyticsService: AnalyticsDashboardService,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
// TODO: remove all of this once channel search is ready
// Temporarily remove channel search from channel filter options
this.analyticsService.filters$.subscribe(filters => {
this.filters = filters;
const channelFilter = filters.find(filter => filter.id === 'channel');
channelFilter.options = channelFilter.options.filter(option => {
return option.id === 'all' || option.id === 'self';
});
this.filters.find(filter => filter.id === 'channel').options =
channelFilter.options;
this.detectChanges();
});
}
// TODO: remove all of this once channel search is ready
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {}
}
<section class="menu" [ngClass]="{ isMobile: isMobile }">
<div class="topbar" *ngIf="isMobile">
<i class="material-icons" (click)="expanded = true">menu</i>
<div class="pageTitle">
Analytics
</div>
</div>
<div
class="overlay"
[ngClass]="{ expanded: expanded }"
(click)="expanded = false"
></div>
<div class="sidebar" [ngClass]="{ expanded: expanded }">
<a class="profile" *ngIf="isMobile" [routerLink]="['/', user.username]">
<img
class="avatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/small/' + user.icontime"
/>
<div class="details">
<div class="name">{{ user.name }}</div>
<!-- TODO: remove username once subscriberCount is working -->
<div class="username">@{{ user.username }}</div>
<!-- TODO: get subscriberCount -->
<!-- <div class="subscribers">
{{ user.subscribers_count | abbr }} subscribers
</div> -->
</div>
</a>
<div class="sidebarTitle">
<h3>Analytics</h3>
<i class="material-icons" *ngIf="isMobile" (click)="expanded = false"
>keyboard_arrow_up</i
>
</div>
<div class="catContainer">
<!-- TODO: apply permissions from categories.default to cats sidebar -->
<div class="cat" *ngFor="let cat of cats">
<a
(click)="expanded = false"
[routerLink]="'../' + cat.id"
routerLinkActive="selected"
>{{ cat?.label }}</a
>
</div>
</div>
</div>
</section>
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
.page.isMobile m-analytics__menu {
margin-right: -32px;
flex: 0 1 0px;
}
m-analytics__menu {
display: block;
max-width: 160px;
// ----------------------------------------
// MOBILE
.isMobile {
.topbar {
z-index: 99999;
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-800);
}
i {
display: block;
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
}
}
.pageTitle {
font-size: 20px;
margin: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
// display: none;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.expanded {
// display: block;
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
z-index: 999999;
position: fixed;
top: 0;
bottom: 0;
left: -360px;
transition: left 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
padding: 0 24px;
width: 300px;
max-width: 70%;
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
left: 0;
}
.sidebarTitle {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
}
i {
font-size: 20px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
border-radius: 50%;
margin-right: 16px;
}
.details {
& > {
padding: 8px 0;
}
.name {
font-weight: bold;
}
.username {
@include m-theme() {
color: themed($m-grey-200);
}
}
.subscribers {
font-size: 11px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
a {
display: block;
padding: 6px 0;
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsMenuComponent } from './menu.component';
describe('AnalyticsMenuComponent', () => {
let component: AnalyticsMenuComponent;
let fixture: ComponentFixture<AnalyticsMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMenuComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {
ActivatedRoute,
Router,
ParamMap,
RoutesRecognized,
} from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import {
AnalyticsDashboardService,
Category,
UserState,
} from '../../dashboard.service';
import categories from '../../categories.default';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-analytics__menu',
templateUrl: './menu.component.html',
})
export class AnalyticsMenuComponent implements OnInit {
isMobile: boolean;
expanded: boolean = false;
minds;
user;
cats = categories;
// subscription: Subscription;
// paramsSubscription: Subscription;
// category$ = this.analyticsService.category$;
selectedCat: string;
constructor(
// public client: Client,
public route: ActivatedRoute,
// private router: Router,
// public analyticsService: AnalyticsDashboardService,
// private cd: ChangeDetectorRef
public session: Session
) {}
ngOnInit() {
this.minds = window.Minds;
this.isMobile = isMobileOrTablet();
this.user = this.session.getLoggedInUser();
}
// updateCategory(categoryId) {
// this.analyticsService.updateCategory(categoryId);
// }
// detectChanges() {
// this.cd.markForCheck();
// this.cd.detectChanges();
// }
}
<section class="metricsSection" [ngClass]="{ isMobile: isMobile }">
<!-- <div class="overflowFade--left"></div>
<div class="overflowScrollButton--left">
<i class="material-icons">chevron_left</i>
</div> -->
<div class="metricsWrapper">
<div class="metricsContainer" *ngIf="metrics$ | async as metrics">
<ng-container *ngFor="let metric of metrics">
<div
class="metric"
(click)="updateMetric(metric)"
[ngClass]="{ active: metric.visualisation }"
*ngIf="metric.permissionGranted"
>
<div class="metricLabel">
<span>{{ metric.label }}</span>
<m-tooltip [anchor]="top" icon="help">
{{ metric.description }}
</m-tooltip>
</div>
<div class="metricSummary" *ngIf="metric.summary">
<ng-container *ngIf="metric.unit === 'number'">
{{ metric.summary.current_value | number }}
</ng-container>
<ng-container *ngIf="metric.unit === 'usd'">
<span>$</span
>{{ metric.summary.current_value / 100 | number: '1.2-2' }}
</ng-container>
</div>
<div
*ngIf="metric.summary"
class="metricDelta"
[ngClass]="{
goodChange: metric.hasChanged && metric.positiveTrend,
badChange: metric.hasChanged && !metric.positiveTrend
}"
>
<i class="material-icons" *ngIf="metric.delta > 0">arrow_upward</i>
<i class="material-icons" *ngIf="metric.delta < 0"
>arrow_downward</i
>
<span>{{ metric.delta | percent: '1.0-1' }}</span>
</div>
</div>
</ng-container>
</div>
</div>
<!-- <div class="overflowFade--right"></div>
<div class="overflowScrollButton--right">
<i class="material-icons">chevron_right</i>
</div> -->
</section>
m-analytics__metrics {
.metricsSection {
position: relative;
[class*='overflowFade--'] {
position: absolute;
top: 0;
bottom: 0;
width: 24px;
z-index: 1;
&.overflowFade--right {
@include m-theme() {
right: 0;
background: linear-gradient(
to right,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
&.overflowFade--left {
@include m-theme() {
left: 0;
background: linear-gradient(
to left,
rgba(themed($m-white), 0) 0,
themed($m-white) 50%
);
}
}
}
[class*='overflowScrollButton--'] {
position: absolute;
top: 50%;
border-radius: 50%;
box-sizing: border-box;
z-index: 2;
transform: translateY(-50%);
transition: all 0.2s ease-in;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white);
box-shadow: 0px 0px 10px -3px rgba(themed($m-black-always), 0.3);
border: 1px solid themed($m-white);
}
&:hover {
@include m-theme() {
border: 1px solid themed($m-blue);
}
}
&.overflowScrollButton--right {
right: -12;
}
&.overflowScrollButton--left {
left: -12;
}
i {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
}
}
.metricsWrapper {
position: relative;
// overflow: hidden;
width: 100%;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
}
.metricsContainer {
scroll-snap-type: x mandatory;
position: relative;
display: flex;
flex-wrap: nowrap;
// overflow-x: auto;
// padding: 0 16px;
// &.metricsContainer::-webkit-scrollbar {
// display: none;
// }
.metric {
cursor: pointer;
scroll-snap-align: start;
flex: 0 0 auto;
width: 20%;
padding: 24px 20px 20px 20px;
font-size: 14px;
box-sizing: border-box;
@include m-theme() {
border-bottom: 8px solid themed($m-white);
}
@include m-theme() {
color: themed($m-grey-200);
}
&.active {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid themed($m-blue);
}
}
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
&:hover:not(.active) {
@include m-theme() {
background-color: rgba(themed($m-grey-100), 0.2);
border-bottom: 8px solid rgba(0, 0, 0, 0);
transition: background-color 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
}
.metricLabel {
white-space: nowrap;
}
m-tooltip {
vertical-align: middle;
margin-left: 4px;
}
.metricSummary {
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.metricDelta {
display: flex;
align-items: baseline;
padding-top: 4px;
font-size: 11px;
.material-icons {
transform: scaleX(0.7);
font-size: 11px;
font-weight: bold;
}
@include m-theme() {
&.goodChange {
color: themed($m-green);
}
&.badChange {
color: themed($m-red);
}
}
}
}
.m-tooltip {
i {
font-size: 12px;
@include m-theme() {
color: rgba(themed($m-grey-200), 0.7);
}
}
.m-tooltip--bubble {
z-index: 9999;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
background-color: themed($m-blue);
}
}
}
}
.metricsSection.isMobile {
.metricsContainer {
.metric {
flex: 1 0 auto;
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsMetricsComponent } from './metrics.component';
describe('AnalyticsMetricsComponent', () => {
let component: AnalyticsMetricsComponent;
let fixture: ComponentFixture<AnalyticsMetricsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsMetricsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsMetricsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric as MetricBase,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
interface MetricExtended extends MetricBase {
delta: number;
hasChanged: boolean;
positiveTrend: boolean;
permissionGranted: boolean;
}
export { MetricExtended as Metric };
@Component({
selector: 'm-analytics__metrics',
templateUrl: './metrics.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsMetricsComponent implements OnInit, OnDestroy {
data;
subscription: Subscription;
isMobile: boolean;
user;
userRoles: string[] = ['user'];
metrics$;
isOverflown = { left: false, right: false };
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
this.user = this.session.getLoggedInUser();
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
if (this.user.pro) {
this.userRoles.push('pro');
}
this.metrics$ = this.analyticsService.metrics$.pipe(
map(_metrics => {
const metrics = _metrics.map(metric => ({ ...metric })); // Clone to avoid updating
for (const metric of metrics) {
metric['permissionGranted'] = metric.permissions.some(role =>
this.userRoles.includes(role)
);
if (metric.summary) {
let delta;
if (metric.summary.comparison_value !== 0) {
delta =
(metric.summary.current_value -
metric.summary.comparison_value) /
(metric.summary.comparison_value || 0);
} else {
delta = 1;
}
metric['delta'] = delta;
metric['hasChanged'] = delta === 0 ? false : true;
if (
(delta > 0 && metric.summary.comparison_positive_inclination) ||
(delta < 0 && !metric.summary.comparison_positive_inclination)
) {
metric['positiveTrend'] = true;
} else {
metric['positiveTrend'] = false;
}
}
}
return metrics;
})
);
this.isMobile = isMobileOrTablet();
}
updateMetric(metric) {
this.analyticsService.updateMetric(metric.id);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
// this.subscription.unsubscribe();
}
checkOverflow() {
// element.scrollWidth - element.clientWidth
}
}
<div
class="m-analytics__searchSuggestions__list"
[hidden]="disabled || !active"
(mousedown)="mousedown($event)"
*ngIf="session.isLoggedIn()"
>
<ng-container *ngIf="!q">
<ng-container *ngFor="let suggestion of recent">
<a
class="m-analytics__searchSuggestions__suggestion"
*ngIf="suggestion.type == 'user'"
(click)="applyChannelFilter(suggestion)"
>
<img src="icon/{{ suggestion.guid }}/small" />
<div>
<div>{{ suggestion.name }}</div>
<div>@{{ suggestion.username }}</div>
</div>
</a>
</ng-container>
</ng-container>
<ng-container *ngIf="q">
<a
class="m-analytics__searchSuggestions__suggestion"
*ngFor="let suggestion of suggestions"
[routerLink]="['/', suggestion.username]"
>
<img src="icon/{{ suggestion.guid }}/small" />
<div>
<div>{{ suggestion.name }}</div>
<div>@{{ suggestion.username }}</div>
</div>
</a>
</ng-container>
</div>
m-analytics__searchSuggestions {
display: block;
.m-analytics__searchSuggestions__list {
padding: 0;
margin: 0;
position: absolute;
z-index: 5000;
box-sizing: border-box;
width: 100%;
@include m-theme() {
background-color: themed($m-white);
border: 1px solid themed($m-grey-50);
}
.m-analytics__searchSuggestions__suggestion {
cursor: pointer;
padding: 4px;
display: block;
text-decoration: none;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
color: themed($m-grey-700);
}
a {
display: flex;
flex-direction: row;
}
img {
border-radius: 50%;
margin: 0 8px 0 4px;
width: 36px;
height: 36px;
@include m-theme() {
background-color: themed($m-grey-800);
}
}
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsSearchSuggestionsComponent } from './search-suggestions.component';
describe('AnalyticsSearchSuggestionsComponent', () => {
let component: AnalyticsSearchSuggestionsComponent;
let fixture: ComponentFixture<AnalyticsSearchSuggestionsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsSearchSuggestionsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsSearchSuggestionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input, ChangeDetectorRef } from '@angular/core';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { RecentService } from '../../../../../services/ux/recent';
import { Session } from '../../../../../services/session';
import { Client } from '../../../../../services/api';
@Component({
selector: 'm-analytics__searchSuggestions',
templateUrl: './search-suggestions.component.html',
})
export class AnalyticsSearchSuggestionsComponent implements OnInit {
suggestions: Array<any> = [];
recent: any[];
q: string = '';
private searchTimeout;
@Input() active: boolean;
@Input('q') set _q(value: string) {
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.q = value || '';
if (!value) {
this.loadRecent();
this.suggestions = [];
return;
}
this.searchTimeout = setTimeout(async () => {
this.loadRecent();
try {
const response: any = await this.client.get('api/v2/search/suggest', {
q: value,
limit: 4,
});
this.suggestions = response.entities;
console.log(response.entities);
} catch (e) {
console.error(e);
this.suggestions = [];
}
}, 300);
}
constructor(
private analyticsService: AnalyticsDashboardService,
public session: Session,
public client: Client,
public recentService: RecentService,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
this.loadRecent();
}
applyChannelFilter(suggestion) {
// TODO: remove dummy data
let selection = suggestion.guid || 'test';
const selectedFilterStr = `channel::${selection}`;
// TODO: enable admin gate
// if (this.session.isAdmin()) {
this.analyticsService.updateFilter(selectedFilterStr);
// }
}
loadRecent() {
if (this.session.getLoggedInUser()) {
// TODO: Q - Does this only store channels?
this.recent = this.recentService.fetch('recent', 6);
}
}
mousedown(e) {
e.preventDefault();
setTimeout(() => {
this.active = false;
this.detectChanges();
}, 300);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<div class="mdl-textfield mdl-js-textfield">
<i class="material-icons" (click)="setFocus()">search</i>
<input
[(ngModel)]="q"
(focus)="focus()"
(blur)="blur()"
name="q"
class="mdl-textfield__input"
type="text"
id="search"
autocomplete="off"
placeholder="Filter by channel"
#searchInput
/>
<!-- <label class="mdl-textfield__label" for="search">abc</label> -->
<m-analytics__searchSuggestions
[q]="q"
[active]="active"
></m-analytics__searchSuggestions>
</div>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsSearchComponent } from './search.component';
describe('AnalyticsSearchComponent', () => {
let component: AnalyticsSearchComponent;
let fixture: ComponentFixture<AnalyticsSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsSearchComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, ViewChild, Input, ElementRef } from '@angular/core';
// import { Observable, Subscription } from 'rxjs';
import {
AnalyticsDashboardService,
Filter,
Option,
} from '../../dashboard.service';
import { Session } from '../../../../../services/session';
@Component({
selector: 'm-analytics__search',
templateUrl: './search.component.html',
})
export class AnalyticsSearchComponent implements OnInit {
active: boolean;
q: string;
id: string;
hasSearchContext: boolean = true;
searchContext: string | Promise<string> = '';
@ViewChild('searchInput', { static: true }) searchInput: ElementRef;
constructor(
private analyticsService: AnalyticsDashboardService,
private session: Session
) {}
ngOnInit() {}
search() {
// const qs: { q; ref; id? } = { q: this.q, ref: 'top' };
// if (this.id) {
// qs.id = this.id;
// }
// if (this.featureService.has('top-feeds')) {
// this.router.navigate([
// '/newsfeed/global/top',
// { query: this.q, period: '24h' },
// ]);
// } else {
// this.router.navigate(['search', qs]);
// }
}
keyup(e) {
if (e.keyCode === 13 && this.session.isLoggedIn()) {
this.search();
this.unsetFocus();
}
// TODO: allow to tab through suggestions?
}
focus() {
this.active = true;
}
blur() {
setTimeout(() => (this.active = false), 100);
}
setFocus() {
if (this.searchInput.nativeElement) {
this.searchInput.nativeElement.focus();
}
}
unsetFocus() {
if (this.searchInput.nativeElement) {
this.searchInput.nativeElement.blur();
}
}
// protected getActiveSearchContext(fragments: string[]) {
// this.searchContext = ''; // this would be 'channels'
// this.id = ''; //this would be a guid
// fragments.forEach((fragment: string) => {
// let param = fragment.split('=');
// if (param[0] === 'q') {
// this.q = decodeURIComponent(param[1]);
// }
// if (param[0] === 'id') {
// this.id = param[1];
// this.searchContext = this.context.resolveLabel(
// decodeURIComponent(param[1])
// );
// }
// if (param[0] == 'type' && !this.searchContext) {
// this.searchContext = this.context.resolveStaticLabel(
// decodeURIComponent(param[1])
// );
// }
// });
// }
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AnalyticsTableComponent } from './table.component';
describe('AnalyticsTableComponent', () => {
let component: AnalyticsTableComponent;
let fixture: ComponentFixture<AnalyticsTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AnalyticsTableComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AnalyticsTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
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.
<p>
analytics__layout--summary works!
</p>
<p>
analytics__layout--table works!
</p>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.