Reformat code
This commit is contained in:
parent
6e27539eca
commit
420cd788c4
94 changed files with 10592 additions and 2608 deletions
8
.prettierrc
Normal file
8
.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
67
angular.json
67
angular.json
|
|
@ -22,13 +22,8 @@
|
|||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"es5BrowserSupport": true
|
||||
},
|
||||
|
|
@ -40,7 +35,7 @@
|
|||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"optimization": false,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
|
|
@ -76,65 +71,15 @@
|
|||
"browserTarget": "frontend:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"frontend-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"prefix": "",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "frontend:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "frontend:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "frontend"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to frontend!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
3912
package-lock.json
generated
3912
package-lock.json
generated
File diff suppressed because it is too large
Load diff
23
package.json
23
package.json
|
|
@ -5,17 +5,18 @@
|
|||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"build:prod": "ng build --prod --base-href /life-qa/",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~7.2.0",
|
||||
"@angular/animations": "^7.2.15",
|
||||
"@angular/cdk": "^7.3.7",
|
||||
"@angular/common": "~7.2.0",
|
||||
"@angular/compiler": "~7.2.0",
|
||||
"@angular/core": "~7.2.0",
|
||||
"@angular/forms": "~7.2.0",
|
||||
"@angular/material": "^7.3.7",
|
||||
"@angular/platform-browser": "~7.2.0",
|
||||
"@angular/platform-browser-dynamic": "~7.2.0",
|
||||
"@angular/router": "~7.2.0",
|
||||
|
|
@ -29,18 +30,14 @@
|
|||
"@angular/cli": "~7.3.8",
|
||||
"@angular/compiler-cli": "~7.2.0",
|
||||
"@angular/language-service": "~7.2.0",
|
||||
"@types/node": "~8.9.4",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.5.0",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.0.0",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "~1.1.2",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.4.0",
|
||||
"husky": "^3.0.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.18.2",
|
||||
"pretty-quick": "^1.11.1",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.2.2"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { PagesComponent } from './components/pages/pages.component';
|
||||
|
||||
const routes: Routes = [];
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PagesComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
export class AppRoutingModule {}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,2 @@
|
|||
<!--The content below is only a placeholder and can be replaced.-->
|
||||
<div style="text-align:center">
|
||||
<h1>
|
||||
Welcome to {{ title }}!
|
||||
</h1>
|
||||
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
|
||||
</div>
|
||||
<h2>Here are some links to help you start: </h2>
|
||||
<ul>
|
||||
<li>
|
||||
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
|
||||
</li>
|
||||
<li>
|
||||
<h2><a target="_blank" rel="noopener" href="https://angular.io/cli">CLI Documentation</a></h2>
|
||||
</li>
|
||||
<li>
|
||||
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<app-modal></app-modal>
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'frontend'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('frontend');
|
||||
});
|
||||
|
||||
it('should render title in a h1 tag', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontend!');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from './services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
|
|
|||
|
|
@ -3,16 +3,48 @@ import { NgModule } from '@angular/core';
|
|||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { PageComponent } from './components/pages/page/page.component';
|
||||
import { TowerComponent } from './components/pages/page/tower/tower.component';
|
||||
import { DoubleSliderComponent } from './components/shared/double-slider/double-slider.component';
|
||||
import { PagesComponent } from './components/pages/pages.component';
|
||||
import { SelectAddComponent } from './components/shared/select-add/select-add.component';
|
||||
import { ModalComponent } from './components/modal/modal.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BlockComponent } from './components/pages/page/tower/block/block.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { EditBlockComponent } from './components/modal/modals/edit-block/edit-block.component';
|
||||
import { SettingsComponent } from './components/modal/modals/settings/settings.component';
|
||||
import { RemoveTowerComponent } from './components/modal/modals/remove-tower/remove-tower.component';
|
||||
import { RemovePageComponent } from './components/modal/modals/remove-page/remove-page.component';
|
||||
import { GetStartedComponent } from './components/modal/modals/get-started/get-started.component';
|
||||
import { CreateBlockComponent } from './components/modal/modals/create-block/create-block.component';
|
||||
import { RemoveBlockComponent } from './components/modal/modals/remove-block/remove-block.component';
|
||||
import { ToggleComponent } from './components/shared/toggle/toggle.component';
|
||||
import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule
|
||||
AppComponent,
|
||||
PageComponent,
|
||||
TowerComponent,
|
||||
DoubleSliderComponent,
|
||||
PagesComponent,
|
||||
SelectAddComponent,
|
||||
ModalComponent,
|
||||
BlockComponent,
|
||||
EditBlockComponent,
|
||||
SettingsComponent,
|
||||
RemoveTowerComponent,
|
||||
RemovePageComponent,
|
||||
GetStartedComponent,
|
||||
CreateBlockComponent,
|
||||
RemoveBlockComponent,
|
||||
ToggleComponent,
|
||||
TasksComponent
|
||||
],
|
||||
imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
|
|
|||
9
src/app/components/modal/modal.component.html
Normal file
9
src/app/components/modal/modal.component.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<main class="{{ modalService.active ? 'active' : '' }}" [ngSwitch]="modalService.active?.type">
|
||||
<app-create-block *ngSwitchCase="ModalType.createBlock"></app-create-block>
|
||||
<app-edit-block *ngSwitchCase="ModalType.editBlock"></app-edit-block>
|
||||
<app-remove-block *ngSwitchCase="ModalType.removeBlock"></app-remove-block>
|
||||
<app-remove-tower *ngSwitchCase="ModalType.removeTower"></app-remove-tower>
|
||||
<app-settings *ngSwitchCase="ModalType.settings"></app-settings>
|
||||
<app-get-started *ngSwitchCase="ModalType.getStarted"></app-get-started>
|
||||
<app-remove-page *ngSwitchCase="ModalType.removePage"></app-remove-page>
|
||||
</main>
|
||||
29
src/app/components/modal/modal.component.scss
Normal file
29
src/app/components/modal/modal.component.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../styles';
|
||||
|
||||
main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include center-child();
|
||||
|
||||
padding: var(--large-padding);
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
background: $background-gradient;
|
||||
transition: opacity 300ms;
|
||||
|
||||
&:not(.active) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: var(--medium-padding);
|
||||
}
|
||||
}
|
||||
20
src/app/components/modal/modal.component.ts
Normal file
20
src/app/components/modal/modal.component.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService, ModalType } from '../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal',
|
||||
templateUrl: './modal.component.html',
|
||||
styleUrls: ['./modal.component.scss']
|
||||
})
|
||||
export class ModalComponent {
|
||||
// Needed for accessing the enum from html.
|
||||
ModalType = ModalType;
|
||||
|
||||
constructor(public modalService: ModalService) {
|
||||
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
this.modalService.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Create an item</h1>
|
||||
</div>
|
||||
|
||||
<div class="select-add-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-select-add
|
||||
class="select"
|
||||
[options]="modalService.active.input"
|
||||
[default]="modalService.active.input[0]"
|
||||
[alwaysDropShadow]="true"
|
||||
[onlyShadowBorder]="true"
|
||||
[placeholder]="'Tag this item…'"
|
||||
(value)="selected = $event"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<textarea placeholder="Write a description here…" [(ngModel)]="description"></textarea>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="false"
|
||||
(value)="isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="submit()">Create</button>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include card();
|
||||
box-shadow: $shadow;
|
||||
|
||||
width: 66vw;
|
||||
max-width: 400px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
position: relative;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-block',
|
||||
templateUrl: './create-block.component.html',
|
||||
styleUrls: ['./create-block.component.scss']
|
||||
})
|
||||
export class CreateBlockComponent {
|
||||
selected: string;
|
||||
description: string = null;
|
||||
isDone: boolean;
|
||||
|
||||
constructor(public modalService: ModalService) {}
|
||||
|
||||
submit() {
|
||||
this.modalService.submit({
|
||||
selected: this.selected,
|
||||
description: this.description,
|
||||
isDone: this.isDone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>View item</h1>
|
||||
</div>
|
||||
|
||||
<div class="select-add-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-select-add
|
||||
class="select"
|
||||
[options]="modalService.active.input.options"
|
||||
[default]="modalService.active.input.options[0]"
|
||||
[alwaysDropShadow]="true"
|
||||
[onlyShadowBorder]="true"
|
||||
[placeholder]="'Tag this item…'"
|
||||
(value)="selected = $event"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<textarea placeholder="Write a description here…" [(ngModel)]="modalService.active.input.description"></textarea>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="modalService.active.input.isDone"
|
||||
(value)="modalService.active.input.isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="submit()">Modify</button>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include card();
|
||||
box-shadow: $shadow;
|
||||
|
||||
width: 66vw;
|
||||
max-width: 400px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
position: relative;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-block',
|
||||
templateUrl: './edit-block.component.html',
|
||||
styleUrls: ['./edit-block.component.scss']
|
||||
})
|
||||
export class EditBlockComponent {
|
||||
selected: string;
|
||||
|
||||
constructor(public modalService: ModalService) {}
|
||||
|
||||
submit() {
|
||||
this.modalService.submit({
|
||||
selected: this.selected,
|
||||
description: this.modalService.active.input.description,
|
||||
isDone: this.modalService.active.input.isDone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
get-started works!
|
||||
</p>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-get-started',
|
||||
templateUrl: './get-started.component.html',
|
||||
styleUrls: ['./get-started.component.scss']
|
||||
})
|
||||
export class GetStartedComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
remove-block works!
|
||||
</p>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-block',
|
||||
templateUrl: './remove-block.component.html',
|
||||
styleUrls: ['./remove-block.component.scss']
|
||||
})
|
||||
export class RemoveBlockComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<section>
|
||||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Are you sure?</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You are trying to remove <strong>{{ this.modalService.active.input }}</strong
|
||||
>.
|
||||
</p>
|
||||
|
||||
<button (click)="modalService.submit()">Remove</button>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
section {
|
||||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
|
||||
position: relative;
|
||||
box-shadow: $shadow;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-page',
|
||||
templateUrl: './remove-page.component.html',
|
||||
styleUrls: ['./remove-page.component.scss']
|
||||
})
|
||||
export class RemovePageComponent {
|
||||
constructor(public modalService: ModalService) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<section>
|
||||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Are you sure?</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You are trying to remove
|
||||
<span [ngStyle]="{ color: modalService.active.input.baseColor.toString() }">{{
|
||||
modalService.active.input.name ? modalService.active.input.name : 'an unnamed tower'
|
||||
}}</span
|
||||
>.
|
||||
</p>
|
||||
|
||||
<button (click)="modalService.submit()">Remove</button>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
section {
|
||||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
|
||||
position: relative;
|
||||
box-shadow: $shadow;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-tower',
|
||||
templateUrl: './remove-tower.component.html',
|
||||
styleUrls: ['./remove-tower.component.scss']
|
||||
})
|
||||
export class RemoveTowerComponent {
|
||||
constructor(public modalService: ModalService) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'Hide create tower button'"
|
||||
[afterText]="'Show create tower button'"
|
||||
[default]="!dataService.active.userData?.hideCreateTowerButton"
|
||||
(value)="dataService.active.userData.hideCreateTowerButton = !$event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<p *ngIf="dataService.active?.towers.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
|
||||
|
||||
<button (click)="deletePage()">Delete current page</button>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
|
||||
position: relative;
|
||||
box-shadow: $shadow;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--medium-font-size);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { DataService } from '../../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent {
|
||||
constructor(public modalService: ModalService, public dataService: DataService) {}
|
||||
|
||||
async deletePage() {
|
||||
try {
|
||||
await this.modalService.showRemovePage(this.dataService.active.name);
|
||||
this.dataService.remove();
|
||||
this.modalService.submit();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/app/components/pages/page/page.component.html
Normal file
35
src/app/components/pages/page/page.component.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<button
|
||||
*ngIf="page && page.towers.length < 5 && !dataService.active?.userData?.hideCreateTowerButton"
|
||||
(click)="createTower()"
|
||||
>
|
||||
Create tower
|
||||
</button>
|
||||
|
||||
<section class="towers" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropDrag($event)">
|
||||
<app-tower
|
||||
*ngFor="let tower of page?.towers"
|
||||
[tower]="tower"
|
||||
[dateRange]="dateRange"
|
||||
cdkDrag
|
||||
(cdkDragStarted)="startDrag(page.towers.indexOf(tower))"
|
||||
></app-tower>
|
||||
</section>
|
||||
|
||||
<img
|
||||
*ngIf="isDragging"
|
||||
src="assets/trash.svg"
|
||||
alt="trashcan"
|
||||
(pointerenter)="trashEnter()"
|
||||
(pointerleave)="trashExit()"
|
||||
(pointerup)="removeTower()"
|
||||
/>
|
||||
|
||||
<div class="double-slider-container">
|
||||
<app-double-slider
|
||||
*ngIf="!isDragging && dates.length >= MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER"
|
||||
[values]="dates"
|
||||
[labels]="dateLabels"
|
||||
(lowerBound)="startDate = $event"
|
||||
(upperBound)="endDate = $event"
|
||||
></app-double-slider>
|
||||
</div>
|
||||
53
src/app/components/pages/page/page.component.scss
Normal file
53
src/app/components/pages/page/page.component.scss
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.towers {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
flex: 1 0 auto;
|
||||
|
||||
transition: box-shadow $short-animation-time;
|
||||
|
||||
&.cdk-drop-list-dragging {
|
||||
*:not(.cdk-drag-placeholder) {
|
||||
transition: transform $long-animation-time cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@include inner-spacing(var(--medium-padding), $horizontal: true);
|
||||
}
|
||||
|
||||
.double-slider-container {
|
||||
@media (max-height: $min-height) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include square(47.33333px);
|
||||
padding: 47.3333px 0;
|
||||
margin: auto;
|
||||
|
||||
position: relative;
|
||||
z-index: 1500;
|
||||
|
||||
transition: transform $long-animation-time;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/app/components/pages/page/page.component.ts
Normal file
98
src/app/components/pages/page/page.component.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Page } from '../../../model/page';
|
||||
import { ModalService } from '../../../services/modal.service';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page',
|
||||
templateUrl: './page.component.html',
|
||||
styleUrls: ['./page.component.scss']
|
||||
})
|
||||
export class PageComponent {
|
||||
private _page: Page;
|
||||
@Input() set page(value: Page) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._page = value;
|
||||
value.subscribe(() => this.updateDates());
|
||||
this.updateDates();
|
||||
}
|
||||
|
||||
get page(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
readonly MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER = 3;
|
||||
|
||||
isDragging = false;
|
||||
draggedTowerIndex: number;
|
||||
nearTrashcan = false;
|
||||
|
||||
dates: Date[] = [];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
|
||||
get dateLabels(): string[] {
|
||||
return this.dates.map(d => d.toLocaleDateString());
|
||||
}
|
||||
|
||||
get dateRange(): { from: Date; to: Date } {
|
||||
return this.dates.length >= this.MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER
|
||||
? {
|
||||
from: this.startDate,
|
||||
to: this.endDate
|
||||
}
|
||||
: {
|
||||
from: new Date(0, 0),
|
||||
to: new Date(10000, 0)
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private modalService: ModalService, public dataService: DataService) {}
|
||||
|
||||
createTower() {
|
||||
this.page.addTower();
|
||||
}
|
||||
|
||||
dropDrag(event: any) {
|
||||
this.page.moveTower(event);
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
startDrag(id: number) {
|
||||
this.draggedTowerIndex = id;
|
||||
this.isDragging = true;
|
||||
}
|
||||
|
||||
trashEnter() {
|
||||
this.nearTrashcan = true;
|
||||
window.document.querySelector('.cdk-drag-preview').className += ' trash-highlight';
|
||||
}
|
||||
|
||||
trashExit() {
|
||||
this.nearTrashcan = false;
|
||||
const elem = window.document.querySelector('.cdk-drag-preview');
|
||||
elem.className = elem.className
|
||||
.split(' ')
|
||||
.slice(0, -1)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
async removeTower() {
|
||||
try {
|
||||
const tower = this.page.towers[this.draggedTowerIndex];
|
||||
await this.modalService.showRemoveTower(tower);
|
||||
this.page.removeTower(tower);
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
private updateDates() {
|
||||
this.dates = this.page.towers
|
||||
.reduce((all, t) => [...t.blocks.map(b => b.created), ...all], [])
|
||||
.sort((d1, d2) => d1.getTime() - d2.getTime());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<div [ngStyle]="{ 'background-color': block.color }" (click)="handleClick()"></div>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
@import '../../../../../../styles';
|
||||
|
||||
:host {
|
||||
position: relative;
|
||||
width: calc(100% / 6);
|
||||
padding-bottom: calc(100% / 6);
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include gravitate();
|
||||
}
|
||||
}
|
||||
32
src/app/components/pages/page/tower/block/block.component.ts
Normal file
32
src/app/components/pages/page/tower/block/block.component.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Block } from '../../../../../model/block';
|
||||
import { ModalService } from '../../../../../services/modal.service';
|
||||
import { Tower } from '../../../../../model/tower';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
templateUrl: './block.component.html',
|
||||
styleUrls: ['./block.component.scss']
|
||||
})
|
||||
export class BlockComponent {
|
||||
@Input() block: Block;
|
||||
@Input() tower: Tower;
|
||||
|
||||
constructor(private modalService: ModalService) {}
|
||||
|
||||
async handleClick() {
|
||||
try {
|
||||
const { selected, description, isDone } = await this.modalService.showEditBlock({
|
||||
options: this.tower.tags,
|
||||
default: this.block.tag,
|
||||
description: this.block.description,
|
||||
isDone: this.block.isDone
|
||||
});
|
||||
this.block.tag = selected;
|
||||
this.block.description = description;
|
||||
this.block.isDone = isDone;
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="container">
|
||||
<p class="header" (click)="isOpen = !isOpen">
|
||||
{{ tasks.length == 0 ? 'no' : '' }}
|
||||
<strong>
|
||||
{{ tasks.length == 0 ? '' : tasks.length }}
|
||||
</strong>
|
||||
{{ tasks.length == 1 ? 'task' : 'tasks' }}
|
||||
</p>
|
||||
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
|
||||
<p
|
||||
*ngFor="let task of tasks"
|
||||
(click)="handleClick(task)"
|
||||
[innerText]="task.description ? task.description : 'unknown'"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
@import '../../../../../../styles';
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.container {
|
||||
@include card();
|
||||
box-shadow: $shadow-border;
|
||||
padding: var(--small-padding);
|
||||
margin: var(--small-padding);
|
||||
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
font-size: var(--medium-font-size);
|
||||
}
|
||||
|
||||
.all-task {
|
||||
@include inner-spacing(var(--small-padding));
|
||||
|
||||
:first-child {
|
||||
margin-top: var(--small-padding);
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow-y: hidden;
|
||||
|
||||
height: 0;
|
||||
transition: height $long-animation-time;
|
||||
|
||||
p {
|
||||
max-width: 60px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/app/components/pages/page/tower/tasks/tasks.component.ts
Normal file
41
src/app/components/pages/page/tower/tasks/tasks.component.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { Block } from '../../../../../model/block';
|
||||
import { Tower } from '../../../../../model/tower';
|
||||
import { ModalService } from '../../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss']
|
||||
})
|
||||
export class TasksComponent implements OnInit {
|
||||
@Input() tasks: Block[];
|
||||
@Input() tower: Tower;
|
||||
|
||||
@Input() isOpen = false;
|
||||
|
||||
@ViewChild('allTask') allTask: ElementRef;
|
||||
|
||||
constructor(private modalService: ModalService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
async handleClick(block: Block) {
|
||||
try {
|
||||
const { selected, description, isDone } = await this.modalService.showEditBlock({
|
||||
options: this.tower.tags,
|
||||
default: block.tag,
|
||||
description: block.description,
|
||||
isDone: block.isDone
|
||||
});
|
||||
block.tag = selected;
|
||||
block.description = description;
|
||||
if (!block.isDone && isDone) {
|
||||
block.created = new Date();
|
||||
}
|
||||
block.isDone = isDone;
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/app/components/pages/page/tower/tower.component.html
Normal file
22
src/app/components/pages/page/tower/tower.component.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<div class="tower">
|
||||
<div class="container">
|
||||
<div class="tasks-container">
|
||||
<app-tasks [tasks]="tasks" [tower]="tower"></app-tasks>
|
||||
</div>
|
||||
|
||||
<img src="assets/plus-sign.svg" alt="add item" (click)="addBlock()" />
|
||||
|
||||
<div class="block-container {{ isFalling ? 'falling' : '' }}">
|
||||
<app-block *ngFor="let block of drawableBlocks" [block]="block" [tower]="tower"></app-block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="tower-name" class="hidden">Card name</label>
|
||||
<input
|
||||
id="tower-name"
|
||||
type="text"
|
||||
placeholder="…"
|
||||
[(ngModel)]="tower.name"
|
||||
[ngStyle]="{ color: tower.baseColor.toString() }"
|
||||
/>
|
||||
</div>
|
||||
140
src/app/components/pages/page/tower/tower.component.scss
Normal file
140
src/app/components/pages/page/tower/tower.component.scss
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
cursor: pointer;
|
||||
|
||||
&.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.cdk-drag-preview,
|
||||
&:hover {
|
||||
div {
|
||||
.container {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.trash-highlight {
|
||||
.container {
|
||||
transform: scale(0.75);
|
||||
position: relative;
|
||||
|
||||
:before {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tower {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include inner-spacing(var(--small-padding));
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
|
||||
@include card();
|
||||
|
||||
transition: transform $short-animation-time, box-shadow $long-animation-time;
|
||||
|
||||
@include inner-spacing(var(--medium-padding));
|
||||
|
||||
width: 100%;
|
||||
|
||||
:before {
|
||||
content: '';
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: red;
|
||||
|
||||
opacity: 0;
|
||||
border-radius: var(--border-radius);
|
||||
transition: opacity $short-animation-time;
|
||||
}
|
||||
|
||||
.tasks-container {
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
height: 56px;
|
||||
@media (max-width: $mobile-width) {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
opacity: 0.33;
|
||||
transition: opacity $long-animation-time;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-end;
|
||||
transform: scaleY(-1);
|
||||
flex: 1;
|
||||
|
||||
&.falling > *:last-child {
|
||||
animation: falling 1.5s cubic-bezier(0.5, 0, 1, 0) forwards;
|
||||
|
||||
@keyframes falling {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(500%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
font-size: var(--medium-font-size);
|
||||
text-align: center;
|
||||
@media (min-width: $mobile-width) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/app/components/pages/page/tower/tower.component.ts
Normal file
49
src/app/components/pages/page/tower/tower.component.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||
import { Tower } from '../../../../model/tower';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { Block } from '../../../../model/block';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tower',
|
||||
templateUrl: './tower.component.html',
|
||||
styleUrls: ['./tower.component.scss']
|
||||
})
|
||||
export class TowerComponent {
|
||||
@Input() set dateRange(value: { from: Date; to: Date }) {
|
||||
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
|
||||
return;
|
||||
}
|
||||
this._dateRange = value;
|
||||
}
|
||||
|
||||
get dateRange(): { from: Date; to: Date } {
|
||||
return this._dateRange;
|
||||
}
|
||||
|
||||
public constructor(private modalService: ModalService) {}
|
||||
|
||||
get drawableBlocks(): Block[] {
|
||||
return this.tower.blocks.filter(
|
||||
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
|
||||
);
|
||||
}
|
||||
|
||||
get tasks(): Block[] {
|
||||
return this.tower.blocks.filter(block => !block.isDone);
|
||||
}
|
||||
|
||||
@Input() tower: Tower;
|
||||
|
||||
_dateRange: { from: Date; to: Date };
|
||||
|
||||
isFalling = true;
|
||||
|
||||
public async addBlock() {
|
||||
try {
|
||||
const { selected: tag, description, isDone } = await this.modalService.showCreateBlock(this.tower.tags);
|
||||
this.tower.addBlock({ tag, description, isDone });
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/app/components/pages/pages.component.html
Normal file
18
src/app/components/pages/pages.component.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<div class="select-add-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-select-add
|
||||
[options]="dataService.pageNames"
|
||||
[default]="dataService.active?.name"
|
||||
(value)="selectPage($event)"
|
||||
[placeholder]="'Add a new page…'"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<div class="page-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-page *ngIf="dataService.active !== null" [page]="dataService.active"></app-page>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="openSettings()">Settings</button>
|
||||
21
src/app/components/pages/pages.component.scss
Normal file
21
src/app/components/pages/pages.component.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@import '../../../styles';
|
||||
|
||||
:host {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.select-add-container {
|
||||
width: 250px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
40
src/app/components/pages/pages.component.ts
Normal file
40
src/app/components/pages/pages.component.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Page } from '../../model/page';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { ModalService } from '../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pages',
|
||||
templateUrl: './pages.component.html',
|
||||
styleUrls: ['./pages.component.scss']
|
||||
})
|
||||
export class PagesComponent {
|
||||
@ViewChild('top') top: ElementRef;
|
||||
@ViewChild('page') page: ElementRef;
|
||||
@ViewChild('bottom') bottom: ElementRef;
|
||||
|
||||
constructor(public dataService: DataService, private modalService: ModalService) {}
|
||||
|
||||
async selectPage(selected: string) {
|
||||
if (!this.dataService.pageNames.includes(selected)) {
|
||||
const page = new Page({
|
||||
name: selected,
|
||||
towers: [],
|
||||
userData: {}
|
||||
});
|
||||
|
||||
this.dataService.push(page);
|
||||
page.addTower();
|
||||
}
|
||||
|
||||
await this.dataService.changeActiveByName(selected);
|
||||
}
|
||||
|
||||
async openSettings() {
|
||||
try {
|
||||
await this.modalService.showSettings();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<div class="container">
|
||||
<label for="date-selector-1">date selector 1</label>
|
||||
<label for="date-selector-2">date selector 2</label>
|
||||
<input id="date-selector-1" type="range" min="0" [max]="MAX - 1" [(ngModel)]="oneValue" />
|
||||
<input id="date-selector-2" type="range" min="0" [max]="MAX - 1" [(ngModel)]="otherValue" />
|
||||
<div class="value-container">
|
||||
<span
|
||||
*ngFor="let i of drawnLabelsIndices"
|
||||
[innerHTML]="drawnLabels[i]"
|
||||
[ngStyle]="{ transform: getOffset(i) }"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
$height: 70px;
|
||||
$width: 300px;
|
||||
$slider-size: 40px;
|
||||
|
||||
.container {
|
||||
width: $width;
|
||||
height: $height;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin: $slider-size / 2 auto 0 auto;
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
|
||||
height: $slider-size;
|
||||
width: $slider-size;
|
||||
border-radius: 1000px;
|
||||
|
||||
background-color: $light-color;
|
||||
transform-origin: center center;
|
||||
transform: translateY(-$slider-size / 2 + $line-height / 2);
|
||||
|
||||
transition: box-shadow $long-animation-time, transform $long-animation-time;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow;
|
||||
transform: translateY(-$slider-size / 2 + $line-height / 2) scale(1.1);
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
-webkit-appearance: none;
|
||||
|
||||
width: 100%;
|
||||
height: $line-height;
|
||||
background-color: $text-color;
|
||||
border-radius: 1000px;
|
||||
}
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.value-container {
|
||||
@include small-text();
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { range } from '../../../utils/range';
|
||||
|
||||
@Component({
|
||||
selector: 'app-double-slider',
|
||||
templateUrl: './double-slider.component.html',
|
||||
styleUrls: ['./double-slider.component.scss']
|
||||
})
|
||||
export class DoubleSliderComponent {
|
||||
@Input() labels: string[];
|
||||
|
||||
@Input() set values(values: any[]) {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._values = values;
|
||||
this.calculateLabels();
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
get values(): any[] {
|
||||
return this._values;
|
||||
}
|
||||
|
||||
get oneValue(): number {
|
||||
return this._oneValue;
|
||||
}
|
||||
|
||||
set oneValue(value: number) {
|
||||
this._oneValue = value;
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
get otherValue(): number {
|
||||
return this._otherValue;
|
||||
}
|
||||
|
||||
set otherValue(value: number) {
|
||||
this._otherValue = value;
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
private _values: any[];
|
||||
|
||||
@Output() lowerBound: EventEmitter<any> = new EventEmitter();
|
||||
@Output() upperBound: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
drawnLabels: string[];
|
||||
|
||||
readonly MAX = 100;
|
||||
|
||||
private _oneValue = 0;
|
||||
|
||||
private _otherValue: number = this.MAX - 1;
|
||||
|
||||
drawnLabelsIndices: Iterable<number>;
|
||||
|
||||
private calculateLabels() {
|
||||
const labelCount = 6;
|
||||
const jumpLength = Math.round(this.labels.length / labelCount);
|
||||
this.drawnLabels = this.labels.filter((_, index) => index % jumpLength === 0);
|
||||
this.drawnLabelsIndices = range({ max: this.drawnLabels.length });
|
||||
}
|
||||
|
||||
private indexFromValue(value: number): number {
|
||||
return Math.floor((value / this.MAX) * this.values.length);
|
||||
}
|
||||
|
||||
getOffset(index: number): string {
|
||||
const labelIndex = index / this.drawnLabels.length;
|
||||
const slider1Index = this.oneValue / this.MAX - 0.1;
|
||||
const slider2Index = this.otherValue / this.MAX - 0.1;
|
||||
|
||||
const dist = (a, b) => Math.abs(a - b);
|
||||
|
||||
const labelSliderDistance = Math.min(dist(labelIndex, slider1Index), dist(labelIndex, slider2Index));
|
||||
|
||||
const ACTIVE_ZONE = 0.2;
|
||||
const BASE_TRANSFORM = 'translateX(-50%) rotate(-45deg) translateY(100%)';
|
||||
|
||||
if (labelSliderDistance > ACTIVE_ZONE) {
|
||||
return BASE_TRANSFORM;
|
||||
}
|
||||
|
||||
return `translateY(${Math.pow((ACTIVE_ZONE - labelSliderDistance) / ACTIVE_ZONE, 1) * 30}px) ${BASE_TRANSFORM}`;
|
||||
}
|
||||
|
||||
private emitValue() {
|
||||
if (this.oneValue < this.otherValue) {
|
||||
this.lowerBound.emit(this.values[this.indexFromValue(this.oneValue)]);
|
||||
this.upperBound.emit(this.values[this.indexFromValue(this.otherValue)]);
|
||||
} else {
|
||||
this.lowerBound.emit(this.values[this.indexFromValue(this.otherValue)]);
|
||||
this.upperBound.emit(this.values[this.indexFromValue(this.oneValue)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<div class="select-add {{ onlyShadowBorder ? 'shadow-border' : '' }}">
|
||||
<div #top class="top" (click)="toggle()">
|
||||
<p [innerHTML]="selected ? selected : placeholder"></p>
|
||||
<img src="assets/arrow.svg" [className]="isOpen ? 'upside-down' : ''" alt="arrow" />
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<div #bottom class="bottom {{ isOpen ? 'open' : '' }}">
|
||||
<p *ngFor="let option of otherOptions" [innerHTML]="option" (click)="select(option)"></p>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
*ngIf="options.length <= maxItemCount"
|
||||
placeholder="Add a value…"
|
||||
[(ngModel)]="newOption"
|
||||
(keyup)="handleKeys($event)"
|
||||
/>
|
||||
|
||||
<button *ngIf="options.length <= maxItemCount" (click)="addNewOption()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="background {{ isOpen || alwaysDropShadow ? 'active' : '' }}"
|
||||
[ngStyle]="{ height: backgroundHeight }"
|
||||
></div>
|
||||
</div>
|
||||
117
src/app/components/shared/select-add/select-add.component.scss
Normal file
117
src/app/components/shared/select-add/select-add.component.scss
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
$inner-padding: var(--medium-padding);
|
||||
.select-add {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.top,
|
||||
.bottom {
|
||||
padding: $inner-padding;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
@include sub-title-text();
|
||||
}
|
||||
|
||||
img {
|
||||
@include square(16px);
|
||||
transition: transform $long-animation-time;
|
||||
|
||||
&.upside-down {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
|
||||
position: absolute;
|
||||
overflow-y: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
pointer-events: all;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
|
||||
padding-top: 0;
|
||||
|
||||
@include inner-spacing($inner-padding);
|
||||
|
||||
transform: translateY(-100%);
|
||||
visibility: hidden;
|
||||
|
||||
&.open {
|
||||
visibility: visible;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
transition: transform $long-animation-time;
|
||||
|
||||
p {
|
||||
@include sub-title-text();
|
||||
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@include card();
|
||||
|
||||
z-index: 3;
|
||||
|
||||
transition: box-shadow $long-animation-time, height $long-animation-time;
|
||||
|
||||
&.active {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.background {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow-border {
|
||||
.background.active {
|
||||
box-shadow: $shadow-border;
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow-border:hover {
|
||||
.background {
|
||||
box-shadow: $shadow-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/app/components/shared/select-add/select-add.component.ts
Normal file
66
src/app/components/shared/select-add/select-add.component.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-select-add',
|
||||
templateUrl: './select-add.component.html',
|
||||
styleUrls: ['./select-add.component.scss']
|
||||
})
|
||||
export class SelectAddComponent {
|
||||
@Input() placeholder = 'Add a new value…';
|
||||
@Input() maxItemCount = 7;
|
||||
@Input() options: string[];
|
||||
@Input() alwaysDropShadow = false;
|
||||
@Input() onlyShadowBorder = false;
|
||||
|
||||
@Input() set default(value: string) {
|
||||
this.selected = value;
|
||||
if (value) {
|
||||
this.value.emit(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Output() value: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
@ViewChild('top') top: ElementRef;
|
||||
@ViewChild('bottom') bottom: ElementRef;
|
||||
|
||||
selected: string;
|
||||
newOption: string;
|
||||
isOpen = false;
|
||||
|
||||
get otherOptions(): string[] {
|
||||
return this.options.filter(a => a !== this.selected);
|
||||
}
|
||||
|
||||
handleKeys(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
this.addNewOption();
|
||||
}
|
||||
}
|
||||
|
||||
get backgroundHeight(): string {
|
||||
if (this.isOpen && this.top && this.bottom) {
|
||||
const topHeight = this.top.nativeElement.clientHeight;
|
||||
const bottomHeight = this.bottom.nativeElement.clientHeight;
|
||||
return `${topHeight + bottomHeight}px`;
|
||||
}
|
||||
return `100%`;
|
||||
}
|
||||
|
||||
addNewOption() {
|
||||
if (this.newOption) {
|
||||
this.select(this.newOption);
|
||||
this.newOption = '';
|
||||
}
|
||||
}
|
||||
|
||||
select(option: string) {
|
||||
this.selected = option;
|
||||
this.value.emit(this.selected);
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
}
|
||||
5
src/app/components/shared/toggle/toggle.component.html
Normal file
5
src/app/components/shared/toggle/toggle.component.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<span [className]="!on ? 'active' : ''" (click)="on = false" [innerText]="beforeText"></span>
|
||||
|
||||
<input type="checkbox" [(ngModel)]="on" [className]="on ? 'on' : ''" />
|
||||
|
||||
<span [className]="on ? 'active' : ''" (click)="on = true" [innerText]="afterText"></span>
|
||||
66
src/app/components/shared/toggle/toggle.component.scss
Normal file
66
src/app/components/shared/toggle/toggle.component.scss
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
:host {
|
||||
$size: 30px;
|
||||
|
||||
@include center-child();
|
||||
@include inner-spacing(var(--medium-padding), $horizontal: true);
|
||||
|
||||
span {
|
||||
@include medium-text();
|
||||
max-width: 3 * $size;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
|
||||
width: 2 * $size;
|
||||
height: $size;
|
||||
|
||||
border-radius: 1000px;
|
||||
box-shadow: $shadow-border;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
|
||||
@include square($size);
|
||||
|
||||
border-radius: 1000px;
|
||||
background-color: $text-color;
|
||||
|
||||
transition: box-shadow $long-animation-time, left $long-animation-time, transform $long-animation-time;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
box-shadow: $shadow;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
&.on:hover:after {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
&.on:after {
|
||||
left: $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/app/components/shared/toggle/toggle.component.ts
Normal file
27
src/app/components/shared/toggle/toggle.component.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle',
|
||||
templateUrl: './toggle.component.html',
|
||||
styleUrls: ['./toggle.component.scss']
|
||||
})
|
||||
export class ToggleComponent {
|
||||
@Input() beforeText: string;
|
||||
@Input() afterText: string;
|
||||
|
||||
@Output() value: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
@Input() set default(value: boolean) {
|
||||
this.on = value;
|
||||
}
|
||||
|
||||
private _on = false;
|
||||
set on(value: boolean) {
|
||||
this._on = value;
|
||||
this.value.emit(value);
|
||||
}
|
||||
|
||||
get on(): boolean {
|
||||
return this._on;
|
||||
}
|
||||
}
|
||||
6
src/app/interfaces/persistance/block.ts
Normal file
6
src/app/interfaces/persistance/block.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface IBlock {
|
||||
created: Date;
|
||||
tag: string;
|
||||
isDone: boolean;
|
||||
description: string;
|
||||
}
|
||||
5
src/app/interfaces/persistance/color.ts
Normal file
5
src/app/interfaces/persistance/color.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface IColor {
|
||||
h: number;
|
||||
s: number;
|
||||
l: number;
|
||||
}
|
||||
14
src/app/interfaces/persistance/page.ts
Normal file
14
src/app/interfaces/persistance/page.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { ITower } from './tower';
|
||||
|
||||
export interface IPage {
|
||||
name: string;
|
||||
towers: ITower[];
|
||||
|
||||
userData: {
|
||||
hideCreateTowerButton: boolean;
|
||||
defaultDateRange: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
};
|
||||
};
|
||||
}
|
||||
8
src/app/interfaces/persistance/tower.ts
Normal file
8
src/app/interfaces/persistance/tower.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { IBlock } from './block';
|
||||
import { IColor } from './color';
|
||||
|
||||
export interface ITower {
|
||||
name: string;
|
||||
blocks: IBlock[];
|
||||
baseColor: IColor;
|
||||
}
|
||||
4
src/app/interfaces/vector.ts
Normal file
4
src/app/interfaces/vector.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface Vector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
51
src/app/model/base.ts
Normal file
51
src/app/model/base.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export class Base {
|
||||
private static propertyList: any = {};
|
||||
protected subscribers: (() => void)[] = [];
|
||||
|
||||
constructor(properties: any) {
|
||||
const type = this.constructor.name;
|
||||
if (!Base.propertyList.hasOwnProperty(type)) {
|
||||
Base.propertyList[type] = [];
|
||||
}
|
||||
|
||||
for (const property in properties) {
|
||||
if (properties.hasOwnProperty(property)) {
|
||||
const propertyName = `__${property}`;
|
||||
this[propertyName] = properties[property];
|
||||
|
||||
Object.defineProperty(this, property, {
|
||||
get: () => this[propertyName],
|
||||
set: value => {
|
||||
if (value == this[propertyName]) {
|
||||
return;
|
||||
}
|
||||
this[propertyName] = value;
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
|
||||
if (!Base.propertyList[type].includes(property)) {
|
||||
Base.propertyList[type].push(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return Base.propertyList[this.constructor.name].reduce(
|
||||
(object, property) => ({
|
||||
[property]: this[property],
|
||||
...object
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
subscribe(func: () => void) {
|
||||
this.subscribers.push(func);
|
||||
}
|
||||
|
||||
protected update() {
|
||||
this.subscribers.map(f => f());
|
||||
}
|
||||
}
|
||||
23
src/app/model/block.ts
Normal file
23
src/app/model/block.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Base } from './base';
|
||||
import { IBlock } from '../interfaces/persistance/block';
|
||||
import { Color } from './color';
|
||||
|
||||
export class Block extends Base implements IBlock {
|
||||
constructor(props: IBlock) {
|
||||
super(props);
|
||||
|
||||
if (this.created.constructor.name !== 'Date') {
|
||||
// Prevent update message
|
||||
// @ts-ignore
|
||||
this.__created = new Date(this.created);
|
||||
}
|
||||
}
|
||||
|
||||
color: Color;
|
||||
|
||||
// Only here to prevent ts warnings.
|
||||
created: Date;
|
||||
isDone: boolean;
|
||||
description: string;
|
||||
tag: string;
|
||||
}
|
||||
28
src/app/model/color.ts
Normal file
28
src/app/model/color.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { IColor } from '../interfaces/persistance/color';
|
||||
import { Base } from './base';
|
||||
|
||||
export class Color extends Base implements IColor {
|
||||
constructor(props: IColor) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
// Only here to prevent ts warnings.
|
||||
h: number;
|
||||
s: number;
|
||||
l: number;
|
||||
|
||||
public lighten(by: number) {
|
||||
const newL = this.l + by;
|
||||
if (this.l > 100) {
|
||||
this.l = 100;
|
||||
} else if (this.l < 0) {
|
||||
this.l = 0;
|
||||
}
|
||||
|
||||
return new Color({ h: this.h, s: this.s, l: newL });
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `hsl(${this.h}, ${this.s}%, ${this.l}%)`;
|
||||
}
|
||||
}
|
||||
62
src/app/model/page.ts
Normal file
62
src/app/model/page.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Base } from './base';
|
||||
import { IPage } from '../interfaces/persistance/page';
|
||||
import { Tower } from './tower';
|
||||
import { ITower } from '../interfaces/persistance/tower';
|
||||
|
||||
export class Page extends Base implements IPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// @ts-ignore to prevent update message
|
||||
this.__towers = this.towers.map(t => this.createTower(t));
|
||||
}
|
||||
|
||||
// Only here to prevent ts warnings.
|
||||
name: string;
|
||||
towers: Tower[];
|
||||
userData: {
|
||||
hideCreateTowerButton: boolean;
|
||||
defaultDateRange: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
};
|
||||
};
|
||||
|
||||
moveTower({ previousIndex, currentIndex }: { previousIndex: number; currentIndex: number }) {
|
||||
if (previousIndex === currentIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tower = this.towers[previousIndex];
|
||||
this.towers.splice(previousIndex, 1);
|
||||
this.towers.splice(currentIndex, 0, tower);
|
||||
this.update();
|
||||
}
|
||||
|
||||
addTower(name = '') {
|
||||
let hue;
|
||||
do {
|
||||
hue = Math.random() * 360;
|
||||
} while (30 <= hue && hue <= 200);
|
||||
console.log(hue);
|
||||
|
||||
this.towers.push(
|
||||
this.createTower({
|
||||
name,
|
||||
blocks: [],
|
||||
baseColor: { h: hue, s: 100, l: 50 }
|
||||
})
|
||||
);
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
private createTower(props: ITower): Tower {
|
||||
const tower = new Tower(props);
|
||||
tower.subscribe(() => this.update());
|
||||
return tower;
|
||||
}
|
||||
|
||||
removeTower(tower: Tower) {
|
||||
this.towers = this.towers.filter(t => t !== tower);
|
||||
}
|
||||
}
|
||||
54
src/app/model/tower.ts
Normal file
54
src/app/model/tower.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { ITower } from '../interfaces/persistance/tower';
|
||||
import { Color } from './color';
|
||||
import { Block } from './block';
|
||||
import { Base } from './base';
|
||||
import { IBlock } from '../interfaces/persistance/block';
|
||||
import { hashCode } from '../utils/hash';
|
||||
|
||||
export class Tower extends Base implements ITower {
|
||||
constructor(props: ITower) {
|
||||
super(props);
|
||||
|
||||
// @ts-ignore to prevent update message
|
||||
this.__baseColor = new Color(this.baseColor);
|
||||
|
||||
this.blocks = this.blocks.map(b => this.createBlock(b));
|
||||
this.blocks.sort((a, b) => a.created.getTime() - b.created.getTime());
|
||||
}
|
||||
|
||||
tags: string[];
|
||||
|
||||
// Only here to prevent ts warnings.
|
||||
name: string;
|
||||
blocks: Block[];
|
||||
baseColor: Color;
|
||||
|
||||
addBlock(props: { tag: string; description: string; isDone: boolean }) {
|
||||
this.blocks.push(
|
||||
this.createBlock({
|
||||
created: new Date(),
|
||||
...props
|
||||
})
|
||||
);
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
private createBlock(props: IBlock): Block {
|
||||
const block = new Block(props);
|
||||
block.subscribe(() => this.update());
|
||||
return block;
|
||||
}
|
||||
|
||||
protected update() {
|
||||
this.tags = [];
|
||||
for (const block of this.blocks) {
|
||||
if (!this.tags.includes(block.tag)) {
|
||||
this.tags.push(block.tag);
|
||||
}
|
||||
block.color = this.baseColor.lighten(hashCode(block.tag) * 50);
|
||||
}
|
||||
|
||||
super.update();
|
||||
}
|
||||
}
|
||||
93
src/app/services/data.service.ts
Normal file
93
src/app/services/data.service.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { StoreService } from './store.service';
|
||||
import { Page } from '../model/page';
|
||||
|
||||
const USER_DATA_KEY = 'life-towers.user-data.v.1';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
get active(): Page {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
get pageNames(): string[] {
|
||||
return this.data.map(p => p.name);
|
||||
}
|
||||
|
||||
private subscribers: (() => void)[] = [];
|
||||
private _active: Page = null;
|
||||
private data: Page[];
|
||||
private hasLoaded = new Promise(resolve => (this.afterLoadFinished = resolve));
|
||||
private afterLoadFinished: () => void;
|
||||
|
||||
constructor(private storeService: StoreService) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
push(value: Page) {
|
||||
value.subscribe(() => this.save());
|
||||
this.data.push(value);
|
||||
this._active = value;
|
||||
this.save();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.data = this.data.filter(p => p !== this.active);
|
||||
this._active = this.data.length > 0 ? this.data[0] : null;
|
||||
this.save();
|
||||
}
|
||||
|
||||
subscribe(func: () => void) {
|
||||
this.subscribers.push(func);
|
||||
}
|
||||
|
||||
async changeActiveByName(name: string): Promise<void> {
|
||||
await this.hasLoaded;
|
||||
this._active = this.data.filter(p => p.name === name)[0];
|
||||
this.saveActiveIndex(this.data.indexOf(this.active));
|
||||
this.update();
|
||||
}
|
||||
|
||||
async changeActiveByIndex(index: number): Promise<void> {
|
||||
await this.hasLoaded;
|
||||
this._active = this.data[index];
|
||||
this.saveActiveIndex(index);
|
||||
this.update();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
this.data = await this.storeService.load();
|
||||
this.data.map(p => p.subscribe(() => this.save()));
|
||||
this._active = this.data.length > 0 ? this.data[0] : null;
|
||||
this.loadActiveIndex();
|
||||
this.afterLoadFinished();
|
||||
}
|
||||
|
||||
private save() {
|
||||
this.storeService.save(this.data);
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.subscribers.map(f => f());
|
||||
}
|
||||
|
||||
private loadActiveIndex() {
|
||||
const userData = JSON.parse(window.localStorage.getItem(USER_DATA_KEY));
|
||||
if (userData === null) {
|
||||
return;
|
||||
}
|
||||
this._active = this.data[userData.index];
|
||||
}
|
||||
|
||||
private saveActiveIndex(index: number) {
|
||||
window.localStorage.setItem(
|
||||
USER_DATA_KEY,
|
||||
JSON.stringify({
|
||||
index
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
83
src/app/services/modal.service.ts
Normal file
83
src/app/services/modal.service.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Tower } from '../model/tower';
|
||||
import { top } from '../utils/top';
|
||||
|
||||
export enum ModalType {
|
||||
createBlock,
|
||||
editBlock,
|
||||
removeBlock,
|
||||
settings,
|
||||
removeTower,
|
||||
removePage,
|
||||
getStarted
|
||||
}
|
||||
|
||||
interface Modal {
|
||||
type: ModalType;
|
||||
input: any;
|
||||
resolve: (output: any) => void;
|
||||
reject: () => void;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ModalService {
|
||||
private modalStack: Modal[] = [];
|
||||
|
||||
showCreateBlock(options: string[]): Promise<{ selected: string; description: string; isDone: boolean }> {
|
||||
return this.createPromiseAndPushToStack(options, ModalType.createBlock);
|
||||
}
|
||||
|
||||
showEditBlock(data: {
|
||||
default: string;
|
||||
options: string[];
|
||||
description: string;
|
||||
isDone: boolean;
|
||||
}): Promise<{ selected: string; description: string; isDone: boolean }> {
|
||||
return this.createPromiseAndPushToStack(data, ModalType.editBlock);
|
||||
}
|
||||
|
||||
showSettings(): Promise<void> {
|
||||
return this.createPromiseAndPushToStack(null, ModalType.settings);
|
||||
}
|
||||
|
||||
showRemoveTower(tower: Tower): Promise<void> {
|
||||
return this.createPromiseAndPushToStack(tower, ModalType.removeTower);
|
||||
}
|
||||
|
||||
showRemovePage(name: string): Promise<void> {
|
||||
return this.createPromiseAndPushToStack(name, ModalType.removePage);
|
||||
}
|
||||
|
||||
get active(): Modal {
|
||||
return top(this.modalStack);
|
||||
}
|
||||
|
||||
submit(output?: any) {
|
||||
const { resolve } = this.modalStack.pop();
|
||||
resolve(output);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
const { reject } = this.modalStack.pop();
|
||||
reject();
|
||||
}
|
||||
|
||||
private createPromiseAndPushToStack(input: any, type: ModalType): Promise<any> {
|
||||
const modal = {
|
||||
input,
|
||||
type,
|
||||
resolve: () => {},
|
||||
reject: () => {}
|
||||
};
|
||||
|
||||
const modalPromise = new Promise((resolve, reject) => {
|
||||
modal.resolve = resolve;
|
||||
modal.reject = reject;
|
||||
});
|
||||
|
||||
this.modalStack.push(modal);
|
||||
return modalPromise;
|
||||
}
|
||||
}
|
||||
102
src/app/services/store.service.ts
Normal file
102
src/app/services/store.service.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Page } from '../model/page';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'life-towers.data.v.2.1';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StoreService {
|
||||
private storedData: Page[];
|
||||
|
||||
private mockData: string = JSON.stringify([
|
||||
{
|
||||
name: 'Work & life',
|
||||
userData: {},
|
||||
towers: [
|
||||
{
|
||||
name: 'work',
|
||||
baseColor: { h: 0, s: 100, l: 50 },
|
||||
blocks: [
|
||||
{
|
||||
created: new Date(2015, 2, 13),
|
||||
tag: 'a',
|
||||
description: 'done it',
|
||||
isDone: true
|
||||
},
|
||||
{
|
||||
created: new Date(2016, 2, 15),
|
||||
tag: 'go to school',
|
||||
description: 'done it'
|
||||
},
|
||||
{
|
||||
created: new Date(2017, 2, 15),
|
||||
tag: 'go to work',
|
||||
isDone: true
|
||||
},
|
||||
{
|
||||
created: new Date(2018, 2, 13),
|
||||
tag: 'go to work',
|
||||
description: 'done it',
|
||||
isDone: true
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 3, 13),
|
||||
tag: 'go to work'
|
||||
},
|
||||
{
|
||||
created: new Date(2020, 2, 15),
|
||||
tag: 'go to school',
|
||||
description: 'done it',
|
||||
isDone: true
|
||||
},
|
||||
{
|
||||
created: new Date(2021, 2, 15),
|
||||
tag: 'go to school'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
baseColor: { h: 180, s: 100, l: 50 },
|
||||
name: 'life',
|
||||
blocks: [
|
||||
{
|
||||
created: new Date(2019, 3, 13),
|
||||
tag: 'go home',
|
||||
description: 'done it'
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 4, 13),
|
||||
tag: 'go home'
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 4, 15),
|
||||
tag: 'go to work',
|
||||
description: 'done it'
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 4, 15, 14),
|
||||
tag: 'go to work'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
constructor() {
|
||||
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
const data = JSON.parse(localStorageData ? localStorageData : this.mockData);
|
||||
this.storedData = data.map(p => new Page(p));
|
||||
}
|
||||
|
||||
async load(): Promise<Page[]> {
|
||||
return this.storedData;
|
||||
}
|
||||
|
||||
async save(data: Page[]) {
|
||||
this.storedData = data;
|
||||
console.log('save', this.storedData);
|
||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.storedData));
|
||||
}
|
||||
}
|
||||
14
src/app/utils/hash.ts
Normal file
14
src/app/utils/hash.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export const hashCode = (text: string) => {
|
||||
let hash = 0;
|
||||
if (text.length == 0) {
|
||||
return hash;
|
||||
}
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
hash /= Math.pow(2, 32) - 1;
|
||||
return hash;
|
||||
};
|
||||
7
src/app/utils/range.ts
Normal file
7
src/app/utils/range.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const range = ({ min = 0, max = Infinity, step = 1 }: { min?: number; max?: number; step?: number }) => {
|
||||
return {
|
||||
*[Symbol.iterator]() {
|
||||
for (let i = min; i < max; yield i, i += step);
|
||||
}
|
||||
};
|
||||
};
|
||||
3
src/app/utils/top.ts
Normal file
3
src/app/utils/top.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const top = <T>(iterable: ArrayLike<T>): T => {
|
||||
return iterable.length > 0 ? iterable[iterable.length - 1] : null;
|
||||
};
|
||||
4
src/assets/arrow.svg
Normal file
4
src/assets/arrow.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 292.362 292.362" style="enable-background:new 0 0 292.362 292.362;" xml:space="preserve" class=""><g><g>
|
||||
<path d="M286.935,69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952,0-9.233,1.807-12.85,5.424 C1.807,72.998,0,77.279,0,82.228c0,4.948,1.807,9.229,5.424,12.847l127.907,127.907c3.621,3.617,7.902,5.428,12.85,5.428 s9.233-1.811,12.847-5.428L286.935,95.074c3.613-3.617,5.427-7.898,5.427-12.847C292.362,77.279,290.548,72.998,286.935,69.377z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
</g></g> </svg>
|
||||
|
After Width: | Height: | Size: 746 B |
12
src/assets/plus-sign.svg
Normal file
12
src/assets/plus-sign.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 31.059 31.059" style="enable-background:new 0 0 31.059 31.059;" xml:space="preserve" width="512px" height="512px" class=""><g><g>
|
||||
<g>
|
||||
<path d="M15.529,31.059C6.966,31.059,0,24.092,0,15.529C0,6.966,6.966,0,15.529,0 c8.563,0,15.529,6.966,15.529,15.529C31.059,24.092,24.092,31.059,15.529,31.059z M15.529,1.774 c-7.585,0-13.755,6.171-13.755,13.755s6.17,13.754,13.755,13.754c7.584,0,13.754-6.17,13.754-13.754S23.113,1.774,15.529,1.774z" data-original="#010002" class="active-path" data-old_color="#010002" fill="#5D576B"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M21.652,16.416H9.406c-0.49,0-0.888-0.396-0.888-0.887c0-0.49,0.397-0.888,0.888-0.888h12.246 c0.49,0,0.887,0.398,0.887,0.888C22.539,16.02,22.143,16.416,21.652,16.416z" data-original="#010002" class="active-path" data-old_color="#010002" fill="#5D576B"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M15.529,22.539c-0.49,0-0.888-0.397-0.888-0.887V9.406c0-0.49,0.398-0.888,0.888-0.888 c0.49,0,0.887,0.398,0.887,0.888v12.246C16.416,22.143,16.02,22.539,15.529,22.539z" data-original="#010002" class="active-path" data-old_color="#010002" fill="#5D576B"/>
|
||||
</g>
|
||||
</g></g> </svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
13
src/assets/trash.svg
Normal file
13
src/assets/trash.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 729.837 729.838" style="enable-background:new 0 0 729.837 729.838;" xml:space="preserve"><g><g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M589.193,222.04c0-6.296,5.106-11.404,11.402-11.404S612,215.767,612,222.04v437.476c0,19.314-7.936,36.896-20.67,49.653 c-12.733,12.734-30.339,20.669-49.653,20.669H188.162c-19.315,0-36.943-7.935-49.654-20.669 c-12.734-12.734-20.669-30.313-20.669-49.653V222.04c0-6.296,5.108-11.404,11.403-11.404c6.296,0,11.404,5.131,11.404,11.404 v437.476c0,13.02,5.37,24.922,13.97,33.521c8.6,8.601,20.503,13.993,33.522,13.993h353.517c13.019,0,24.896-5.394,33.498-13.993 c8.624-8.624,13.992-20.503,13.992-33.498V222.04H589.193z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
<path d="M279.866,630.056c0,6.296-5.108,11.403-11.404,11.403s-11.404-5.107-11.404-11.403v-405.07 c0-6.296,5.108-11.404,11.404-11.404s11.404,5.108,11.404,11.404V630.056z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
<path d="M376.323,630.056c0,6.296-5.107,11.403-11.403,11.403s-11.404-5.107-11.404-11.403v-405.07 c0-6.296,5.108-11.404,11.404-11.404s11.403,5.108,11.403,11.404V630.056z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
<path d="M472.803,630.056c0,6.296-5.106,11.403-11.402,11.403c-6.297,0-11.404-5.107-11.404-11.403v-405.07 c0-6.296,5.107-11.404,11.404-11.404c6.296,0,11.402,5.108,11.402,11.404V630.056L472.803,630.056z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
<path d="M273.214,70.323c0,6.296-5.108,11.404-11.404,11.404c-6.295,0-11.403-5.108-11.403-11.404 c0-19.363,7.911-36.943,20.646-49.677C283.787,7.911,301.368,0,320.73,0h88.379c19.339,0,36.92,7.935,49.652,20.669 c12.734,12.734,20.67,30.362,20.67,49.654c0,6.296-5.107,11.404-11.403,11.404s-11.403-5.108-11.403-11.404 c0-13.019-5.369-24.922-13.97-33.522c-8.602-8.601-20.503-13.994-33.522-13.994h-88.378c-13.043,0-24.922,5.369-33.546,13.97 C278.583,45.401,273.214,57.28,273.214,70.323z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
<path d="M99.782,103.108h530.273c11.189,0,21.405,4.585,28.818,11.998l0.047,0.048c7.413,7.412,11.998,17.628,11.998,28.818 v29.46c0,6.295-5.108,11.403-11.404,11.403h-0.309H70.323c-6.296,0-11.404-5.108-11.404-11.403v-0.285v-29.175 c0-11.166,4.585-21.406,11.998-28.818l0.048-0.048C78.377,107.694,88.616,103.108,99.782,103.108L99.782,103.108z M630.056,125.916H99.782c-4.965,0-9.503,2.02-12.734,5.274L87,131.238c-3.255,3.23-5.274,7.745-5.274,12.734v18.056h566.361 v-18.056c0-4.965-2.02-9.503-5.273-12.734l-0.049-0.048C639.536,127.936,635.021,125.916,630.056,125.916z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
</g>
|
||||
</g>
|
||||
</g></g> </svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
4
src/assets/x-sign.svg
Normal file
4
src/assets/x-sign.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 212.982 212.982" style="enable-background:new 0 0 212.982 212.982;" xml:space="preserve" width="512px" height="512px" class=""><g><g id="Close">
|
||||
<path d="M131.804,106.491l75.936-75.936c6.99-6.99,6.99-18.323,0-25.312 c-6.99-6.99-18.322-6.99-25.312,0l-75.937,75.937L30.554,5.242c-6.99-6.99-18.322-6.99-25.312,0c-6.989,6.99-6.989,18.323,0,25.312 l75.937,75.936L5.242,182.427c-6.989,6.99-6.989,18.323,0,25.312c6.99,6.99,18.322,6.99,25.312,0l75.937-75.937l75.937,75.937 c6.989,6.99,18.322,6.99,25.312,0c6.99-6.99,6.99-18.322,0-25.312L131.804,106.491z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
</g></g> </svg>
|
||||
|
After Width: | Height: | Size: 816 B |
|
|
@ -1,14 +1,23 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Frontend</title>
|
||||
<base href="/">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Frontend</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300|Raleway&display=swap&subset=latin-ext"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#5d576b" />
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
22
src/library/animations.scss
Normal file
22
src/library/animations.scss
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@import 'common-variables';
|
||||
|
||||
$long-animation-time: 200ms;
|
||||
$short-animation-time: 100ms;
|
||||
|
||||
@mixin gravitate {
|
||||
cursor: pointer;
|
||||
transition: box-shadow $long-animation-time, transform $long-animation-time;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin jump {
|
||||
cursor: pointer;
|
||||
transition: transform $long-animation-time;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
14
src/library/common-variables.scss
Normal file
14
src/library/common-variables.scss
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
$accent-color: #a2666f;
|
||||
$text-color: #5d576b;
|
||||
$light-color: #ffffff;
|
||||
|
||||
$background-gradient: linear-gradient(90deg, #fff9e077 0, #ffd6d677 100%);
|
||||
|
||||
$shadow: 0 0 1.5px 1.5px rgba(0, 0, 0, 0.1), 0 0 3px 2px rgba(0, 0, 0, 0.05);
|
||||
$shadow-border: 0 0 0 0.75px rgba(0, 0, 0, 0.1);
|
||||
|
||||
$normal-font: 'Open Sans Condensed', sans-serif;
|
||||
$title-font: 'Raleway', serif;
|
||||
|
||||
$mobile-width: 520px;
|
||||
$min-height: 400px;
|
||||
54
src/library/forms.scss
Normal file
54
src/library/forms.scss
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
@import 'text';
|
||||
@import 'animations';
|
||||
|
||||
textarea {
|
||||
@include normal-text();
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
resize: none;
|
||||
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
@include sub-title-text();
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
|
||||
display: block;
|
||||
border: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 1px $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
-webkit-appearance: none;
|
||||
|
||||
margin: 8px auto 0 auto;
|
||||
user-select: none;
|
||||
|
||||
background: transparent;
|
||||
border: 0;
|
||||
text-decoration: underline;
|
||||
|
||||
@include medium-text();
|
||||
@include jump();
|
||||
}
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
62
src/library/main.scss
Normal file
62
src/library/main.scss
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
@import 'common-variables';
|
||||
@import 'animations';
|
||||
@import 'text';
|
||||
@import 'spacing';
|
||||
@import 'forms';
|
||||
@import 'utils';
|
||||
|
||||
:root {
|
||||
--border-radius: 5px;
|
||||
|
||||
--large-padding: 30px;
|
||||
--medium-padding: 15px;
|
||||
--small-padding: 10px;
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
--border-radius: 3px;
|
||||
|
||||
--large-padding: 20px;
|
||||
--medium-padding: 15px;
|
||||
--small-padding: 7.5px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin card {
|
||||
border-radius: var(--border-radius);
|
||||
background-color: $light-color;
|
||||
}
|
||||
|
||||
@mixin center-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin exit {
|
||||
@include square(16px);
|
||||
background: url('/assets/x-sign.svg') no-repeat center center;
|
||||
background-size: 50% 50%;
|
||||
box-sizing: content-box;
|
||||
padding: 8px;
|
||||
|
||||
@include jump();
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
box-shadow: $shadow-border;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: $text-color;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
9
src/library/spacing.scss
Normal file
9
src/library/spacing.scss
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@mixin inner-spacing($spacing, $horizontal: false) {
|
||||
& > *:not(:last-child) {
|
||||
@if $horizontal {
|
||||
margin-right: $spacing;
|
||||
} @else {
|
||||
margin-bottom: $spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/library/text.scss
Normal file
57
src/library/text.scss
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
@import 'common-variables';
|
||||
|
||||
:root {
|
||||
--larger-font-size: 22px;
|
||||
--large-font-size: 18px;
|
||||
--medium-font-size: 16px;
|
||||
--small-font-size: 11px;
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
--larger-font-size: 20px;
|
||||
--large-font-size: 16px;
|
||||
--medium-font-size: 14px;
|
||||
--small-font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin title-text {
|
||||
font-family: $title-font;
|
||||
color: $text-color;
|
||||
font-size: var(--larger-font-size);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@mixin sub-title-text {
|
||||
font-family: $title-font;
|
||||
color: $text-color;
|
||||
font-size: var(--medium-font-size);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@mixin normal-text {
|
||||
font-family: $normal-font;
|
||||
color: $text-color;
|
||||
font-size: var(--larger-font-size);
|
||||
}
|
||||
|
||||
@mixin medium-text {
|
||||
font-family: $normal-font;
|
||||
color: $text-color;
|
||||
font-size: var(--medium-font-size);
|
||||
}
|
||||
|
||||
@mixin small-text {
|
||||
font-family: $normal-font;
|
||||
color: $text-color;
|
||||
font-size: var(--small-font-size);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@include title-text();
|
||||
}
|
||||
|
||||
p {
|
||||
@include normal-text();
|
||||
}
|
||||
4
src/library/utils.scss
Normal file
4
src/library/utils.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@mixin square($size) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
}
|
||||
|
|
@ -8,5 +8,6 @@ if (environment.production) {
|
|||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
|
|
|||
|
|
@ -1 +1,35 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
@import 'library/main';
|
||||
|
||||
$line-height: 2px;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&::selection {
|
||||
background: $text-color;
|
||||
color: $light-color;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background: $background-gradient;
|
||||
}
|
||||
|
||||
body {
|
||||
text-align: center;
|
||||
padding: var(--large-padding);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@
|
|||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"types": []
|
||||
"types": [],
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
"exclude": ["test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
|
|
|||
83
tslint.json
83
tslint.json
|
|
@ -1,66 +1,49 @@
|
|||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rulesDirectory": ["node_modules/codelyzer"],
|
||||
"rules": {
|
||||
"array-type": false,
|
||||
"arrow-parens": false,
|
||||
"deprecation": {
|
||||
"severity": "warn"
|
||||
},
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"interface-name": false,
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"arrow-return-shorthand": true,
|
||||
"callable-types": true,
|
||||
"class-name": true,
|
||||
"forin": true,
|
||||
"import-blacklist": [true, "rxjs", "rxjs/Rx"],
|
||||
"interface-over-type-literal": true,
|
||||
"label-position": true,
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
"order": ["static-field", "instance-field", "static-method", "instance-method"]
|
||||
}
|
||||
],
|
||||
"no-consecutive-blank-lines": false,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-super": true,
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-empty-interface": true,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": [true, "ignore-params"],
|
||||
"no-misused-new": true,
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": false,
|
||||
"no-string-throw": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-initializer": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"ordered-imports": false,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"trailing-comma": false,
|
||||
"prefer-const": true,
|
||||
"radix": true,
|
||||
"triple-equals": [true, "allow-null-check"],
|
||||
"typeof-compare": true,
|
||||
"unified-signatures": true,
|
||||
"variable-name": false,
|
||||
"directive-selector": [true, "attribute", "app", "camelCase"],
|
||||
"component-selector": [true, "element", "app", "kebab-case"],
|
||||
"no-output-on-prefix": true,
|
||||
"use-input-property-decorator": true,
|
||||
"use-output-property-decorator": true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue