This commit is contained in:
schmelczerandras 2019-08-25 15:49:51 +02:00
parent e96ac49c8e
commit e992152a75
28 changed files with 289 additions and 79 deletions

View file

@ -7,8 +7,8 @@
<!-- wrapper for easier styling --> <!-- wrapper for easier styling -->
<app-select-add <app-select-add
class="select" class="select"
[options]="modalService.active.input" [options]="modalService.active.input.options"
[default]="modalService.active.input[0]" [default]="modalService.active.input.options[0]"
[alwaysDropShadow]="true" [alwaysDropShadow]="true"
[onlyShadowBorder]="true" [onlyShadowBorder]="true"
[placeholder]="'Tag this item…'" [placeholder]="'Tag this item…'"
@ -24,10 +24,10 @@
<app-toggle <app-toggle
[beforeText]="'This task hasn\'t been finished yet'" [beforeText]="'This task hasn\'t been finished yet'"
[afterText]="'Goal already accomplished'" [afterText]="'Goal already accomplished'"
[default]="false" [default]="!modalService.active.input.isTask"
(value)="isDone = $event" (value)="isDone = $event"
></app-toggle> ></app-toggle>
</div> </div>
<!-- wrapper for easier styling --> <!-- wrapper for easier styling -->
<button (click)="submit()">Create</button> <button (click)="submit()" [disabled]="!selected">Create</button>

View file

@ -14,6 +14,10 @@ export class CreateBlockComponent {
constructor(public modalService: ModalService) {} constructor(public modalService: ModalService) {}
submit() { submit() {
if (!this.selected) {
return;
}
this.modalService.submit({ this.modalService.submit({
selected: this.selected, selected: this.selected,
description: this.description, description: this.description,

View file

@ -16,7 +16,7 @@
</section> </section>
<img <img
*ngIf="isDragging" [ngClass]="isDragging ? 'active' : ''"
src="assets/trash.svg" src="assets/trash.svg"
alt="trashcan" alt="trashcan"
(pointerenter)="trashEnter()" (pointerenter)="trashEnter()"
@ -24,9 +24,9 @@
(pointerup)="removeTower()" (pointerup)="removeTower()"
/> />
<div class="double-slider-container"> <div class="double-slider-container" [ngStyle]="{ opacity: isDragging ? '0' : '1' }">
<app-double-slider <app-double-slider
*ngIf="!isDragging && dates.length >= MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER" *ngIf="dates.length >= MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER"
[values]="dates" [values]="dates"
[labels]="dateLabels" [labels]="dateLabels"
(lowerBound)="startDate = $event" (lowerBound)="startDate = $event"

View file

@ -17,6 +17,8 @@
justify-content: center; justify-content: center;
width: 100%; width: 100%;
margin-left: auto;
margin-right: auto;
flex: 1 0 auto; flex: 1 0 auto;
@ -28,7 +30,33 @@
} }
} }
@include inner-spacing(var(--medium-padding), $horizontal: true); max-width: 800px;
& > * {
max-width: 200px;
box-sizing: content-box;
flex: 0 0 auto;
&:not(:nth-last-child(1)) {
margin-right: var(--medium-padding);
@media (max-width: $mobile-width) {
margin-right: var(--small-padding);
}
}
}
position: relative;
@for $i from 1 to 6 {
& > *:first-child:nth-last-child(#{$i}),
& > *:first-child:nth-last-child(#{$i}) ~ * {
width: calc((100% - (#{$i} - 1) * var(--medium-padding)) / #{$i});
@media (max-width: $mobile-width) {
width: calc((100% - (#{$i} - 1) * var(--small-padding)) / #{$i});
}
}
}
} }
.double-slider-container { .double-slider-container {
@ -38,16 +66,25 @@
} }
img { img {
@include square(47.33333px); @include square(48px);
padding: 47.3333px 0; padding: 16px;
margin: auto;
position: relative; position: absolute;
z-index: 1500; z-index: 1500;
bottom: 8px;
left: 50%;
margin: 0 !important;
transform: translateX(-50%) scale(0);
transition: transform $long-animation-time; transition: transform $long-animation-time;
&.active {
transform: translateX(-50%) scale(1);
}
&:hover { &:hover {
transform: scale(1.2); transform: translateX(-50%) scale(1.1);
} }
} }
} }

View file

@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Page } from '../../../model/page'; import { Page } from '../../../model/page';
import { ModalService } from '../../../services/modal.service'; import { ModalService } from '../../../services/modal.service';
import { DataService } from '../../../services/data.service'; import { DataService } from '../../../services/data.service';
@ -20,6 +20,8 @@ export class PageComponent {
this.updateDates(); this.updateDates();
} }
@Output() isDragHappening: EventEmitter<boolean> = new EventEmitter();
get page(): Page { get page(): Page {
return this._page; return this._page;
} }
@ -59,11 +61,13 @@ export class PageComponent {
dropDrag(event: any) { dropDrag(event: any) {
this.page.moveTower(event); this.page.moveTower(event);
this.isDragging = false; this.isDragging = false;
this.isDragHappening.emit(false);
} }
startDrag(id: number) { startDrag(id: number) {
this.draggedTowerIndex = id; this.draggedTowerIndex = id;
this.isDragging = true; this.isDragging = true;
this.isDragHappening.emit(true);
} }
trashEnter() { trashEnter() {

View file

@ -1,16 +1,14 @@
<div class="container"> <div class="container {{ tasks.length > 0 ? 'show-hover' : '' }}">
<p class="header" (click)="isOpen = !isOpen"> <p class="header" (click)="isOpen = !isOpen">
{{ tasks.length == 0 ? 'no' : '' }}
<strong> <strong>
{{ tasks.length == 0 ? '' : tasks.length }} {{ tasks.length == 0 ? '' : tasks.length }}
</strong> </strong>
{{ tasks.length == 1 ? 'task' : 'tasks' }} {{ tasks.length == 0 ? '&#8203;' : tasks.length == 1 ? 'task' : 'tasks' }}
</p> </p>
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }"> <div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
<p <div class="task-container" *ngFor="let task of tasks" [ngStyle]="{ color: task.color.toString() }">
*ngFor="let task of tasks" <div [ngStyle]="{ 'background-color': task.color.toString() }"></div>
(click)="handleClick(task)" <p (click)="handleClick(task)" [innerText]="task.description ? task.description : 'unknown'"></p>
[innerText]="task.description ? task.description : 'unknown'" </div>
></p>
</div> </div>
</div> </div>

View file

@ -3,12 +3,20 @@
:host { :host {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
position: relative;
z-index: 100000;
.container { .container {
@include card(); @include card();
box-shadow: $shadow-border;
padding: var(--small-padding); cursor: pointer;
margin: var(--small-padding); transition: box-shadow $long-animation-time;
&.show-hover:hover {
box-shadow: $shadow-border;
}
padding: calc(var(--small-padding) / 2);
margin: calc(var(--small-padding) / 2);
max-height: 30vh; max-height: 30vh;
overflow-y: auto; overflow-y: auto;
@ -18,7 +26,6 @@
} }
p { p {
width: 100%;
font-size: var(--medium-font-size); font-size: var(--medium-font-size);
} }
@ -29,23 +36,43 @@
margin-top: var(--small-padding); margin-top: var(--small-padding);
} }
width: 100%; height: 0;
box-sizing: border-box; box-sizing: border-box;
transition: height $long-animation-time;
overflow-y: hidden; overflow-y: hidden;
height: 0; .task-container {
transition: height $long-animation-time; display: flex;
align-items: center;
p {
max-width: 60px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
text-align: left;
&:hover { &:hover {
font-weight: bold; p {
@media (min-width: $mobile-width) {
color: inherit !important;
}
}
}
div {
flex: 0 0 auto;
margin: 0 calc(var(--small-padding) / 2) 0 0;
@include square(var(--small-padding));
@media (max-width: $mobile-width) {
@include square(calc(var(--small-padding) / 2));
}
}
p {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
text-align: left;
@media (max-width: $mobile-width) {
font-size: var(--small-font-size);
}
position: relative;
} }
} }
} }

View file

@ -2,6 +2,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Block } from '../../../../../model/block'; import { Block } from '../../../../../model/block';
import { Tower } from '../../../../../model/tower'; import { Tower } from '../../../../../model/tower';
import { ModalService } from '../../../../../services/modal.service'; import { ModalService } from '../../../../../services/modal.service';
import { CancelService } from '../../../../../services/cancel.service';
@Component({ @Component({
selector: 'app-tasks', selector: 'app-tasks',
@ -12,11 +13,25 @@ export class TasksComponent implements OnInit {
@Input() tasks: Block[]; @Input() tasks: Block[];
@Input() tower: Tower; @Input() tower: Tower;
@Input() isOpen = false; private _isOpen = false;
@Input() set isOpen(value: boolean) {
if (value) {
this.cancelService.cancelAllExcept(this);
}
this._isOpen = value;
}
get isOpen(): boolean {
return this._isOpen;
}
@ViewChild('allTask') allTask: ElementRef; @ViewChild('allTask') allTask: ElementRef;
constructor(private modalService: ModalService) {} constructor(private modalService: ModalService, private cancelService: CancelService) {
this.cancelService.subscribe(this, () => {
this.isOpen = false;
});
}
ngOnInit() {} ngOnInit() {}
@ -39,4 +54,16 @@ export class TasksComponent implements OnInit {
// pass // 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
}
}
} }

View file

@ -15,7 +15,7 @@
<input <input
id="tower-name" id="tower-name"
type="text" type="text"
placeholder="…" placeholder="name…"
[(ngModel)]="tower.name" [(ngModel)]="tower.name"
[ngStyle]="{ color: tower.baseColor.toString() }" [ngStyle]="{ color: tower.baseColor.toString() }"
/> />

View file

@ -11,8 +11,17 @@
opacity: 0; opacity: 0;
} }
&.cdk-drag-preview,
&:hover { &:hover {
@media (min-width: $mobile-width) {
div {
.container {
box-shadow: $shadow;
}
}
}
}
&.cdk-drag-preview {
div { div {
.container { .container {
box-shadow: $shadow; box-shadow: $shadow;
@ -39,7 +48,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%; max-width: 100%;
height: 100%; height: 100%;
@include inner-spacing(var(--small-padding)); @include inner-spacing(var(--small-padding));
@ -52,7 +61,7 @@
position: relative; position: relative;
@include card(); @include card();
overflow: hidden;
transition: transform $short-animation-time, box-shadow $long-animation-time; transition: transform $short-animation-time, box-shadow $long-animation-time;
@include inner-spacing(var(--medium-padding)); @include inner-spacing(var(--medium-padding));
@ -65,7 +74,7 @@
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
z-index: 3; z-index: 2;
left: 0; left: 0;
top: 0; top: 0;
@ -85,9 +94,9 @@
position: relative; position: relative;
z-index: 2; z-index: 2;
height: 56px; height: 48px;
@media (max-width: $mobile-width) { @media (max-width: $mobile-width) {
height: 42px; height: 32px;
} }
opacity: 0.33; opacity: 0.33;
@ -130,7 +139,7 @@
} }
input[type='text'] { input[type='text'] {
font-size: var(--medium-font-size); font-size: var(--small-font-size);
text-align: center; text-align: center;
@media (min-width: $mobile-width) { @media (min-width: $mobile-width) {
width: 50%; width: 50%;

View file

@ -1,4 +1,4 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Tower } from '../../../../model/tower'; import { Tower } from '../../../../model/tower';
import { ModalService } from '../../../../services/modal.service'; import { ModalService } from '../../../../services/modal.service';
import { Block } from '../../../../model/block'; import { Block } from '../../../../model/block';
@ -40,7 +40,10 @@ export class TowerComponent {
public async addBlock() { public async addBlock() {
try { try {
const { selected: tag, description, isDone } = await this.modalService.showCreateBlock(this.tower.tags); const { selected: tag, description, isDone } = await this.modalService.showCreateBlock({
options: this.tower.tags,
isTask: false
});
this.tower.addBlock({ tag, description, isDone }); this.tower.addBlock({ tag, description, isDone });
} catch (e) { } catch (e) {
// pass // pass

View file

@ -11,8 +11,12 @@
<div class="page-container"> <div class="page-container">
<!-- wrapper for easier styling --> <!-- wrapper for easier styling -->
<app-page *ngIf="dataService.active !== null" [page]="dataService.active"></app-page> <app-page
*ngIf="dataService.active !== null"
[page]="dataService.active"
(isDragHappening)="isDragHappening = $event"
></app-page>
</div> </div>
<!-- wrapper for easier styling --> <!-- wrapper for easier styling -->
<button (click)="openSettings()">Settings</button> <button [ngClass]="isDragHappening ? 'transparent' : ''" (click)="openSettings()">Settings</button>

View file

@ -18,4 +18,12 @@
.page-container { .page-container {
flex: 1 0 auto; flex: 1 0 auto;
} }
button {
transition: opacity $long-animation-time;
&.transparent {
opacity: 0;
}
}
} }

View file

@ -13,6 +13,8 @@ export class PagesComponent {
@ViewChild('page') page: ElementRef; @ViewChild('page') page: ElementRef;
@ViewChild('bottom') bottom: ElementRef; @ViewChild('bottom') bottom: ElementRef;
isDragHappening = false;
constructor(public dataService: DataService, private modalService: ModalService) {} constructor(public dataService: DataService, private modalService: ModalService) {}
async selectPage(selected: string) { async selectPage(selected: string) {

View file

@ -37,11 +37,12 @@ $slider-size: 40px;
transition: box-shadow $long-animation-time, transform $long-animation-time; transition: box-shadow $long-animation-time, transform $long-animation-time;
&:hover { @media (min-width: $mobile-width) {
box-shadow: $shadow; &:hover {
transform: translateY(-$slider-size / 2 + $line-height / 2) scale(1.1); box-shadow: $shadow;
transform: translateY(-$slider-size / 2 + $line-height / 2) scale(1.1);
}
} }
cursor: pointer; cursor: pointer;
position: relative; position: relative;

View file

@ -16,6 +16,12 @@ export class DoubleSliderComponent {
this._values = values; this._values = values;
this.calculateLabels(); this.calculateLabels();
if (this._oneValue > this._otherValue) {
this._oneValue = this.MAX - 1;
} else {
this._otherValue = this.MAX - 1;
}
this.emitValue(); this.emitValue();
} }

View file

@ -98,8 +98,10 @@ $inner-padding: var(--medium-padding);
} }
&:hover { &:hover {
.background { @media (min-width: $mobile-width) {
box-shadow: $shadow; .background {
box-shadow: $shadow;
}
} }
} }

View file

@ -1,4 +1,5 @@
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { CancelService } from '../../../services/cancel.service';
@Component({ @Component({
selector: 'app-select-add', selector: 'app-select-add',
@ -28,6 +29,12 @@ export class SelectAddComponent {
newOption: string; newOption: string;
isOpen = false; isOpen = false;
constructor(private cancelService: CancelService) {
this.cancelService.subscribe(this, () => {
this.isOpen = false;
});
}
get otherOptions(): string[] { get otherOptions(): string[] {
return this.options.filter(a => a !== this.selected); return this.options.filter(a => a !== this.selected);
} }

View file

@ -4,6 +4,11 @@ import { Color } from './color';
export class Block extends Base implements IBlock { export class Block extends Base implements IBlock {
constructor(props: IBlock) { constructor(props: IBlock) {
// TODO: remove
if (props.isDone === undefined) {
props.isDone = true;
}
super(props); super(props);
if (this.created.constructor.name !== 'Date') { if (this.created.constructor.name !== 'Date') {

View file

@ -5,6 +5,11 @@ import { ITower } from '../interfaces/persistance/tower';
export class Page extends Base implements IPage { export class Page extends Base implements IPage {
constructor(props) { constructor(props) {
// TODO: remove
if (!props.userData) {
props.userData = {};
}
super(props); super(props);
// @ts-ignore to prevent update message // @ts-ignore to prevent update message
this.__towers = this.towers.map(t => this.createTower(t)); this.__towers = this.towers.map(t => this.createTower(t));
@ -37,7 +42,6 @@ export class Page extends Base implements IPage {
do { do {
hue = Math.random() * 360; hue = Math.random() * 360;
} while (30 <= hue && hue <= 200); } while (30 <= hue && hue <= 200);
console.log(hue);
this.towers.push( this.towers.push(
this.createTower({ this.createTower({

View file

@ -3,7 +3,7 @@ import { Color } from './color';
import { Block } from './block'; import { Block } from './block';
import { Base } from './base'; import { Base } from './base';
import { IBlock } from '../interfaces/persistance/block'; import { IBlock } from '../interfaces/persistance/block';
import { hashCode } from '../utils/hash'; import { hash } from '../utils/hash';
export class Tower extends Base implements ITower { export class Tower extends Base implements ITower {
constructor(props: ITower) { constructor(props: ITower) {
@ -46,7 +46,7 @@ export class Tower extends Base implements ITower {
if (!this.tags.includes(block.tag)) { if (!this.tags.includes(block.tag)) {
this.tags.push(block.tag); this.tags.push(block.tag);
} }
block.color = this.baseColor.lighten(hashCode(block.tag) * 50); block.color = this.baseColor.lighten(hash(block.tag) * 50);
} }
super.update(); super.update();

View file

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
interface Subscriber {
callback: () => void;
object: object;
}
@Injectable({
providedIn: 'root'
})
export class CancelService {
private subscribers: Subscriber[] = [];
constructor() {}
subscribe(object: object, callback: () => void) {
this.subscribers.push({
object,
callback
});
}
cancelAllExcept(except: object) {
this.subscribers.filter(s => s.object !== except).map(s => s.callback());
}
cancelAll() {
this.subscribers.map(s => s.callback());
}
}

View file

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Tower } from '../model/tower'; import { Tower } from '../model/tower';
import { top } from '../utils/top'; import { top } from '../utils/top';
import { CancelService } from './cancel.service';
export enum ModalType { export enum ModalType {
createBlock, createBlock,
@ -25,8 +26,13 @@ interface Modal {
export class ModalService { export class ModalService {
private modalStack: Modal[] = []; private modalStack: Modal[] = [];
showCreateBlock(options: string[]): Promise<{ selected: string; description: string; isDone: boolean }> { constructor(private cancelService: CancelService) {}
return this.createPromiseAndPushToStack(options, ModalType.createBlock);
showCreateBlock(input: {
options: string[];
isTask: boolean;
}): Promise<{ selected: string; description: string; isDone: boolean }> {
return this.createPromiseAndPushToStack(input, ModalType.createBlock);
} }
showEditBlock(data: { showEditBlock(data: {
@ -65,6 +71,8 @@ export class ModalService {
} }
private createPromiseAndPushToStack(input: any, type: ModalType): Promise<any> { private createPromiseAndPushToStack(input: any, type: ModalType): Promise<any> {
this.cancelService.cancelAll();
const modal = { const modal = {
input, input,
type, type,

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Page } from '../model/page'; import { Page } from '../model/page';
const LOCAL_STORAGE_KEY = 'life-towers.data.v.2.1'; const LOCAL_STORAGE_KEY = 'life-towers.data.v.2';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View file

@ -1,14 +1,8 @@
export const hashCode = (text: string) => { export const hash = (text: string): number => {
let hash = 0; // Return number between 0 and 1.
if (text.length == 0) { if (!text) {
return hash; return 0;
} }
for (let i = 0; i < text.length; i++) { const hash = Array.prototype.reduce.call(text, (hash, char) => (hash << 5) - hash + char.charCodeAt(0), 7);
const char = text.charCodeAt(i); return hash / (Math.pow(2, 32) - 1);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
hash /= Math.pow(2, 32) - 1;
return hash;
}; };

View file

@ -1,5 +1,5 @@
$accent-color: #a2666f; $accent-color: #a2666f;
$text-color: #5d576b; $text-color: #5d576bff;
$light-color: #ffffff; $light-color: #ffffff;
$background-gradient: linear-gradient(90deg, #fff9e077 0, #ffd6d677 100%); $background-gradient: linear-gradient(90deg, #fff9e077 0, #ffd6d677 100%);

View file

@ -43,10 +43,35 @@ button {
background: transparent; background: transparent;
border: 0; border: 0;
text-decoration: underline;
@include medium-text(); @include medium-text();
@include jump(); font-size: var(--large-font-size);
$height: 2px;
cursor: pointer;
border-bottom: solid $height #5d576b55;
position: relative;
&:disabled {
color: #5d576b55;
border-bottom: solid $height #5d576b33;
cursor: not-allowed;
}
&:not(:disabled):hover {
&:after {
width: 100%;
}
}
&:after {
content: '';
width: 0;
height: $height;
position: absolute;
left: 0;
bottom: calc(-1 * #{$height});
background-color: $text-color;
transition: width 300ms;
}
} }
label { label {

View file

@ -60,3 +60,8 @@ img {
border-radius: var(--border-radius); border-radius: var(--border-radius);
cursor: pointer; cursor: pointer;
} }
* {
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}