Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 706fe745d3 | |||
|
|
1598260ce3 | ||
|
|
19aad2b2af | ||
|
|
674f07f5f1 | ||
|
|
97e94ec154 | ||
|
|
32704c5561 | ||
|
|
fc0d64fce7 | ||
|
|
3a1accaae1 | ||
|
|
3101c973eb | ||
|
|
d612678682 | ||
|
|
9933f4f9ff |
0
.editorconfig
Normal file → Executable file
2
.gitignore
vendored
Normal file → Executable file
|
|
@ -44,3 +44,5 @@ testem.log
|
|||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.firebase
|
||||
|
|
|
|||
0
.prettierrc
Normal file → Executable file
0
README.md
Normal file → Executable file
0
angular.json
Normal file → Executable file
13
firebase.json
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"hosting": {
|
||||
"public": "dist/frontend",
|
||||
"site": "towers-schmelczer-dev",
|
||||
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
16611
package-lock.json
generated
Normal file → Executable file
7
package.json
Normal file → Executable file
|
|
@ -5,7 +5,7 @@
|
|||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --prod --base-href /life-qa/",
|
||||
"build:prod": "ng build --prod",
|
||||
"format:fix": "pretty-quick --staged",
|
||||
"precommit": "run-s format:fix lint",
|
||||
"format:check": "prettier --config ./.prettierrc --list-different \"src/{app,environments,assets}/**/*{.ts,.js,.json,.css,.scss}\"",
|
||||
|
|
@ -24,12 +24,14 @@
|
|||
"@angular/platform-browser-dynamic": "~7.2.0",
|
||||
"@angular/router": "~7.2.0",
|
||||
"core-js": "^2.5.4",
|
||||
"ng": "^0.0.0",
|
||||
"rxjs": "~6.3.3",
|
||||
"tslib": "^1.10.0",
|
||||
"uuid": "^3.3.3",
|
||||
"zone.js": "~0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.13.0",
|
||||
"@angular-devkit/build-angular": "^0.13.10",
|
||||
"@angular/cli": "~7.3.8",
|
||||
"@angular/compiler-cli": "~7.2.0",
|
||||
"@angular/language-service": "~7.2.0",
|
||||
|
|
@ -41,6 +43,7 @@
|
|||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.18.2",
|
||||
"pretty-quick": "^1.11.1",
|
||||
"sass": "^1.32.5",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.2.2"
|
||||
|
|
|
|||
0
src/app/app-routing.module.ts
Normal file → Executable file
6
src/app/app.component.html
Normal file → Executable file
|
|
@ -1,2 +1,4 @@
|
|||
<app-modal></app-modal>
|
||||
<router-outlet></router-outlet>
|
||||
<main (click)="cancelService.cancelAll()">
|
||||
<app-modal></app-modal>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
|
|
|||
3
src/app/app.component.scss
Normal file → Executable file
|
|
@ -0,0 +1,3 @@
|
|||
main {
|
||||
height: 100%;
|
||||
}
|
||||
22
src/app/app.component.ts
Normal file → Executable file
|
|
@ -1,10 +1,24 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, DoCheck } from '@angular/core';
|
||||
import { CancelService } from './services/cancel.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
styleUrls: ['./app.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'frontend';
|
||||
export class AppComponent implements DoCheck {
|
||||
title = 'life';
|
||||
|
||||
constructor(public cancelService: CancelService) {
|
||||
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
this.cancelService.cancelAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
// console.log('app change detection');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/app/app.module.ts
Normal file → Executable file
|
|
@ -1,6 +1,5 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { PageComponent } from './components/pages/page/page.component';
|
||||
|
|
@ -13,39 +12,39 @@ 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';
|
||||
import { ColorPipe } from './pipes/color.pipe';
|
||||
import { BlocksComponent } from './components/modal/modals/blocks/blocks.component';
|
||||
import { FormatDatePipe } from './pipes/format-date.pipe';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
PageComponent,
|
||||
BlockComponent,
|
||||
TowerComponent,
|
||||
DoubleSliderComponent,
|
||||
PagesComponent,
|
||||
SelectAddComponent,
|
||||
ModalComponent,
|
||||
BlockComponent,
|
||||
EditBlockComponent,
|
||||
SettingsComponent,
|
||||
RemoveTowerComponent,
|
||||
RemovePageComponent,
|
||||
GetStartedComponent,
|
||||
CreateBlockComponent,
|
||||
RemoveBlockComponent,
|
||||
ToggleComponent,
|
||||
TasksComponent,
|
||||
ColorPipe
|
||||
ColorPipe,
|
||||
BlocksComponent,
|
||||
FormatDatePipe
|
||||
],
|
||||
imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule],
|
||||
imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule, HttpClientModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
|
|
|||
20
src/app/components/modal/modal.component.html
Normal file → Executable file
|
|
@ -1,9 +1,11 @@
|
|||
<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>
|
||||
<section
|
||||
(click)="modalService.cancel()"
|
||||
class="{{ modalService.active ? 'active' : '' }}"
|
||||
[ngSwitch]="modalService.active?.type"
|
||||
>
|
||||
<app-blocks (save)="save = $event" (click)="$event.stopPropagation()" *ngSwitchCase="ModalType.blocks"></app-blocks>
|
||||
<app-remove-tower (click)="$event.stopPropagation()" *ngSwitchCase="ModalType.removeTower"></app-remove-tower>
|
||||
<app-settings (click)="$event.stopPropagation()" *ngSwitchCase="ModalType.settings"></app-settings>
|
||||
<app-get-started (click)="$event.stopPropagation()" *ngSwitchCase="ModalType.getStarted"></app-get-started>
|
||||
<app-remove-page (click)="$event.stopPropagation()" *ngSwitchCase="ModalType.removePage"></app-remove-page>
|
||||
</section>
|
||||
|
|
|
|||
8
src/app/components/modal/modal.component.scss
Normal file → Executable file
|
|
@ -1,13 +1,13 @@
|
|||
@import '../../../styles';
|
||||
|
||||
main {
|
||||
section {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
|
||||
@include center-child();
|
||||
|
||||
|
|
|
|||
13
src/app/components/modal/modal.component.ts
Normal file → Executable file
|
|
@ -1,5 +1,6 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService, ModalType } from '../../services/modal.service';
|
||||
import { CancelService } from '../../services/cancel.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal',
|
||||
|
|
@ -7,12 +8,16 @@ import { ModalService, ModalType } from '../../services/modal.service';
|
|||
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') {
|
||||
save: () => void = null;
|
||||
|
||||
constructor(public modalService: ModalService, private cancelService: CancelService) {
|
||||
this.cancelService.subscribe(this, () => {
|
||||
if (this.save) {
|
||||
this.save();
|
||||
this.save = null;
|
||||
} else {
|
||||
this.modalService.cancel();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
85
src/app/components/modal/modals/blocks/blocks.component.html
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
<section #container *ngIf="tower">
|
||||
<div class="card placeholder"></div>
|
||||
<div
|
||||
*ngFor="let i of range({ max: blocks.length })"
|
||||
(click)="$event.stopPropagation(); scrollToChild(i + 1)"
|
||||
class="card {{ i + 1 === activeChild ? 'active' : '' }} {{
|
||||
i + 2 === activeChild || i === activeChild ? 'near-active' : ''
|
||||
}}"
|
||||
>
|
||||
<div class="mask"></div>
|
||||
|
||||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<div class="block" [ngStyle]="{ 'background-color': tower.getColorOfTag(editedValues[i].tag) | color }"></div>
|
||||
<h1 [innerText]="editedValues[i]?.created | formatDate"></h1>
|
||||
</div>
|
||||
|
||||
<div class="select-add-container">
|
||||
<app-select-add
|
||||
class="select"
|
||||
[options]="tower.tags"
|
||||
[default]="editedValues[i].tag"
|
||||
[alwaysDropShadow]="true"
|
||||
[onlyShadowBorder]="true"
|
||||
[placeholder]="'Tag this item…'"
|
||||
(value)="editedValues[i].tag = $event"
|
||||
></app-select-add>
|
||||
</div>
|
||||
|
||||
<textarea placeholder="Write a description here…" [(ngModel)]="editedValues[i].description"></textarea>
|
||||
|
||||
<div>
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="blocks[i].isDone"
|
||||
(value)="editedValues[i].isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
(click)="$event.stopPropagation(); scrollToChild(blocks.length + 1)"
|
||||
class="card {{ blocks.length + 1 === activeChild ? 'active' : '' }} {{
|
||||
blocks.length === activeChild ? 'near-active' : ''
|
||||
}} "
|
||||
>
|
||||
<div class="mask"></div>
|
||||
|
||||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<div class="block" [ngStyle]="{ 'background-color': tower.getColorOfTag(top(editedValues).tag) | color }"></div>
|
||||
<h1>Create now</h1>
|
||||
</div>
|
||||
|
||||
<div class="select-add-container">
|
||||
<app-select-add
|
||||
class="select"
|
||||
[options]="tower.tags"
|
||||
[default]="tower.tags.length ? tower.tags[0] : null"
|
||||
[alwaysDropShadow]="true"
|
||||
[onlyShadowBorder]="true"
|
||||
[placeholder]="'Set a category…'"
|
||||
[newValuePlaceholder]="'Add a category…'"
|
||||
(value)="top(editedValues).tag = $event"
|
||||
></app-select-add>
|
||||
</div>
|
||||
|
||||
<textarea placeholder="Write a description here…" [(ngModel)]="top(editedValues).description"></textarea>
|
||||
|
||||
<div>
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="onlyDone"
|
||||
(value)="top(editedValues).isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<button (click)="submitAdd()" [disabled]="!top(editedValues).tag">Create and exit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card placeholder"></div>
|
||||
</section>
|
||||
175
src/app/components/modal/modals/blocks/blocks.component.scss
Executable file
|
|
@ -0,0 +1,175 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include center-child();
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
@include card();
|
||||
box-shadow: $shadow;
|
||||
display: block;
|
||||
|
||||
transform-origin: center center;
|
||||
|
||||
flex: 0 0 auto;
|
||||
width: 66vw;
|
||||
max-width: 400px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
margin: calc(var(--large-padding) / 2);
|
||||
position: relative;
|
||||
|
||||
&.near-active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
|
||||
@include card();
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: var(--large-padding);
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0 !important;
|
||||
width: 60vw;
|
||||
max-width: 60vw;
|
||||
}
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
position: relative;
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@include exit();
|
||||
}
|
||||
|
||||
.block {
|
||||
@include square(12px);
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
height: 32px;
|
||||
@media (max-width: $mobile-width) {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
|
||||
transition: opacity $short-animation-time;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.25;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
@include square(16px);
|
||||
}
|
||||
|
||||
transition: opacity $short-animation-time;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: calc(-1 * #{$line-height});
|
||||
left: 0;
|
||||
height: $line-height;
|
||||
background-color: $text-color;
|
||||
width: 0;
|
||||
transition: width $long-animation-time;
|
||||
}
|
||||
|
||||
@media (min-width: $mobile-width) {
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:hover {
|
||||
&:before {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&:before {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card:last-child:after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: var(--large-padding);
|
||||
right: calc(-1 * var(--large-padding));
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
183
src/app/components/modal/modals/blocks/blocks.component.ts
Executable file
|
|
@ -0,0 +1,183 @@
|
|||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { Tower } from '../../../../model/tower';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Block } from '../../../../model/block';
|
||||
import { IBlock } from '../../../../interfaces/persistance/block';
|
||||
import { CancelService } from '../../../../services/cancel.service';
|
||||
import { range } from 'src/app/utils/range';
|
||||
import { top } from 'src/app/utils/top';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks',
|
||||
templateUrl: './blocks.component.html',
|
||||
styleUrls: ['./blocks.component.scss']
|
||||
})
|
||||
export class BlocksComponent implements OnInit, OnDestroy {
|
||||
readonly range = range;
|
||||
readonly top = top;
|
||||
tower: Tower;
|
||||
editedValues: Array<Partial<IBlock>>;
|
||||
endOfScrollToken = 0;
|
||||
activeChild: number;
|
||||
scrollMayEnd = true;
|
||||
onlyDone: boolean;
|
||||
@ViewChild('container') container: ElementRef;
|
||||
|
||||
private intervalID: number;
|
||||
|
||||
constructor(
|
||||
public modalService: ModalService,
|
||||
private cancelService: CancelService,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
private component: ElementRef
|
||||
) {
|
||||
window.addEventListener('resize', this.onScroll.bind(this));
|
||||
}
|
||||
|
||||
@Output() save: EventEmitter<() => void> = new EventEmitter();
|
||||
|
||||
get blocks(): Array<Block> {
|
||||
return this.tower.blocks.filter(b => b.isDone === this.onlyDone);
|
||||
}
|
||||
|
||||
@HostListener('click') cancel() {
|
||||
this.cancelService.cancelAll();
|
||||
}
|
||||
|
||||
@HostListener('touchstart') fingerDown() {
|
||||
this.scrollMayEnd = false;
|
||||
}
|
||||
|
||||
@HostListener('touchend') fingerUp() {
|
||||
this.scrollMayEnd = true;
|
||||
this.onScroll();
|
||||
}
|
||||
|
||||
@HostListener('scroll') onScroll() {
|
||||
const newToken = ++this.endOfScrollToken;
|
||||
setTimeout(() => {
|
||||
if (newToken === this.endOfScrollToken && this.scrollMayEnd) {
|
||||
this.adjustPosition();
|
||||
}
|
||||
}, 150);
|
||||
this.animateScroll();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const {
|
||||
tower$,
|
||||
onlyDone,
|
||||
startBlock
|
||||
}: { tower$: Observable<Tower>; onlyDone: boolean; startBlock: Block } = this.modalService.active.input;
|
||||
|
||||
this.save.emit(() => this.submitChange());
|
||||
|
||||
this.intervalID = setInterval(() => this.changeDetector.detectChanges(), 1000);
|
||||
|
||||
this.onlyDone = onlyDone;
|
||||
const subscription = tower$.subscribe(value => {
|
||||
if (value) {
|
||||
this.tower = value;
|
||||
this.editedValues = this.blocks.map(({ isDone, description, tag, created }) => ({
|
||||
isDone,
|
||||
description,
|
||||
tag,
|
||||
created
|
||||
}));
|
||||
this.editedValues.push({
|
||||
tag: this.tower.tags.length ? this.tower.tags[0] : null,
|
||||
isDone: this.onlyDone,
|
||||
description: ''
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollToChild(startBlock ? this.blocks.indexOf(startBlock) + 1 : this.blocks.length + 1, true);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
animateScroll() {
|
||||
if (!this.container || !this.component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.component.nativeElement;
|
||||
|
||||
[...this.container.nativeElement.children]
|
||||
.slice(1, -1)
|
||||
.forEach(element =>
|
||||
this.animate(
|
||||
element.style,
|
||||
element.querySelector('.mask').style,
|
||||
Math.abs(element.offsetLeft - c.scrollLeft + element.clientWidth / 2 - window.innerWidth / 2) /
|
||||
element.clientWidth
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
animate(cardStyle, maskStyle, t: number) {
|
||||
t = Math.min(2, Math.max(0, t));
|
||||
cardStyle.opacity = (1.33 * (1 - t / 2)).toString();
|
||||
t = Math.min(1, Math.max(0, t));
|
||||
maskStyle.opacity = Math.pow(t, 0.5).toString();
|
||||
maskStyle.display = t <= 0.05 ? 'none' : 'block';
|
||||
}
|
||||
|
||||
adjustPosition() {
|
||||
if (!this.container || !this.component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const c = this.component.nativeElement;
|
||||
|
||||
const middle =
|
||||
[...this.container.nativeElement.children]
|
||||
.slice(1, -1)
|
||||
.map(element => Math.abs(element.offsetLeft - c.scrollLeft + element.clientWidth / 2 - window.innerWidth / 2))
|
||||
.map((value, index) => (Math.abs(index + 1 - this.activeChild) === 1 ? Math.abs(value - 100) : value))
|
||||
.reduce(
|
||||
(middleIndex, current, currentIndex, list) => (list[middleIndex] < current ? middleIndex : currentIndex),
|
||||
0
|
||||
) + 1;
|
||||
|
||||
this.scrollToChild(middle);
|
||||
}
|
||||
|
||||
scrollToChild(index: number, instantly?: boolean) {
|
||||
this.activeChild = index;
|
||||
const element = this.container.nativeElement.children[index];
|
||||
|
||||
this.component.nativeElement.scrollTo({
|
||||
left: element.offsetLeft - (window.innerWidth / 2 - element.clientWidth / 2),
|
||||
behavior: instantly ? 'auto' : 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
submitAdd() {
|
||||
top(this.editedValues).created = new Date();
|
||||
this.tower.addBlock(top(this.editedValues) as IBlock);
|
||||
this.cancelService.cancelAll();
|
||||
}
|
||||
|
||||
submitChange() {
|
||||
this.blocks.forEach((b, i) => b.changeKeys(this.editedValues[i]));
|
||||
this.modalService.submit();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearInterval(this.intervalID);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<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.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)]="description"></textarea>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="isDone"
|
||||
(value)="isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="submit()" [disabled]="!selected">Create</button>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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 = !this.modalService.active.input.isTask;
|
||||
|
||||
constructor(public modalService: ModalService) {}
|
||||
|
||||
submit() {
|
||||
if (!this.selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalService.submit({
|
||||
selected: this.selected,
|
||||
description: this.description,
|
||||
isDone: this.isDone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<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.default"
|
||||
[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>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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
src/app/components/modal/modals/get-started/get-started.component.html
Normal file → Executable file
0
src/app/components/modal/modals/get-started/get-started.component.scss
Normal file → Executable file
0
src/app/components/modal/modals/get-started/get-started.component.ts
Normal file → Executable file
|
|
@ -1,3 +0,0 @@
|
|||
<p>
|
||||
remove-block works!
|
||||
</p>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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
src/app/components/modal/modals/remove-page/remove-page.component.html
Normal file → Executable file
0
src/app/components/modal/modals/remove-page/remove-page.component.scss
Normal file → Executable file
0
src/app/components/modal/modals/remove-page/remove-page.component.ts
Normal file → Executable file
0
src/app/components/modal/modals/remove-tower/remove-tower.component.html
Normal file → Executable file
0
src/app/components/modal/modals/remove-tower/remove-tower.component.scss
Normal file → Executable file
0
src/app/components/modal/modals/remove-tower/remove-tower.component.ts
Normal file → Executable file
8
src/app/components/modal/modals/settings/settings.component.html
Normal file → Executable file
|
|
@ -4,7 +4,6 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'Hide create tower button'"
|
||||
[afterText]="'Show create tower button'"
|
||||
|
|
@ -12,8 +11,11 @@
|
|||
(value)="page.setHideCreateTowerButton(!$event)"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<p *ngIf="page.towers.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
|
||||
|
||||
<button (click)="deletePage()">Delete current page</button>
|
||||
<input id="token" type="text" [(ngModel)]="token" />
|
||||
|
||||
<button (click)="setNewToken()">Set token</button>
|
||||
|
||||
<button (click)="$event.stopPropagation() || deletePage()">Delete current page</button>
|
||||
|
|
|
|||
10
src/app/components/modal/modals/settings/settings.component.scss
Normal file → Executable file
|
|
@ -4,7 +4,7 @@
|
|||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
max-width: 400px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
|
@ -31,4 +31,12 @@
|
|||
p {
|
||||
font-size: var(--medium-font-size);
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
src/app/components/modal/modals/settings/settings.component.ts
Normal file → Executable file
|
|
@ -1,29 +1,56 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { DataService } from '../../../../services/data.service';
|
||||
import { Page } from '../../../../model/page';
|
||||
import { Data } from '../../../../model/data';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { MapStoreService } from '../../../../services/map-store.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
constructor(public modalService: ModalService, public dataService: DataService) {}
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
data: Data;
|
||||
page: Page;
|
||||
|
||||
ngOnInit() {
|
||||
this.modalService.active.input.subscribe(p => (this.page = p));
|
||||
private dataSubscription: Subscription;
|
||||
private pageSubscription: Subscription;
|
||||
|
||||
token: string;
|
||||
|
||||
constructor(public modalService: ModalService, private store: MapStoreService) {
|
||||
this.token = store.userToken;
|
||||
}
|
||||
|
||||
page: Page;
|
||||
ngOnInit() {
|
||||
const { data$, page$ } = this.modalService.active.input;
|
||||
|
||||
this.dataSubscription = data$.subscribe(d => (this.data = d));
|
||||
this.pageSubscription = page$.subscribe(p => (this.page = p));
|
||||
}
|
||||
|
||||
async deletePage() {
|
||||
try {
|
||||
await this.modalService.showRemovePage(this.page.name);
|
||||
this.dataService.removePage(this.page);
|
||||
this.data.removePage(this.page);
|
||||
this.modalService.submit();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
setNewToken() {
|
||||
this.store.userToken = this.token;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.dataSubscription) {
|
||||
this.dataSubscription.unsubscribe();
|
||||
}
|
||||
if (this.pageSubscription) {
|
||||
this.pageSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
src/app/components/pages/page/page.component.html
Normal file → Executable file
0
src/app/components/pages/page/page.component.scss
Normal file → Executable file
7
src/app/components/pages/page/page.component.ts
Normal file → Executable file
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Page } from '../../../model/page';
|
||||
import { ModalService } from '../../../services/modal.service';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Range } from '../../../interfaces/range';
|
||||
import { Subject } from 'rxjs/internal/Subject';
|
||||
|
|
@ -34,16 +33,14 @@ export class PageComponent implements OnInit {
|
|||
return this.dates.map(d => d.toLocaleDateString());
|
||||
}
|
||||
|
||||
constructor(private modalService: ModalService, public dataService: DataService) {}
|
||||
constructor(private modalService: ModalService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.page$.subscribe(value => {
|
||||
if (value) {
|
||||
this.towers = value.towers.map((t, index) => {
|
||||
if (index < this.towers.length) {
|
||||
if (this.towers[index].getValue() !== t) {
|
||||
this.towers[index].next(t);
|
||||
}
|
||||
this.towers[index].next(t);
|
||||
return this.towers[index];
|
||||
}
|
||||
return new BehaviorSubject(t);
|
||||
|
|
|
|||
2
src/app/components/pages/page/tower/block/block.component.html
Normal file → Executable file
|
|
@ -1 +1 @@
|
|||
<div [ngStyle]="{ 'background-color': block.color | color }" (click)="handleClick()"></div>
|
||||
<div [ngStyle]="{ 'background-color': block.color | color }" (click)="$event.stopPropagation() || handleClick()"></div>
|
||||
|
|
|
|||
0
src/app/components/pages/page/tower/block/block.component.scss
Normal file → Executable file
21
src/app/components/pages/page/tower/block/block.component.ts
Normal file → Executable file
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Block } from '../../../../../model/block';
|
||||
import { ChangeDetectorRef, Component, Input } from '@angular/core';
|
||||
import { ModalService } from '../../../../../services/modal.service';
|
||||
import { ColoredBlock, Tower } from '../../../../../model/tower';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
|
|
@ -10,23 +10,16 @@ import { ColoredBlock, Tower } from '../../../../../model/tower';
|
|||
})
|
||||
export class BlockComponent {
|
||||
@Input() block: ColoredBlock;
|
||||
@Input() tower: Tower;
|
||||
@Input() tower$: Observable<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
|
||||
});
|
||||
console.log(description);
|
||||
this.block.changeProperties({
|
||||
tag: selected,
|
||||
description,
|
||||
isDone
|
||||
await this.modalService.showBlocks({
|
||||
tower$: this.tower$,
|
||||
startBlock: this.block,
|
||||
onlyDone: true
|
||||
});
|
||||
} catch {
|
||||
// pass
|
||||
|
|
|
|||
3
src/app/components/pages/page/tower/tasks/tasks.component.html
Normal file → Executable file
|
|
@ -1,8 +1,9 @@
|
|||
<div *ngIf="tasks" class="container {{ tasks.length > 0 ? 'show-hover' : '' }}">
|
||||
<div *ngIf="tasks" class="container {{ tasks.length > 0 ? 'show-hover' : '' }}" (click)="$event.stopPropagation()">
|
||||
<p class="header" (click)="isOpen = !isOpen">
|
||||
<strong>
|
||||
{{ tasks.length == 0 ? '' : tasks.length }}
|
||||
</strong>
|
||||
<!-- ​ is the zero width space -->
|
||||
{{ tasks.length == 0 ? '​' : tasks.length == 1 ? 'task' : 'tasks' }}
|
||||
</p>
|
||||
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
|
||||
|
|
|
|||
0
src/app/components/pages/page/tower/tasks/tasks.component.scss
Normal file → Executable file
50
src/app/components/pages/page/tower/tasks/tasks.component.ts
Normal file → Executable file
|
|
@ -1,19 +1,19 @@
|
|||
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||
import { Block } from '../../../../../model/block';
|
||||
import { Tower } from '../../../../../model/tower';
|
||||
import { ModalService } from '../../../../../services/modal.service';
|
||||
import { CancelService } from '../../../../../services/cancel.service';
|
||||
import { IColor } from '../../../../../interfaces/color';
|
||||
import { IBlock } from '../../../../../interfaces/persistance/block';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss']
|
||||
})
|
||||
export class TasksComponent implements OnInit {
|
||||
export class TasksComponent {
|
||||
@Input() tasks: Array<Block & { color: IColor }>;
|
||||
@Input() tower: Tower;
|
||||
@Input() tower$: Observable<Tower>;
|
||||
|
||||
private _isOpen = false;
|
||||
@Input() set isOpen(value: boolean) {
|
||||
|
|
@ -29,47 +29,27 @@ export class TasksComponent implements OnInit {
|
|||
|
||||
@ViewChild('allTask') allTask: ElementRef;
|
||||
|
||||
constructor(private modalService: ModalService, private cancelService: CancelService) {
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private cancelService: CancelService,
|
||||
private changeDetection: ChangeDetectorRef
|
||||
) {
|
||||
this.cancelService.subscribe(this, () => {
|
||||
this.isOpen = false;
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
await this.modalService.showBlocks({
|
||||
tower$: this.tower$,
|
||||
startBlock: block,
|
||||
onlyDone: false
|
||||
});
|
||||
|
||||
const change: Partial<IBlock> = {
|
||||
tag: selected,
|
||||
description,
|
||||
isDone
|
||||
};
|
||||
if (!block.isDone && isDone) {
|
||||
change.created = new Date();
|
||||
}
|
||||
|
||||
block.changeProperties(change);
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
public async addTask() {
|
||||
try {
|
||||
const { selected: tag, description, isDone } = await this.modalService.showCreateBlock({
|
||||
options: this.tower.tags,
|
||||
isTask: true
|
||||
});
|
||||
this.tower.addBlock({ tag, description, isDone });
|
||||
} catch (e) {
|
||||
// pass
|
||||
} finally {
|
||||
this.changeDetection.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/app/components/pages/page/tower/tower.component.html
Normal file → Executable file
|
|
@ -1,30 +1,31 @@
|
|||
<div class="tower" *ngIf="tower$ | async">
|
||||
<div class="container">
|
||||
<div class="tasks-container">
|
||||
<app-tasks [tasks]="tasks" [tower]="tower$ | async"></app-tasks>
|
||||
<app-tasks [tasks]="tasks" [tower$]="tower$"></app-tasks>
|
||||
</div>
|
||||
|
||||
<img src="assets/plus-sign.svg" alt="add item" (click)="addBlock()" />
|
||||
<img src="assets/plus-sign.svg" alt="add item" (click)="$event.stopPropagation() || addBlock()" />
|
||||
|
||||
<div class="block-container-container">
|
||||
<div class="block-container" *ngIf="blocks.length > 0">
|
||||
<div class="block-container" *ngIf="styledBlocks.length > 0">
|
||||
<app-block
|
||||
*ngFor="let block of drawableBlocks"
|
||||
[ngClass]="block.cssClass"
|
||||
[ngStyle]="block.style"
|
||||
*ngFor="let block of drawableBlocks"
|
||||
[block]="block"
|
||||
[tower]="tower$ | async"
|
||||
[tower$]="tower$"
|
||||
></app-block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="tower-name" class="hidden">Card name</label>
|
||||
<input
|
||||
id="tower-name"
|
||||
type="text"
|
||||
placeholder="name…"
|
||||
[(ngModel)]="towerName"
|
||||
[ngStyle]="{ color: (tower$ | async)?.baseColor | color }"
|
||||
/>
|
||||
<label class="hidden">
|
||||
Card name
|
||||
<input
|
||||
type="text"
|
||||
placeholder="name…"
|
||||
[(ngModel)]="towerName"
|
||||
[ngStyle]="{ color: (tower$ | async)?.baseColor | color }"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
24
src/app/components/pages/page/tower/tower.component.scss
Normal file → Executable file
|
|
@ -13,18 +13,25 @@
|
|||
|
||||
&:hover {
|
||||
@media (min-width: $mobile-width) {
|
||||
div {
|
||||
.container {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
div.container {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.cdk-drag-preview {
|
||||
div {
|
||||
.container {
|
||||
box-shadow: $shadow;
|
||||
div.container {
|
||||
@media (max-width: $mobile-width) {
|
||||
@keyframes shadow {
|
||||
from {
|
||||
box-shadow: none;
|
||||
}
|
||||
to {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
|
||||
animation: shadow $long-animation-time forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,9 +94,6 @@
|
|||
transition: opacity $short-animation-time;
|
||||
}
|
||||
|
||||
.tasks-container {
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
|
|
|||
73
src/app/components/pages/page/tower/tower.component.ts
Normal file → Executable file
|
|
@ -1,9 +1,10 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { ColoredBlock, Tower } from '../../../../model/tower';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Range } from '../../../../interfaces/range';
|
||||
import { top } from '../../../../utils/top';
|
||||
import { CancelService } from '../../../../services/cancel.service';
|
||||
|
||||
type StyledBlock = ColoredBlock & { style: { [p: string]: string }; shouldDraw: boolean; cssClass: string };
|
||||
|
||||
|
|
@ -14,9 +15,9 @@ type StyledBlock = ColoredBlock & { style: { [p: string]: string }; shouldDraw:
|
|||
})
|
||||
export class TowerComponent implements OnInit {
|
||||
@Input() dateRange$: Observable<Range<Date>>;
|
||||
private dateRange: Range<Date>;
|
||||
|
||||
@Input() tower$: Observable<Tower>;
|
||||
|
||||
private dateRange: Range<Date>;
|
||||
private tower: Tower;
|
||||
|
||||
get towerName(): string {
|
||||
|
|
@ -28,52 +29,55 @@ export class TowerComponent implements OnInit {
|
|||
}
|
||||
|
||||
tasks: Array<ColoredBlock>;
|
||||
blocks: Array<StyledBlock> = [];
|
||||
|
||||
styledBlocks: Array<StyledBlock> = [];
|
||||
|
||||
get drawableBlocks(): Array<StyledBlock> {
|
||||
return this.blocks.filter(b => b.shouldDraw);
|
||||
return this.styledBlocks.filter(b => b.shouldDraw);
|
||||
}
|
||||
|
||||
public constructor(private modalService: ModalService) {}
|
||||
public constructor(private modalService: ModalService, private changeDetection: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.tower$.subscribe(value => {
|
||||
// console.log(this.tower, value);
|
||||
if (value) {
|
||||
this.blocks = value.coloredBlocks
|
||||
this.styledBlocks = value.coloredBlocks
|
||||
.filter(b => b.isDone)
|
||||
.map(b => {
|
||||
let classedBlock = b as StyledBlock;
|
||||
const classedBlock = b as StyledBlock;
|
||||
classedBlock.shouldDraw = true;
|
||||
classedBlock.style = { transform: 'translateY(0)', opacity: '1' };
|
||||
classedBlock.cssClass = '';
|
||||
return classedBlock;
|
||||
});
|
||||
|
||||
if (this.tower) {
|
||||
console.log(this.tower, this.tower.latestVersion, value);
|
||||
}
|
||||
if (this.tower && this.tower.latestVersion === value) {
|
||||
let difference = this.tower.blocks.map((b, index) => {
|
||||
const difference = this.tower.blocks.map((b, index) => {
|
||||
return b === value.blocks[index];
|
||||
});
|
||||
|
||||
console.log(this.tower.blocks);
|
||||
if (
|
||||
(difference.every(i => i) && this.tower.blocks.length + 1 === value.blocks.length) ||
|
||||
(difference.every(i => i) &&
|
||||
this.tower.blocks.length + 1 === value.blocks.length &&
|
||||
top(value.blocks).isDone) ||
|
||||
(this.tower.blocks.length === value.blocks.length &&
|
||||
this.tower.blocks.filter(b => b.isDone).length + 1 === value.blocks.filter(b => b.isDone).length)
|
||||
) {
|
||||
const lastBlock = top(this.blocks);
|
||||
const lastBlock = top(this.styledBlocks);
|
||||
if (lastBlock) {
|
||||
lastBlock.style = { opacity: '0' };
|
||||
lastBlock.cssClass = 'descend';
|
||||
setTimeout(() => (lastBlock.style = { transform: 'translateY(0)', opacity: '1' }), 0);
|
||||
lastBlock.style = { transform: 'translateY(500%)', opacity: '0' };
|
||||
setTimeout(() => {
|
||||
this.makeBlockDescend(lastBlock);
|
||||
this.changeDetection.markForCheck();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tasks = value.coloredBlocks.filter(block => !block.isDone);
|
||||
|
||||
this.tower = value;
|
||||
this.changeDetection.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -83,29 +87,36 @@ export class TowerComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
makeBlockDescend(block: StyledBlock) {
|
||||
block.cssClass = 'descend';
|
||||
block.style = { transform: 'translateY(0)', opacity: '1' };
|
||||
}
|
||||
|
||||
makeBlockAscend(block: StyledBlock) {
|
||||
block.cssClass = 'ascend';
|
||||
block.style = { transform: 'translateY(500%)', opacity: '0' };
|
||||
}
|
||||
|
||||
initData(newDateRange: Range<Date>) {
|
||||
for (const block of this.blocks) {
|
||||
for (const block of this.styledBlocks) {
|
||||
block.shouldDraw = newDateRange.from <= block.created;
|
||||
|
||||
if ((block.cssClass === '' || block.cssClass === 'descend') && newDateRange.to < block.created) {
|
||||
block.cssClass = 'ascend';
|
||||
block.style = { transform: 'translateY(500%)', opacity: '0' };
|
||||
if (newDateRange.to < block.created) {
|
||||
this.makeBlockAscend(block);
|
||||
}
|
||||
if (block.shouldDraw && block.cssClass === 'ascend' && block.created < newDateRange.to) {
|
||||
block.cssClass = 'descend';
|
||||
block.style = { transform: 'translateY(0)', opacity: '1' };
|
||||
if (block.shouldDraw && block.created <= newDateRange.to) {
|
||||
this.makeBlockDescend(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async addBlock() {
|
||||
try {
|
||||
const { selected: tag, description, isDone } = await this.modalService.showCreateBlock({
|
||||
options: this.tower.tags,
|
||||
isTask: false
|
||||
await this.modalService.showBlocks({
|
||||
tower$: this.tower$,
|
||||
onlyDone: true
|
||||
});
|
||||
this.tower.addBlock({ tag, description, isDone });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
src/app/components/pages/pages.component.html
Normal file → Executable file
|
|
@ -4,7 +4,9 @@
|
|||
[options]="pageNames"
|
||||
[default]="(selectedPage$ | async)?.name"
|
||||
(value)="selectPage($event)"
|
||||
(optionChange)="changeName($event)"
|
||||
[placeholder]="'Add a new page…'"
|
||||
[editable]="true"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
|
@ -15,4 +17,6 @@
|
|||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button [ngClass]="isDragHappening ? 'transparent' : ''" (click)="openSettings()">Settings</button>
|
||||
<button [ngClass]="isDragHappening ? 'transparent' : ''" (click)="$event.stopPropagation(); openSettings()">
|
||||
Settings
|
||||
</button>
|
||||
|
|
|
|||
0
src/app/components/pages/pages.component.scss
Normal file → Executable file
39
src/app/components/pages/pages.component.ts
Normal file → Executable file
|
|
@ -1,9 +1,11 @@
|
|||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { Page } from '../../model/page';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { ModalService } from '../../services/modal.service';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Data } from '../../model/data';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
|
||||
const USER_DATA_KEY = 'life-towers.user-data.v.2';
|
||||
|
||||
|
|
@ -17,6 +19,7 @@ export class PagesComponent implements OnInit {
|
|||
@ViewChild('page') page: ElementRef;
|
||||
@ViewChild('bottom') bottom: ElementRef;
|
||||
|
||||
data: Data;
|
||||
pages: Array<Page>;
|
||||
isDragHappening = false;
|
||||
|
||||
|
|
@ -32,7 +35,11 @@ export class PagesComponent implements OnInit {
|
|||
private readonly _selectedPage: BehaviorSubject<Page> = new BehaviorSubject(null);
|
||||
readonly selectedPage$: Observable<Page> = this._selectedPage.asObservable();
|
||||
|
||||
constructor(public dataService: DataService, private modalService: ModalService) {
|
||||
constructor(
|
||||
public dataService: DataService,
|
||||
private modalService: ModalService,
|
||||
private changeDetection: ChangeDetectorRef
|
||||
) {
|
||||
const userData = JSON.parse(window.localStorage.getItem(USER_DATA_KEY));
|
||||
if (userData !== null) {
|
||||
this.selectedPageName = userData.selectedPage;
|
||||
|
|
@ -40,9 +47,11 @@ export class PagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataService.children$.subscribe(pages => {
|
||||
if (pages) {
|
||||
if (this.pages && this.pages.length - 1 === pages.length) {
|
||||
this.dataService.children$.subscribe(dataContainer => {
|
||||
if (dataContainer && dataContainer.length > 0) {
|
||||
this.data = dataContainer[0];
|
||||
const pages = this.data.pages;
|
||||
if (this.pages && !pages.includes(this._selectedPage.getValue().latestVersion)) {
|
||||
this.selectedPageName = null;
|
||||
}
|
||||
this.pages = pages;
|
||||
|
|
@ -51,8 +60,17 @@ export class PagesComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
changeName({ from, to }: { from: string; to: string }) {
|
||||
const page = this.pages.find(p => p.name === from);
|
||||
if (page) {
|
||||
if (from === this.selectedPageName) {
|
||||
this.selectedPageName = to;
|
||||
}
|
||||
page.changeName(to);
|
||||
}
|
||||
}
|
||||
|
||||
selectPage(name: string) {
|
||||
console.log(name);
|
||||
if (!name) {
|
||||
if (this.pages && this.pages.length > 0) {
|
||||
name = this.pages[0].name;
|
||||
|
|
@ -69,7 +87,7 @@ export class PagesComponent implements OnInit {
|
|||
|
||||
if (this.pages && name) {
|
||||
if (!this.pageNames.includes(name)) {
|
||||
this.dataService.addPage(name);
|
||||
this.data.addPage(name);
|
||||
}
|
||||
|
||||
const index = this.pageNames.indexOf(name);
|
||||
|
|
@ -82,9 +100,14 @@ export class PagesComponent implements OnInit {
|
|||
|
||||
async openSettings() {
|
||||
try {
|
||||
await this.modalService.showSettings(this.selectedPage$);
|
||||
await this.modalService.showSettings({
|
||||
page$: this.selectedPage$,
|
||||
data$: of(this.data)
|
||||
});
|
||||
} catch {
|
||||
// pass
|
||||
} finally {
|
||||
this.changeDetection.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
src/app/components/shared/double-slider/double-slider.component.html
Normal file → Executable file
0
src/app/components/shared/double-slider/double-slider.component.scss
Normal file → Executable file
5
src/app/components/shared/double-slider/double-slider.component.ts
Normal file → Executable file
|
|
@ -93,7 +93,7 @@ export class DoubleSliderComponent {
|
|||
}
|
||||
|
||||
private emitValue() {
|
||||
const range = {
|
||||
this.range.emit({
|
||||
from:
|
||||
this.oneValue < this.otherValue
|
||||
? this.values[this.indexFromValue(this.oneValue)]
|
||||
|
|
@ -102,7 +102,6 @@ export class DoubleSliderComponent {
|
|||
this.oneValue < this.otherValue
|
||||
? this.values[this.indexFromValue(this.otherValue)]
|
||||
: this.values[this.indexFromValue(this.oneValue)]
|
||||
};
|
||||
this.range.emit(range);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
src/app/components/shared/select-add/select-add.component.html
Normal file → Executable file
|
|
@ -1,22 +1,40 @@
|
|||
<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 class="select-add {{ onlyShadowBorder ? 'shadow-border' : '' }}" (click)="$event.stopPropagation()">
|
||||
<div #top class="top" (click)="!editMode && toggle()">
|
||||
<p [innerHTML]="selected ? selected : placeholder" *ngIf="!editMode || !selected; else editableSelected"></p>
|
||||
<ng-template #editableSelected>
|
||||
<input type="text" [value]="selected" (change)="changeOption(selected, $event)" />
|
||||
</ng-template>
|
||||
<img src="assets/arrow.svg" (click)="onArrowClick($event)" [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>
|
||||
<ng-container *ngIf="!editMode; else editableOthers">
|
||||
<p *ngFor="let option of otherOptions" [innerHTML]="option" (click)="select(option)"></p>
|
||||
</ng-container>
|
||||
<ng-template #editableOthers>
|
||||
<input
|
||||
type="text"
|
||||
*ngFor="let option of otherOptions"
|
||||
[value]="option"
|
||||
(change)="changeOption(option, $event)"
|
||||
/>
|
||||
</ng-template>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
*ngIf="options.length <= maxItemCount"
|
||||
placeholder="Add a value…"
|
||||
[placeholder]="newValuePlaceholder"
|
||||
[(ngModel)]="newOption"
|
||||
(keyup)="handleKeys($event)"
|
||||
/>
|
||||
|
||||
<button *ngIf="options.length <= maxItemCount" (click)="addNewOption()">Add</button>
|
||||
<div class="buttons">
|
||||
<button *ngIf="options.length <= maxItemCount" (click)="addNewOption()" [disabled]="!newOption">Add</button>
|
||||
<div *ngIf="editable" class="edit {{ editMode ? 'active' : '' }}" (click)="editMode = !editMode">
|
||||
<img src="assets/pen.svg" alt="edit" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
74
src/app/components/shared/select-add/select-add.component.scss
Normal file → Executable file
|
|
@ -19,7 +19,8 @@ $inner-padding: var(--medium-padding);
|
|||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
p,
|
||||
input[type='text'] {
|
||||
display: inline-block;
|
||||
@include sub-title-text();
|
||||
}
|
||||
|
|
@ -36,7 +37,7 @@ $inner-padding: var(--medium-padding);
|
|||
|
||||
.bottom-container {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
height: 300px;
|
||||
|
||||
position: absolute;
|
||||
overflow-y: hidden;
|
||||
|
|
@ -77,6 +78,75 @@ $inner-padding: var(--medium-padding);
|
|||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
height: 32px;
|
||||
@media (max-width: $mobile-width) {
|
||||
height: 24px;
|
||||
}
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
}
|
||||
|
||||
.edit {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
opacity: 0.25;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
@include square(16px);
|
||||
}
|
||||
|
||||
transition: opacity $short-animation-time;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: calc(-1 * #{$line-height});
|
||||
left: 0;
|
||||
height: $line-height;
|
||||
background-color: $text-color;
|
||||
width: 0;
|
||||
transition: width $long-animation-time;
|
||||
}
|
||||
|
||||
@media (min-width: $mobile-width) {
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:hover {
|
||||
&:before {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&:before {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
62
src/app/components/shared/select-add/select-add.component.ts
Normal file → Executable file
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
||||
import { CancelService } from '../../../services/cancel.service';
|
||||
|
||||
@Component({
|
||||
|
|
@ -8,19 +8,31 @@ import { CancelService } from '../../../services/cancel.service';
|
|||
})
|
||||
export class SelectAddComponent {
|
||||
@Input() placeholder = 'Add a new value…';
|
||||
@Input() newValuePlaceholder = 'Add a value…';
|
||||
@Input() maxItemCount = 7;
|
||||
@Input() options: string[];
|
||||
@Input() alwaysDropShadow = false;
|
||||
@Input() onlyShadowBorder = false;
|
||||
@Input() editable = false;
|
||||
|
||||
@Input() set default(value: string) {
|
||||
this.selected = value;
|
||||
if (value) {
|
||||
this.value.emit(value);
|
||||
}
|
||||
}
|
||||
|
||||
backgroundHeight: string;
|
||||
|
||||
private _editMode = false;
|
||||
set editMode(value: boolean) {
|
||||
this._editMode = value;
|
||||
this.backgroundHeight = this.getBackgroundHeight();
|
||||
}
|
||||
|
||||
get editMode(): boolean {
|
||||
return this._editMode;
|
||||
}
|
||||
|
||||
@Output() value: EventEmitter<string> = new EventEmitter();
|
||||
@Output() optionChange: EventEmitter<{ from: string; to: string }> = new EventEmitter();
|
||||
|
||||
@ViewChild('top') top: ElementRef;
|
||||
@ViewChild('bottom') bottom: ElementRef;
|
||||
|
|
@ -29,9 +41,19 @@ export class SelectAddComponent {
|
|||
newOption: string;
|
||||
isOpen = false;
|
||||
|
||||
constructor(private cancelService: CancelService) {
|
||||
constructor(private cancelService: CancelService, private changeDetection: ChangeDetectorRef) {
|
||||
this.cancelService.subscribe(this, () => {
|
||||
this.isOpen = false;
|
||||
this.editMode = false;
|
||||
this.changeDetection.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
changeOption(from: string, event) {
|
||||
// console.log(event);
|
||||
this.optionChange.emit({
|
||||
from,
|
||||
to: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -45,15 +67,6 @@ export class SelectAddComponent {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -69,5 +82,26 @@ export class SelectAddComponent {
|
|||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (!this.isOpen) {
|
||||
this.editMode = false;
|
||||
}
|
||||
this.backgroundHeight = this.getBackgroundHeight();
|
||||
}
|
||||
|
||||
onArrowClick(event) {
|
||||
if (this.editMode) {
|
||||
this.toggle();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private getBackgroundHeight(): string {
|
||||
if (this.isOpen && this.top && this.bottom) {
|
||||
const topHeight = this.top.nativeElement.clientHeight;
|
||||
const bottomHeight = this.bottom.nativeElement.clientHeight;
|
||||
// console.log(topHeight, bottomHeight);
|
||||
return `${topHeight + bottomHeight}px`;
|
||||
}
|
||||
return `100%`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
src/app/components/shared/toggle/toggle.component.html
Normal file → Executable file
|
|
@ -1,5 +1,7 @@
|
|||
<span [className]="!on ? 'active' : ''" (click)="on = false" [innerText]="beforeText"></span>
|
||||
|
||||
<input type="checkbox" [(ngModel)]="on" [className]="on ? 'on' : ''" />
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="on" [className]="on ? 'on' : ''" />
|
||||
</label>
|
||||
|
||||
<span [className]="on ? 'active' : ''" (click)="on = true" [innerText]="afterText"></span>
|
||||
|
|
|
|||
64
src/app/components/shared/toggle/toggle.component.scss
Normal file → Executable file
|
|
@ -24,46 +24,52 @@
|
|||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
label {
|
||||
display: block;
|
||||
|
||||
width: 2 * $size;
|
||||
height: $size;
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
|
||||
border-radius: 1000px;
|
||||
box-shadow: $shadow-border;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
|
||||
@include square($size);
|
||||
width: 2 * $size;
|
||||
height: $size;
|
||||
|
||||
border-radius: 1000px;
|
||||
background-color: $text-color;
|
||||
box-shadow: $shadow-border;
|
||||
|
||||
transition: box-shadow $long-animation-time, left $long-animation-time, transform $long-animation-time;
|
||||
}
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: $mobile-width) {
|
||||
&:hover:after {
|
||||
box-shadow: $shadow;
|
||||
transform: translateX(2px);
|
||||
&: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;
|
||||
}
|
||||
|
||||
&.on:hover:after {
|
||||
transform: translateX(-2px);
|
||||
&.on:after {
|
||||
left: $size;
|
||||
}
|
||||
}
|
||||
|
||||
&.on:after {
|
||||
left: $size;
|
||||
input[type='checkbox'] {
|
||||
@media (min-width: $mobile-width) {
|
||||
&:hover:after {
|
||||
box-shadow: $shadow;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
&.on:hover:after {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
src/app/components/shared/toggle/toggle.component.ts
Normal file → Executable file
0
src/app/interfaces/color.ts
Normal file → Executable file
4
src/app/interfaces/persistance/block.ts
Normal file → Executable file
|
|
@ -1,4 +1,6 @@
|
|||
export interface IBlock {
|
||||
import { IUnique } from './unique';
|
||||
|
||||
export interface IBlock extends IUnique {
|
||||
created: Date;
|
||||
tag: string;
|
||||
isDone: boolean;
|
||||
|
|
|
|||
6
src/app/interfaces/persistance/data.ts
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
import { IPage } from './page';
|
||||
import { IUnique } from './unique';
|
||||
|
||||
export interface IData extends IUnique {
|
||||
pages: Array<IPage>;
|
||||
}
|
||||
7
src/app/interfaces/persistance/page.ts
Normal file → Executable file
|
|
@ -1,12 +1,13 @@
|
|||
import { ITower } from './tower';
|
||||
import { Range } from '../range';
|
||||
import { IUnique } from './unique';
|
||||
|
||||
export interface IPage {
|
||||
export interface IPage extends IUnique {
|
||||
name: string;
|
||||
towers: ITower[];
|
||||
|
||||
userData: {
|
||||
hideCreateTowerButton?: boolean;
|
||||
defaultDateRange?: Range<Date>;
|
||||
hideCreateTowerButton: boolean;
|
||||
defaultDateRange: Range<Date>;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
3
src/app/interfaces/persistance/tower.ts
Normal file → Executable file
|
|
@ -1,7 +1,8 @@
|
|||
import { IBlock } from './block';
|
||||
import { IColor } from '../color';
|
||||
import { IUnique } from './unique';
|
||||
|
||||
export interface ITower {
|
||||
export interface ITower extends IUnique {
|
||||
name: string;
|
||||
blocks: IBlock[];
|
||||
baseColor: IColor;
|
||||
|
|
|
|||
3
src/app/interfaces/persistance/unique.ts
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
export interface IUnique {
|
||||
id?: string;
|
||||
}
|
||||
0
src/app/interfaces/range.ts
Normal file → Executable file
3
src/app/interfaces/serializable.ts
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
export interface ISerializable {
|
||||
serialize(referenceSerializer: (ref: object) => any): object;
|
||||
}
|
||||
56
src/app/model/block.ts
Normal file → Executable file
|
|
@ -1,30 +1,36 @@
|
|||
import { Serializable } from './serializable';
|
||||
import { IBlock } from '../interfaces/persistance/block';
|
||||
import { Node } from '../store/node';
|
||||
import { InnerNode, InnerNodeState } from '../store/inner-node';
|
||||
|
||||
export class Block extends Serializable implements IBlock {
|
||||
constructor(parent: Node, props: IBlock) {
|
||||
super(parent, props, 'Block');
|
||||
this.onAfterClone();
|
||||
}
|
||||
export interface BlockState extends IBlock, InnerNodeState {}
|
||||
|
||||
protected onAfterClone(): void {
|
||||
if (this.created.constructor.name !== 'Date') {
|
||||
this.created = new Date(this.created);
|
||||
}
|
||||
|
||||
// TODO: remove.
|
||||
if (this.isDone === null || this.isDone === undefined) {
|
||||
this.isDone = false;
|
||||
}
|
||||
}
|
||||
|
||||
changeProperties(values: Partial<IBlock>) {
|
||||
this.changeKeys(values);
|
||||
}
|
||||
|
||||
created: Date;
|
||||
isDone: boolean;
|
||||
readonly description: string;
|
||||
export class Block extends InnerNode implements IBlock, BlockState {
|
||||
readonly tag: string;
|
||||
readonly description: string;
|
||||
readonly isDone: boolean;
|
||||
readonly created: Date;
|
||||
|
||||
constructor(props: IBlock, referenceDeserializer: (from: any) => any = e => e) {
|
||||
super([], props.id);
|
||||
if (props.created.constructor.name !== 'Date') {
|
||||
props.created = new Date(props.created);
|
||||
}
|
||||
this.tag = props.tag;
|
||||
this.description = props.description;
|
||||
this.isDone = props.isDone;
|
||||
this.created = props.created;
|
||||
}
|
||||
|
||||
changeKeys(props: Partial<BlockState>): this {
|
||||
return super.changeKeys<BlockState>(props);
|
||||
}
|
||||
|
||||
serialize(referenceSerializer: (ref: object) => any): IBlock {
|
||||
return {
|
||||
...super.serialize(referenceSerializer),
|
||||
tag: this.tag,
|
||||
description: this.description,
|
||||
isDone: this.isDone,
|
||||
created: this.created
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/app/model/data.ts
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
import { InnerNode, InnerNodeState } from '../store/inner-node';
|
||||
import { IData } from '../interfaces/persistance/data';
|
||||
import { Page } from './page';
|
||||
|
||||
export interface DataState extends IData, InnerNodeState {}
|
||||
|
||||
export class Data extends InnerNode implements IData, DataState {
|
||||
constructor(props: IData, referenceDeserializer: (from: any) => any = e => e) {
|
||||
super(props.pages.map(p => new Page(referenceDeserializer(p), referenceDeserializer)), props.id);
|
||||
}
|
||||
|
||||
get pages(): Array<Page> {
|
||||
return this.children as Array<Page>;
|
||||
}
|
||||
|
||||
addPage(name: string) {
|
||||
const page = new Page({
|
||||
name,
|
||||
userData: {
|
||||
hideCreateTowerButton: false,
|
||||
defaultDateRange: {
|
||||
from: null,
|
||||
to: null
|
||||
}
|
||||
},
|
||||
towers: []
|
||||
});
|
||||
this.addChildren([page]);
|
||||
page.addTower();
|
||||
}
|
||||
|
||||
removePage(page: Page) {
|
||||
this.changeKeys<any>({
|
||||
children: this.children.filter(c => c !== page)
|
||||
});
|
||||
}
|
||||
|
||||
serialize(referenceSerializer: (ref: object) => any): IData {
|
||||
return {
|
||||
...super.serialize(referenceSerializer),
|
||||
pages: this.pages.map(referenceSerializer)
|
||||
};
|
||||
}
|
||||
}
|
||||
80
src/app/model/page.ts
Normal file → Executable file
|
|
@ -1,30 +1,41 @@
|
|||
import { Serializable } from './serializable';
|
||||
import { IPage } from '../interfaces/persistance/page';
|
||||
import { Range } from '../interfaces/range';
|
||||
import { Tower } from './tower';
|
||||
import { Node } from '../store/node';
|
||||
import { InnerNode, InnerNodeState } from '../store/inner-node';
|
||||
|
||||
export class Page extends Serializable implements IPage {
|
||||
constructor(parent: Node, props: IPage) {
|
||||
super(parent, props, 'Page');
|
||||
export interface PageState extends InnerNodeState, IPage {
|
||||
towers: Array<Tower>;
|
||||
}
|
||||
|
||||
export class Page extends InnerNode implements IPage, PageState {
|
||||
readonly name: string;
|
||||
|
||||
readonly userData: {
|
||||
hideCreateTowerButton: boolean;
|
||||
defaultDateRange: Range<Date>;
|
||||
};
|
||||
|
||||
constructor(props: IPage, referenceDeserializer: (from: any) => any = e => e) {
|
||||
super(props.towers.map(t => new Tower(referenceDeserializer(t), referenceDeserializer)), props.id);
|
||||
this.name = props.name;
|
||||
this.userData = props.userData;
|
||||
}
|
||||
|
||||
readonly name: string;
|
||||
get towers(): Array<Tower> {
|
||||
return this.children as Array<Tower>;
|
||||
}
|
||||
|
||||
readonly userData: {
|
||||
hideCreateTowerButton: boolean;
|
||||
defaultDateRange: {
|
||||
from: Date;
|
||||
to: Date;
|
||||
};
|
||||
};
|
||||
changeProps(props: Partial<PageState>): this {
|
||||
if (props.hasOwnProperty('towers')) {
|
||||
props.children = props.towers;
|
||||
delete props.towers;
|
||||
}
|
||||
return this.changeKeys<PageState>(props);
|
||||
}
|
||||
|
||||
setHideCreateTowerButton(value: boolean) {
|
||||
this.changeKey({
|
||||
propertyName: 'userData',
|
||||
value: {
|
||||
this.changeProps({
|
||||
userData: {
|
||||
...this.userData,
|
||||
hideCreateTowerButton: value
|
||||
}
|
||||
|
|
@ -41,9 +52,14 @@ export class Page extends Serializable implements IPage {
|
|||
towers.splice(previousIndex, 1);
|
||||
towers.splice(currentIndex, 0, tower);
|
||||
|
||||
this.changeValue({
|
||||
oldValue: this.towers,
|
||||
newValue: towers
|
||||
this.changeProps({
|
||||
towers
|
||||
});
|
||||
}
|
||||
|
||||
changeName(to: string) {
|
||||
this.changeProps({
|
||||
name: to
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -53,17 +69,27 @@ export class Page extends Serializable implements IPage {
|
|||
hue = Math.random() * 360;
|
||||
} while (30 <= hue && hue <= 200);
|
||||
|
||||
new Tower(this, {
|
||||
name,
|
||||
blocks: [],
|
||||
baseColor: { h: hue, s: 100, l: 50 }
|
||||
});
|
||||
this.addChildren([
|
||||
new Tower({
|
||||
name,
|
||||
blocks: [],
|
||||
baseColor: { h: hue, s: 100, l: 50 }
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
removeTower(tower: Tower) {
|
||||
this.changeValue({
|
||||
oldValue: this.towers,
|
||||
newValue: this.towers.filter(t => t !== tower)
|
||||
this.changeProps({
|
||||
towers: this.towers.filter(t => t !== tower)
|
||||
});
|
||||
}
|
||||
|
||||
serialize(referenceSerializer: (ref: object) => any): IPage {
|
||||
return {
|
||||
...super.serialize(referenceSerializer),
|
||||
name: this.name,
|
||||
userData: this.userData,
|
||||
towers: this.towers.map(referenceSerializer)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import { Cloneable } from '../store/cloneable';
|
||||
import { Node } from '../store/node';
|
||||
|
||||
export class Serializable extends Cloneable {
|
||||
protected type: string;
|
||||
|
||||
private static propertyList: any = {};
|
||||
static childrenMap: {
|
||||
[type: string]: {
|
||||
childrenConstructor: typeof Serializable;
|
||||
childrenListName: string;
|
||||
childrenType: string;
|
||||
};
|
||||
};
|
||||
|
||||
protected onAfterClone(): void {
|
||||
// pass
|
||||
}
|
||||
|
||||
protected constructor(parent: Node, properties: any, type: string) {
|
||||
super(parent);
|
||||
|
||||
const compiledType = this.constructor.name;
|
||||
if (!Serializable.propertyList.hasOwnProperty(compiledType)) {
|
||||
Serializable.propertyList[compiledType] = [];
|
||||
}
|
||||
for (const property in properties) {
|
||||
if (properties.hasOwnProperty(property)) {
|
||||
const propertyValue = properties[property];
|
||||
// This should be ran after the original constructor has finished.
|
||||
console.log(type);
|
||||
if (property === Serializable.childrenMap[type].childrenListName) {
|
||||
new Promise(r => r()).then(() => {
|
||||
for (let child of propertyValue) {
|
||||
new Serializable.childrenMap[type].childrenConstructor(
|
||||
this,
|
||||
child,
|
||||
Serializable.childrenMap[type].childrenType
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this[property] = properties[property];
|
||||
}
|
||||
|
||||
if (!Serializable.propertyList[compiledType].includes(property)) {
|
||||
Serializable.propertyList[compiledType].push(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return Serializable.propertyList[this.constructor.name].reduce(
|
||||
(object, property) => ({
|
||||
[property]: this[property],
|
||||
...object
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
76
src/app/model/tower.ts
Normal file → Executable file
|
|
@ -1,40 +1,75 @@
|
|||
import { ITower } from '../interfaces/persistance/tower';
|
||||
import { lighten } from '../utils/color';
|
||||
import { Block } from './block';
|
||||
import { Serializable } from './serializable';
|
||||
import { hash } from '../utils/hash';
|
||||
import { Node } from '../store/node';
|
||||
import { IColor } from '../interfaces/color';
|
||||
import { InnerNode, InnerNodeState } from '../store/inner-node';
|
||||
|
||||
export type ColoredBlock = Block & { color: IColor };
|
||||
|
||||
export class Tower extends Serializable implements ITower {
|
||||
protected type = 'Tower';
|
||||
export interface TowerState extends ITower, InnerNodeState {
|
||||
blocks: Array<Block>;
|
||||
}
|
||||
|
||||
export class Tower extends InnerNode implements ITower, TowerState {
|
||||
readonly name: string;
|
||||
readonly baseColor: IColor;
|
||||
tags: string[];
|
||||
name: string;
|
||||
coloredBlocks: Array<ColoredBlock>;
|
||||
|
||||
constructor(props: ITower, referenceDeserializer: (from: any) => any = e => e) {
|
||||
super(props.blocks.map(b => new Block(referenceDeserializer(b), referenceDeserializer)), props.id);
|
||||
this.name = props.name;
|
||||
this.baseColor = props.baseColor;
|
||||
this.onAfterClone();
|
||||
}
|
||||
|
||||
get blocks(): Array<Block> {
|
||||
return this.children as Array<Block>;
|
||||
}
|
||||
|
||||
coloredBlocks: Array<ColoredBlock>;
|
||||
|
||||
readonly baseColor: IColor;
|
||||
|
||||
constructor(parent: Node, props: ITower) {
|
||||
super(parent, props, 'Tower');
|
||||
this.onAfterClone();
|
||||
changeKeys(props: Partial<TowerState>): this {
|
||||
if (props.hasOwnProperty('blocks')) {
|
||||
props.children = props.blocks;
|
||||
delete props.blocks;
|
||||
}
|
||||
return super.changeKeys<TowerState>(props);
|
||||
}
|
||||
|
||||
protected onAfterClone(): void {
|
||||
addBlock(props: { tag: string; description: string; isDone: boolean }) {
|
||||
this.addChildren([
|
||||
new Block({
|
||||
created: new Date(),
|
||||
...props
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
changeName(name: string) {
|
||||
this.changeKeys({ name });
|
||||
}
|
||||
|
||||
getColorOfTag(tag: string): IColor {
|
||||
return lighten((hash(tag) - 0.5) * 50, this.baseColor);
|
||||
}
|
||||
|
||||
serialize(referenceSerializer: (ref: object) => any): ITower {
|
||||
return {
|
||||
...super.serialize(referenceSerializer),
|
||||
name: this.name,
|
||||
baseColor: this.baseColor,
|
||||
blocks: this.blocks.map(referenceSerializer)
|
||||
};
|
||||
}
|
||||
|
||||
protected onAfterClone() {
|
||||
this.blocks.sort((a, b) => {
|
||||
return a.created.getTime() - b.created.getTime();
|
||||
});
|
||||
|
||||
this.coloredBlocks = this.blocks.map(b => {
|
||||
const coloredBlock = b as ColoredBlock;
|
||||
coloredBlock.color = lighten((hash(coloredBlock.tag) - 0.5) * 50, this.baseColor);
|
||||
coloredBlock.color = this.getColorOfTag(b.tag);
|
||||
return coloredBlock;
|
||||
});
|
||||
|
||||
|
|
@ -45,17 +80,4 @@ export class Tower extends Serializable implements ITower {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBlock(props: { tag: string; description: string; isDone: boolean }) {
|
||||
new Block(this, {
|
||||
created: new Date(),
|
||||
...props
|
||||
});
|
||||
}
|
||||
|
||||
changeName(newName: string) {
|
||||
// For optimization purposes.
|
||||
this.name = newName;
|
||||
this.mutatedUpdate();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
src/app/pipes/color.pipe.ts
Normal file → Executable file
49
src/app/pipes/format-date.pipe.ts
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'formatDate',
|
||||
pure: false
|
||||
})
|
||||
export class FormatDatePipe implements PipeTransform {
|
||||
transform(value: Date): string {
|
||||
const now = new Date();
|
||||
|
||||
const years = Math.floor(now.getFullYear() - value.getFullYear());
|
||||
const months = Math.floor(now.getMonth() - value.getMonth());
|
||||
const days = Math.floor(now.getDay() - value.getDay());
|
||||
const minutes = Math.floor(now.getMinutes() - value.getMinutes());
|
||||
const seconds = Math.floor(now.getSeconds() - value.getSeconds());
|
||||
|
||||
if (years === 1) {
|
||||
return 'a year ago';
|
||||
} else if (years > 1) {
|
||||
return `${years} years ago`;
|
||||
}
|
||||
|
||||
if (months === 1) {
|
||||
return 'a month ago';
|
||||
} else if (months > 1) {
|
||||
return `${months} months ago`;
|
||||
}
|
||||
|
||||
if (days === 1) {
|
||||
return 'a day ago';
|
||||
} else if (days > 1) {
|
||||
return `${days} days ago`;
|
||||
}
|
||||
|
||||
if (minutes === 1) {
|
||||
return 'a minute ago';
|
||||
} else if (minutes > 1) {
|
||||
return `${minutes} minutes ago`;
|
||||
}
|
||||
|
||||
if (seconds === 1) {
|
||||
return 'just now';
|
||||
} else if (seconds > 1) {
|
||||
return `${seconds} seconds ago`;
|
||||
}
|
||||
|
||||
return 'just now';
|
||||
}
|
||||
}
|
||||
53
src/app/services/api.service.ts
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Unique } from '../store/unique';
|
||||
|
||||
const API_URI = 'https://store.schmelczer.dev/api/store/';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
private static getAuthorizationHeader(id: string): HttpHeaders {
|
||||
return new HttpHeaders().set('Authorization', `life-towers-v3 ${id}`);
|
||||
}
|
||||
|
||||
async track(id: string): Promise<void> {
|
||||
await this.http.post(`${API_URI}me`, {}, { headers: ApiService.getAuthorizationHeader(id) }).toPromise();
|
||||
}
|
||||
|
||||
async register(id: string): Promise<void> {
|
||||
await this.http.post(API_URI, { token: id }).toPromise();
|
||||
}
|
||||
|
||||
async getObject(userId: string, objectId: string): Promise<Unique> {
|
||||
return await this.http
|
||||
.get<Unique>(`${API_URI}me/${objectId}`, { headers: ApiService.getAuthorizationHeader(userId) })
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
async postObject(userId: string, objectId: string, serializedObject: string): Promise<void> {
|
||||
await this.http
|
||||
.post(
|
||||
`${API_URI}me/${objectId}`,
|
||||
{ data: serializedObject },
|
||||
{ headers: ApiService.getAuthorizationHeader(userId) }
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
async getRootId(userId: string): Promise<string> {
|
||||
return await this.http
|
||||
// @ts-ignore
|
||||
.get<string>(`${API_URI}me/root`, { headers: ApiService.getAuthorizationHeader(userId), responseType: 'text' })
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
async setRootId(userId: string, rootId: string): Promise<void> {
|
||||
await this.http
|
||||
.put(`${API_URI}me/root`, { root_id: rootId }, { headers: ApiService.getAuthorizationHeader(userId) })
|
||||
.toPromise();
|
||||
}
|
||||
}
|
||||
2
src/app/services/cancel.service.ts
Normal file → Executable file
|
|
@ -11,8 +11,6 @@ interface Subscriber {
|
|||
export class CancelService {
|
||||
private subscribers: Subscriber[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
subscribe(object: object, callback: () => void) {
|
||||
this.subscribers.push({
|
||||
object,
|
||||
|
|
|
|||
89
src/app/services/data.service.ts
Normal file → Executable file
|
|
@ -1,86 +1,35 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { StoreService } from './store.service';
|
||||
import { Page } from '../model/page';
|
||||
import { Root } from '../store/root';
|
||||
import { Serializable } from '../model/serializable';
|
||||
import { Tower } from '../model/tower';
|
||||
import { Block } from '../model/block';
|
||||
import { IPage } from '../interfaces/persistance/page';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MapStoreService } from './map-store.service';
|
||||
import { Data } from '../model/data';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService extends Root<Page> {
|
||||
get pages(): Array<Page> {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
private readonly _safeChildren: BehaviorSubject<Array<Page>> = new BehaviorSubject(null);
|
||||
readonly safeChildren$: Observable<Array<Page>> = this._safeChildren.asObservable();
|
||||
|
||||
constructor(private storeService: StoreService<Array<IPage>>) {
|
||||
export class DataService extends Root<Data> {
|
||||
private shouldSave = true;
|
||||
constructor(private store: MapStoreService) {
|
||||
super();
|
||||
this.init().catch();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
const pages = await this.storeService.load();
|
||||
Serializable.childrenMap = {
|
||||
Page: {
|
||||
childrenListName: 'towers',
|
||||
childrenConstructor: Tower,
|
||||
childrenType: 'Tower'
|
||||
},
|
||||
Tower: {
|
||||
childrenListName: 'blocks',
|
||||
childrenConstructor: Block,
|
||||
childrenType: 'Block'
|
||||
},
|
||||
Block: {
|
||||
childrenListName: null,
|
||||
childrenConstructor: null,
|
||||
childrenType: null
|
||||
this.store.data.subscribe(d => {
|
||||
if (d) {
|
||||
this.shouldSave = false;
|
||||
this.changeKeys({ children: [d] });
|
||||
}
|
||||
};
|
||||
|
||||
for (let page of pages) {
|
||||
new Page(this, page);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.children$.subscribe(value => {
|
||||
this.log();
|
||||
});
|
||||
}, 0);
|
||||
|
||||
this.children$.subscribe(value => {
|
||||
this._safeChildren.next(value);
|
||||
this.save(0);
|
||||
});
|
||||
}
|
||||
|
||||
mutatedUpdate() {
|
||||
this.save(2500);
|
||||
}
|
||||
|
||||
save(timeout: number) {
|
||||
this.storeService.scheduleSave(this.pages, timeout);
|
||||
}
|
||||
|
||||
addPage(name: string) {
|
||||
const page = new Page(this, {
|
||||
name,
|
||||
userData: {},
|
||||
towers: []
|
||||
this.children$.subscribe(_ => {
|
||||
this.log();
|
||||
});
|
||||
page.addTower();
|
||||
}
|
||||
|
||||
removePage(page: Page) {
|
||||
this.changeValue({
|
||||
oldValue: this.children,
|
||||
newValue: this.children.filter(c => c !== page)
|
||||
this.children$.subscribe(data => {
|
||||
if (data && data.length && data[0]) {
|
||||
if (!this.shouldSave) {
|
||||
this.shouldSave = true;
|
||||
return;
|
||||
}
|
||||
this.store.save(data[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
191
src/app/services/map-store.service.ts
Executable file
|
|
@ -0,0 +1,191 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import * as uuid from 'uuid';
|
||||
import { Data } from '../model/data';
|
||||
import { ITower } from '../interfaces/persistance/tower';
|
||||
import { IPage } from '../interfaces/persistance/page';
|
||||
import { IData } from '../interfaces/persistance/data';
|
||||
import { IUnique } from '../interfaces/persistance/unique';
|
||||
import { ApiService } from './api.service';
|
||||
import { Unique } from '../store/unique';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'life-towers.data.v.3';
|
||||
|
||||
interface LifeTowersData {
|
||||
token: string;
|
||||
root: string;
|
||||
objects: {
|
||||
[id: string]: IUnique;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MapStoreService {
|
||||
private state: LifeTowersData;
|
||||
private canSaveTrigger: () => void;
|
||||
private canSave = new Promise(r => (this.canSaveTrigger = r));
|
||||
private dataToSave: Data;
|
||||
|
||||
private saveEverything = false;
|
||||
|
||||
private readonly _data: BehaviorSubject<Data> = new BehaviorSubject(null);
|
||||
readonly data: Observable<Data> = this._data.asObservable();
|
||||
|
||||
constructor(private api: ApiService) {
|
||||
const storedData: string = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
|
||||
if (storedData) {
|
||||
this.state = JSON.parse(storedData);
|
||||
this.initWithLoad().catch();
|
||||
} else {
|
||||
this.initWithRegister().catch();
|
||||
}
|
||||
this.api.track(this.state.token).catch();
|
||||
}
|
||||
|
||||
private static getSeed(): LifeTowersData {
|
||||
const towerId = uuid.v4();
|
||||
const tower: ITower = {
|
||||
id: towerId,
|
||||
name: null,
|
||||
blocks: [],
|
||||
baseColor: { h: 0, s: 100, l: 50 }
|
||||
};
|
||||
|
||||
const pageId = uuid.v4();
|
||||
const page: IPage = {
|
||||
id: pageId,
|
||||
name: 'My first page',
|
||||
towers: [towerId],
|
||||
userData: {
|
||||
hideCreateTowerButton: false,
|
||||
defaultDateRange: {
|
||||
from: null,
|
||||
to: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dataId = uuid.v4();
|
||||
const data: IData = {
|
||||
id: dataId,
|
||||
pages: [pageId]
|
||||
};
|
||||
|
||||
return {
|
||||
token: uuid.v4(),
|
||||
root: dataId,
|
||||
objects: {
|
||||
[dataId]: data,
|
||||
[pageId]: page,
|
||||
[towerId]: tower
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
save(root: Data) {
|
||||
this.dataToSave = root;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.dataToSave === root) {
|
||||
this.realSave(root).catch();
|
||||
}
|
||||
}, 750);
|
||||
}
|
||||
|
||||
private get root(): Data {
|
||||
return new Data(this.state.objects[this.state.root] as IData, id => this.state.objects[id]);
|
||||
}
|
||||
|
||||
get userToken(): string {
|
||||
return this.state.token;
|
||||
}
|
||||
|
||||
set userToken(value: string) {
|
||||
this.state.token = value;
|
||||
this.initWithLoad().catch();
|
||||
}
|
||||
|
||||
private async realSave(root: Data): Promise<void> {
|
||||
await this.canSave;
|
||||
|
||||
const waiting: Array<Unique> = [root];
|
||||
const referenceSerializer = (e: Unique): string => {
|
||||
waiting.push(e);
|
||||
return e.id;
|
||||
};
|
||||
|
||||
while (waiting.length > 0) {
|
||||
const candidate = waiting.pop();
|
||||
if (!this.saveEverything && this.state.objects.hasOwnProperty(candidate.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const serialized = candidate.serialize(referenceSerializer);
|
||||
|
||||
this.state.objects[candidate.id] = serialized;
|
||||
this.api.postObject(this.state.token, candidate.id, JSON.stringify(serialized)).catch();
|
||||
}
|
||||
|
||||
this.api.setRootId(this.state.token, root.id).catch();
|
||||
this.state.root = root.id;
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.state));
|
||||
}
|
||||
|
||||
private async initWithRegister(id?: string) {
|
||||
this.state = MapStoreService.getSeed();
|
||||
if (id) {
|
||||
this.state.token = id;
|
||||
}
|
||||
this._data.next(this.root);
|
||||
|
||||
await this.api.register(this.state.token).catch();
|
||||
this.canSaveTrigger();
|
||||
|
||||
this.saveEverything = true;
|
||||
await this.realSave(this.root);
|
||||
this.saveEverything = false;
|
||||
}
|
||||
|
||||
private async initWithLoad() {
|
||||
this.canSave = new Promise(r => (this.canSaveTrigger = r));
|
||||
|
||||
let realRoot: string;
|
||||
try {
|
||||
realRoot = await this.api.getRootId(this.state.token).catch();
|
||||
} catch {
|
||||
this.initWithRegister(this.state.token).catch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.root !== realRoot) {
|
||||
this.state.root = realRoot;
|
||||
const root = await this.api.getObject(this.state.token, realRoot);
|
||||
this.state.objects[this.state.root] = root;
|
||||
|
||||
const getUnknowns = async (element: any) => {
|
||||
const childrenAliases = ['pages', 'towers', 'blocks'];
|
||||
|
||||
for (const childrenAlias of childrenAliases) {
|
||||
if (element.hasOwnProperty(childrenAlias)) {
|
||||
for (const p of element[childrenAlias]) {
|
||||
if (!this.state.objects.hasOwnProperty(p)) {
|
||||
const unknown = await this.api.getObject(this.state.token, p);
|
||||
this.state.objects[p] = unknown;
|
||||
await getUnknowns(unknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await getUnknowns(root);
|
||||
}
|
||||
this._data.next(this.root);
|
||||
this.canSaveTrigger();
|
||||
}
|
||||
}
|
||||
38
src/app/services/modal.service.ts
Normal file → Executable file
|
|
@ -4,11 +4,11 @@ import { top } from '../utils/top';
|
|||
import { CancelService } from './cancel.service';
|
||||
import { Page } from '../model/page';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Block } from '../model/block';
|
||||
import { Data } from '../model/data';
|
||||
|
||||
export enum ModalType {
|
||||
createBlock,
|
||||
editBlock,
|
||||
removeBlock,
|
||||
blocks,
|
||||
settings,
|
||||
removeTower,
|
||||
removePage,
|
||||
|
|
@ -30,24 +30,12 @@ export class ModalService {
|
|||
|
||||
constructor(private cancelService: CancelService) {}
|
||||
|
||||
showCreateBlock(input: {
|
||||
options: string[];
|
||||
isTask: boolean;
|
||||
}): Promise<{ selected: string; description: string; isDone: boolean }> {
|
||||
return this.createPromiseAndPushToStack(input, ModalType.createBlock);
|
||||
showBlocks(input: { tower$: Observable<Tower>; onlyDone: boolean; startBlock?: Block }): Promise<void> {
|
||||
return this.createPromiseAndPushToStack(input, ModalType.blocks);
|
||||
}
|
||||
|
||||
showEditBlock(data: {
|
||||
default: string;
|
||||
options: string[];
|
||||
description: string;
|
||||
isDone: boolean;
|
||||
}): Promise<{ selected: string; description: string; isDone: boolean }> {
|
||||
return this.createPromiseAndPushToStack(data, ModalType.editBlock);
|
||||
}
|
||||
|
||||
showSettings(selectedPage: Observable<Page>): Promise<void> {
|
||||
return this.createPromiseAndPushToStack(selectedPage, ModalType.settings);
|
||||
showSettings(options: { page$: Observable<Page>; data$: Observable<Data> }): Promise<void> {
|
||||
return this.createPromiseAndPushToStack(options, ModalType.settings);
|
||||
}
|
||||
|
||||
showRemoveTower(tower: Tower): Promise<void> {
|
||||
|
|
@ -63,13 +51,17 @@ export class ModalService {
|
|||
}
|
||||
|
||||
submit(output?: any) {
|
||||
const { resolve } = this.modalStack.pop();
|
||||
resolve(output);
|
||||
const modal = this.modalStack.pop();
|
||||
if (modal) {
|
||||
modal.resolve(output);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
const { reject } = this.modalStack.pop();
|
||||
reject();
|
||||
const modal = this.modalStack.pop();
|
||||
if (modal) {
|
||||
modal.reject();
|
||||
}
|
||||
}
|
||||
|
||||
private createPromiseAndPushToStack(input: any, type: ModalType): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Page } from '../model/page';
|
||||
import { IPage } from '../interfaces/persistance/page';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'life-towers.data.v.2';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StoreService<T> {
|
||||
private saveScheduled = false;
|
||||
private dataToSave: T;
|
||||
private storedData: T;
|
||||
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',
|
||||
isDone: false
|
||||
},
|
||||
{
|
||||
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',
|
||||
isDone: false
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 3, 15),
|
||||
tag: 'go to school',
|
||||
description: 'done it',
|
||||
isDone: true
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 3, 15, 19),
|
||||
tag: 'go to school',
|
||||
isDone: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
baseColor: { h: 180, s: 100, l: 50 },
|
||||
name: 'life',
|
||||
blocks: [
|
||||
{
|
||||
created: new Date(2019, 3, 13),
|
||||
tag: 'go home',
|
||||
description: 'done it',
|
||||
isDone: false
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 4, 13),
|
||||
tag: 'go home',
|
||||
isDone: false
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 4, 15),
|
||||
tag: 'go to work',
|
||||
description: 'done it',
|
||||
isDone: false
|
||||
},
|
||||
{
|
||||
created: new Date(2019, 4, 15, 14),
|
||||
tag: 'go to work',
|
||||
isDone: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
constructor() {
|
||||
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T;
|
||||
}
|
||||
|
||||
scheduleSave(data: T, timeout: number) {
|
||||
this.dataToSave = data;
|
||||
if (!this.saveScheduled) {
|
||||
this.saveScheduled = true;
|
||||
setTimeout(() => {
|
||||
this.saveScheduled = false;
|
||||
this.save(this.dataToSave).catch();
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async load(): Promise<T> {
|
||||
console.log('load', this.storedData);
|
||||
return this.storedData;
|
||||
}
|
||||
|
||||
async save(data: T): Promise<void> {
|
||||
this.storedData = data;
|
||||
const stringified = JSON.stringify(this.storedData, null, 2);
|
||||
console.log('save');
|
||||
// console.log('save', stringified);
|
||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, stringified);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
import { InnerNode } from './inner-node';
|
||||
import { Node } from './node';
|
||||
|
||||
export abstract class Cloneable extends InnerNode {
|
||||
protected constructor(parent: Node) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
protected abstract onAfterClone(): void;
|
||||
|
||||
protected cloneWithMap(map: (node: this) => void): this {
|
||||
const insides = Object.getOwnPropertyDescriptors(this);
|
||||
|
||||
const insidesProxy = new Proxy(insides, {
|
||||
get: (target, prop, proxy) => {
|
||||
if (prop == '__target__') {
|
||||
return target;
|
||||
}
|
||||
if (target.hasOwnProperty(prop)) {
|
||||
const value = target[prop as string].value;
|
||||
if (typeof value === 'function') {
|
||||
return value.bind(proxy);
|
||||
}
|
||||
return value;
|
||||
} else if (target.prototype.hasOwnProperty(prop)) {
|
||||
const value = target.prototype[prop];
|
||||
if (typeof value === 'function') {
|
||||
return value.bind(proxy);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
set: (target, prop, value) => {
|
||||
return (target[prop as string].value = value);
|
||||
}
|
||||
});
|
||||
map(<any>insidesProxy);
|
||||
|
||||
return this.cloneFromInsides(<any>insidesProxy.__target__);
|
||||
}
|
||||
|
||||
protected cloneWithAdd({ propertyName, value }: { value: any; propertyName: string }): this {
|
||||
if (this[propertyName] === value) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const insides = Object.getOwnPropertyDescriptors(this);
|
||||
insides[propertyName].value = value;
|
||||
return this.cloneFromInsides(insides);
|
||||
}
|
||||
|
||||
protected cloneWithChangedKeys(props: { [propertyName: string]: any }): this {
|
||||
const insides = Object.getOwnPropertyDescriptors(this);
|
||||
|
||||
for (let key in props) {
|
||||
if (props.hasOwnProperty(key)) {
|
||||
if (insides.hasOwnProperty(key)) {
|
||||
insides[key].value = props[key];
|
||||
} else {
|
||||
// @ts-ignore
|
||||
insides[key] = {
|
||||
value: props[key]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.cloneFromInsides(insides);
|
||||
}
|
||||
|
||||
protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this {
|
||||
if (oldValue === newValue) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const insides = Object.getOwnPropertyDescriptors(this);
|
||||
|
||||
let wasMatch = false;
|
||||
for (let name in insides) {
|
||||
if (insides.hasOwnProperty(name) && insides[name].value === oldValue) {
|
||||
insides[name].value = newValue;
|
||||
wasMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasMatch) {
|
||||
throw new TypeError(`Object has no property with value: ${oldValue.toString()}`);
|
||||
}
|
||||
|
||||
return this.cloneFromInsides(insides);
|
||||
}
|
||||
|
||||
private cloneFromInsides(insides): this {
|
||||
insides.id.value = Node.id++;
|
||||
insides.copyCount.value++;
|
||||
Node.sumCopyCount++;
|
||||
|
||||
const clone = Object.create(Object.getPrototypeOf(this), insides);
|
||||
clone.onAfterClone();
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export abstract class Initiable {
|
||||
protected constructor() {
|
||||
this.initiate();
|
||||
}
|
||||
protected abstract initiate();
|
||||
}
|
||||
92
src/app/store/inner-node.ts
Normal file → Executable file
|
|
@ -1,9 +1,19 @@
|
|||
import { Node } from './node';
|
||||
import { Node, NodeState } from './node';
|
||||
|
||||
export abstract class InnerNode extends Node {
|
||||
readonly children: Array<InnerNode> = [];
|
||||
protected parent: Node;
|
||||
export interface InnerNodeState extends NodeState {
|
||||
dummy: any;
|
||||
}
|
||||
|
||||
export class InnerNode extends Node implements InnerNodeState {
|
||||
readonly dummy = 3;
|
||||
parent: Node;
|
||||
private nextVersion: this = null;
|
||||
readonly children: Array<InnerNode>;
|
||||
|
||||
protected constructor(children: Array<InnerNode> = [], id?: string) {
|
||||
super(children, id);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
get latestVersion(): this {
|
||||
let version;
|
||||
|
|
@ -13,49 +23,67 @@ export abstract class InnerNode extends Node {
|
|||
return version;
|
||||
}
|
||||
|
||||
mutatedUpdate() {
|
||||
this.parent.mutatedUpdate();
|
||||
addChildren(children: Array<InnerNode>): this {
|
||||
return super.addChildren.call(this.latestVersion, children);
|
||||
}
|
||||
|
||||
map(map: (a: this) => void) {
|
||||
return this.update((self: this) => this.cloneWithMap.call(self, map));
|
||||
replaceChild(update: { oldValue: InnerNode; newValue: InnerNode }): this {
|
||||
return super.replaceChild.call(this.latestVersion, update);
|
||||
}
|
||||
|
||||
changeKeys(props: { [propertyName: string]: any }): this {
|
||||
return this.update((self: this) => this.cloneWithChangedKeys.call(self, props));
|
||||
}
|
||||
|
||||
addChild(update: { child: InnerNode }) {
|
||||
super.addChild.call(this.latestVersion, update);
|
||||
}
|
||||
|
||||
changeChild(update: { oldValue: InnerNode; newValue: InnerNode }) {
|
||||
super.replaceChild.call(this.latestVersion, update);
|
||||
}
|
||||
|
||||
protected abstract cloneWithMap(map: (a: this) => void): this;
|
||||
protected abstract cloneWithChangedKeys(props: { [propertyName: string]: any }): this;
|
||||
|
||||
private update(cloneMethod: (self: this) => this): this {
|
||||
changeKeys<T extends NodeState>(props: Partial<T>): this {
|
||||
if (this.nextVersion !== null) {
|
||||
this.latestVersion.update(cloneMethod);
|
||||
this.latestVersion.changeKeys(props);
|
||||
}
|
||||
|
||||
const clone = cloneMethod(this);
|
||||
if (clone === this) {
|
||||
return this;
|
||||
let shouldClone = false;
|
||||
|
||||
for (const prop in props) {
|
||||
// @ts-ignore
|
||||
if (props.hasOwnProperty(prop) && props[prop] !== this[prop]) {
|
||||
shouldClone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!shouldClone) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let child of clone.children) {
|
||||
child.parent = clone;
|
||||
}
|
||||
const clone = this.cloneWithChangedKeys(props);
|
||||
|
||||
clone.children.forEach(c => (c.parent = clone));
|
||||
|
||||
this.nextVersion = clone;
|
||||
|
||||
this.parent.replaceChild({
|
||||
oldValue: this,
|
||||
newValue: clone
|
||||
});
|
||||
|
||||
this.nextVersion = clone;
|
||||
return clone;
|
||||
}
|
||||
|
||||
protected onAfterClone() {}
|
||||
|
||||
protected cloneWithChangedKeys<T extends NodeState>(props: Partial<T>): this {
|
||||
const insides = Object.getOwnPropertyDescriptors(this);
|
||||
|
||||
for (const key in props) {
|
||||
if (props.hasOwnProperty(key)) {
|
||||
if (insides.hasOwnProperty(key)) {
|
||||
insides[key].value = props[key];
|
||||
} else {
|
||||
// @ts-ignore
|
||||
insides[key] = {
|
||||
value: props[key]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clone = Object.create(Object.getPrototypeOf(this), insides);
|
||||
clone.setUniqueness();
|
||||
clone.onAfterClone();
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
src/app/store/node.ts
Normal file → Executable file
|
|
@ -1,46 +1,46 @@
|
|||
import { InnerNode } from './inner-node';
|
||||
import { Unique } from './unique';
|
||||
import { InnerNode } from './inner-node';
|
||||
|
||||
export abstract class Node extends Unique {
|
||||
readonly children: Array<InnerNode>;
|
||||
// TODO: fix types.
|
||||
protected abstract changeKeys(props: any): this;
|
||||
abstract mutatedUpdate(): void;
|
||||
export interface NodeState {
|
||||
children: Array<InnerNode>;
|
||||
}
|
||||
|
||||
private copyCount = 0;
|
||||
export abstract class Node extends Unique implements NodeState {
|
||||
abstract readonly children: Array<InnerNode>;
|
||||
|
||||
protected initiate() {
|
||||
super.initiate();
|
||||
this.copyCount++;
|
||||
protected constructor(children: Array<InnerNode> = [], id?: string) {
|
||||
super(id);
|
||||
children.forEach(c => (c.parent = this));
|
||||
}
|
||||
protected abstract changeKeys<T extends NodeState>(props: Partial<T>): this;
|
||||
|
||||
addChild({ child }: { child: InnerNode }) {
|
||||
this.changeKeys({
|
||||
children: [...this.children, child]
|
||||
addChildren(children: Array<InnerNode>): this {
|
||||
return this.changeKeys<NodeState>({
|
||||
children: [...this.children, ...children]
|
||||
});
|
||||
}
|
||||
|
||||
replaceChild({ oldValue, newValue }: { oldValue: InnerNode; newValue: InnerNode }) {
|
||||
replaceChild({ oldValue, newValue }: { oldValue: InnerNode; newValue: InnerNode }): this {
|
||||
if (oldValue === newValue) {
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.changeKeys({
|
||||
return this.changeKeys<NodeState>({
|
||||
children: this.children.map(c => (c === oldValue ? newValue : c))
|
||||
});
|
||||
}
|
||||
|
||||
protected _log(indent = ''): string {
|
||||
const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`;
|
||||
let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}siblings: ${this.copyCount}\n`;
|
||||
for (let c of this.children) {
|
||||
let response = `${basicInfo}${' '.repeat(70 - basicInfo.length)}copies: ${this.copies}\n`;
|
||||
for (const c of this.children) {
|
||||
response += `${c._log(indent + ' ')}`;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public log() {
|
||||
console.log(this._log());
|
||||
console.log(`All in all, there are ${Unique.ObjectCount} objects.`);
|
||||
// console.log(this._log());
|
||||
// console.log(`All in all, there are ${Unique.ObjectCount} objects.`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
src/app/store/root.ts
Normal file → Executable file
|
|
@ -1,11 +1,17 @@
|
|||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Node } from './node';
|
||||
import { Node, NodeState } from './node';
|
||||
import { InnerNode } from './inner-node';
|
||||
|
||||
export class Root<T extends InnerNode> extends Node {
|
||||
private readonly _children: BehaviorSubject<Array<T>> = new BehaviorSubject([]);
|
||||
readonly children$: Observable<Array<T>> = this._children.asObservable();
|
||||
readonly children$: Observable<Array<T>>;
|
||||
private readonly _children: BehaviorSubject<Array<T>>;
|
||||
|
||||
constructor(children: Array<T> = []) {
|
||||
super(children);
|
||||
this._children = new BehaviorSubject(children);
|
||||
this.children$ = this._children.asObservable();
|
||||
}
|
||||
|
||||
get children(): Array<T> {
|
||||
return this._children.getValue();
|
||||
|
|
@ -15,27 +21,14 @@ export class Root<T extends InnerNode> extends Node {
|
|||
this._children.next(value);
|
||||
}
|
||||
|
||||
mutatedUpdate() {
|
||||
// pass
|
||||
}
|
||||
|
||||
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) {
|
||||
if (this.children !== oldValue) {
|
||||
throw new TypeError('Only children can be changed.');
|
||||
}
|
||||
this.children = newValue;
|
||||
for (let child of this.children) {
|
||||
child.parent = this;
|
||||
}
|
||||
}
|
||||
|
||||
changeKey({ propertyName, value }: { propertyName: string; value: any }) {
|
||||
if (propertyName !== 'children') {
|
||||
throw new TypeError('Only children can be changed.');
|
||||
}
|
||||
this.children = value;
|
||||
for (let child of this.children) {
|
||||
child.parent = this;
|
||||
changeKeys<U extends NodeState>(props: Partial<U>): this {
|
||||
if (props.hasOwnProperty('children')) {
|
||||
// @ts-ignore
|
||||
this.children = props.children;
|
||||
for (const child of this.children) {
|
||||
child.parent = this;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
46
src/app/store/unique.ts
Normal file → Executable file
|
|
@ -1,17 +1,45 @@
|
|||
import { Initiable } from './initiable';
|
||||
import * as uuid from 'uuid';
|
||||
import { ISerializable } from '../interfaces/serializable';
|
||||
import { IUnique } from '../interfaces/persistance/unique';
|
||||
|
||||
export abstract class Unique extends Initiable {
|
||||
protected static nextId = 0;
|
||||
static get ObjectCount(): number {
|
||||
return Unique.nextId;
|
||||
export class Unique implements ISerializable, IUnique {
|
||||
private static count = 0;
|
||||
|
||||
constructor(id?: string) {
|
||||
if (id) {
|
||||
this._id = id;
|
||||
// console.log('got id ' + id);
|
||||
} else {
|
||||
this.setUniqueness();
|
||||
// console.log('unique ' + this.id);
|
||||
}
|
||||
}
|
||||
|
||||
get id(): number {
|
||||
static get ObjectCount(): number {
|
||||
return Unique.count;
|
||||
}
|
||||
|
||||
private _id: string;
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
private _id: number;
|
||||
|
||||
protected initiate() {
|
||||
this._id = Unique.nextId++;
|
||||
private _copies = 0;
|
||||
|
||||
get copies(): number {
|
||||
return this._copies;
|
||||
}
|
||||
|
||||
protected setUniqueness() {
|
||||
this._id = uuid.v4();
|
||||
Unique.count++;
|
||||
this._copies++;
|
||||
}
|
||||
|
||||
serialize(referenceSerializer: (ref: object) => any): object {
|
||||
return {
|
||||
id: this.id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
src/app/utils/color.ts
Normal file → Executable file
3
src/app/utils/hash.ts
Normal file → Executable file
|
|
@ -4,8 +4,7 @@ export const hash = (text: string): number => {
|
|||
return 0;
|
||||
}
|
||||
const hashValue = Array.prototype.reduce.call(
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
text,
|
||||
text, // tslint:disable-next-line:no-bitwise
|
||||
(value, char) => ((value << 5) - value + (char.charCodeAt(0) as number)) | 0,
|
||||
7
|
||||
);
|
||||
|
|
|
|||
2
src/app/utils/range.ts
Normal file → Executable file
|
|
@ -1,7 +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);
|
||||
for (let i = min; i < max; yield i, i += step) {}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
0
src/app/utils/top.ts
Normal file → Executable file
0
src/assets/.gitkeep
Normal file → Executable file
0
src/assets/arrow.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 746 B |
4
src/assets/pen.svg
Executable 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 528.899 528.899" style="enable-background:new 0 0 528.899 528.899;" xml:space="preserve"><g><g>
|
||||
<path d="M328.883,89.125l107.59,107.589l-272.34,272.34L56.604,361.465L328.883,89.125z M518.113,63.177l-47.981-47.981 c-18.543-18.543-48.653-18.543-67.259,0l-45.961,45.961l107.59,107.59l53.611-53.611 C532.495,100.753,532.495,77.559,518.113,63.177z M0.3,512.69c-1.958,8.812,5.998,16.708,14.811,14.565l119.891-29.069 L27.473,390.597L0.3,512.69z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#5D576B"/>
|
||||
</g></g> </svg>
|
||||
|
After Width: | Height: | Size: 737 B |
0
src/assets/plus-sign.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
src/assets/trash.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |