Reformat code
This commit is contained in:
parent
6e27539eca
commit
420cd788c4
94 changed files with 10592 additions and 2608 deletions
9
src/app/components/modal/modal.component.html
Normal file
9
src/app/components/modal/modal.component.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<main class="{{ modalService.active ? 'active' : '' }}" [ngSwitch]="modalService.active?.type">
|
||||
<app-create-block *ngSwitchCase="ModalType.createBlock"></app-create-block>
|
||||
<app-edit-block *ngSwitchCase="ModalType.editBlock"></app-edit-block>
|
||||
<app-remove-block *ngSwitchCase="ModalType.removeBlock"></app-remove-block>
|
||||
<app-remove-tower *ngSwitchCase="ModalType.removeTower"></app-remove-tower>
|
||||
<app-settings *ngSwitchCase="ModalType.settings"></app-settings>
|
||||
<app-get-started *ngSwitchCase="ModalType.getStarted"></app-get-started>
|
||||
<app-remove-page *ngSwitchCase="ModalType.removePage"></app-remove-page>
|
||||
</main>
|
||||
29
src/app/components/modal/modal.component.scss
Normal file
29
src/app/components/modal/modal.component.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../styles';
|
||||
|
||||
main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include center-child();
|
||||
|
||||
padding: var(--large-padding);
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
background: $background-gradient;
|
||||
transition: opacity 300ms;
|
||||
|
||||
&:not(.active) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: var(--medium-padding);
|
||||
}
|
||||
}
|
||||
20
src/app/components/modal/modal.component.ts
Normal file
20
src/app/components/modal/modal.component.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService, ModalType } from '../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal',
|
||||
templateUrl: './modal.component.html',
|
||||
styleUrls: ['./modal.component.scss']
|
||||
})
|
||||
export class ModalComponent {
|
||||
// Needed for accessing the enum from html.
|
||||
ModalType = ModalType;
|
||||
|
||||
constructor(public modalService: ModalService) {
|
||||
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
this.modalService.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Create an item</h1>
|
||||
</div>
|
||||
|
||||
<div class="select-add-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-select-add
|
||||
class="select"
|
||||
[options]="modalService.active.input"
|
||||
[default]="modalService.active.input[0]"
|
||||
[alwaysDropShadow]="true"
|
||||
[onlyShadowBorder]="true"
|
||||
[placeholder]="'Tag this item…'"
|
||||
(value)="selected = $event"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<textarea placeholder="Write a description here…" [(ngModel)]="description"></textarea>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="false"
|
||||
(value)="isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="submit()">Create</button>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include card();
|
||||
box-shadow: $shadow;
|
||||
|
||||
width: 66vw;
|
||||
max-width: 400px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
position: relative;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-block',
|
||||
templateUrl: './create-block.component.html',
|
||||
styleUrls: ['./create-block.component.scss']
|
||||
})
|
||||
export class CreateBlockComponent {
|
||||
selected: string;
|
||||
description: string = null;
|
||||
isDone: boolean;
|
||||
|
||||
constructor(public modalService: ModalService) {}
|
||||
|
||||
submit() {
|
||||
this.modalService.submit({
|
||||
selected: this.selected,
|
||||
description: this.description,
|
||||
isDone: this.isDone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>View item</h1>
|
||||
</div>
|
||||
|
||||
<div class="select-add-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-select-add
|
||||
class="select"
|
||||
[options]="modalService.active.input.options"
|
||||
[default]="modalService.active.input.options[0]"
|
||||
[alwaysDropShadow]="true"
|
||||
[onlyShadowBorder]="true"
|
||||
[placeholder]="'Tag this item…'"
|
||||
(value)="selected = $event"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<textarea placeholder="Write a description here…" [(ngModel)]="modalService.active.input.description"></textarea>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'This task hasn\'t been finished yet'"
|
||||
[afterText]="'Goal already accomplished'"
|
||||
[default]="modalService.active.input.isDone"
|
||||
(value)="modalService.active.input.isDone = $event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="submit()">Modify</button>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include card();
|
||||
box-shadow: $shadow;
|
||||
|
||||
width: 66vw;
|
||||
max-width: 400px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
position: relative;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-block',
|
||||
templateUrl: './edit-block.component.html',
|
||||
styleUrls: ['./edit-block.component.scss']
|
||||
})
|
||||
export class EditBlockComponent {
|
||||
selected: string;
|
||||
|
||||
constructor(public modalService: ModalService) {}
|
||||
|
||||
submit() {
|
||||
this.modalService.submit({
|
||||
selected: this.selected,
|
||||
description: this.modalService.active.input.description,
|
||||
isDone: this.modalService.active.input.isDone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
get-started works!
|
||||
</p>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-get-started',
|
||||
templateUrl: './get-started.component.html',
|
||||
styleUrls: ['./get-started.component.scss']
|
||||
})
|
||||
export class GetStartedComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
remove-block works!
|
||||
</p>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-block',
|
||||
templateUrl: './remove-block.component.html',
|
||||
styleUrls: ['./remove-block.component.scss']
|
||||
})
|
||||
export class RemoveBlockComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<section>
|
||||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Are you sure?</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You are trying to remove <strong>{{ this.modalService.active.input }}</strong
|
||||
>.
|
||||
</p>
|
||||
|
||||
<button (click)="modalService.submit()">Remove</button>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
section {
|
||||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
|
||||
position: relative;
|
||||
box-shadow: $shadow;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-page',
|
||||
templateUrl: './remove-page.component.html',
|
||||
styleUrls: ['./remove-page.component.scss']
|
||||
})
|
||||
export class RemovePageComponent {
|
||||
constructor(public modalService: ModalService) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<section>
|
||||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Are you sure?</h1>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You are trying to remove
|
||||
<span [ngStyle]="{ color: modalService.active.input.baseColor.toString() }">{{
|
||||
modalService.active.input.name ? modalService.active.input.name : 'an unnamed tower'
|
||||
}}</span
|
||||
>.
|
||||
</p>
|
||||
|
||||
<button (click)="modalService.submit()">Remove</button>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
section {
|
||||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
|
||||
position: relative;
|
||||
box-shadow: $shadow;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-tower',
|
||||
templateUrl: './remove-tower.component.html',
|
||||
styleUrls: ['./remove-tower.component.scss']
|
||||
})
|
||||
export class RemoveTowerComponent {
|
||||
constructor(public modalService: ModalService) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<div class="header">
|
||||
<div class="exit" (click)="modalService.cancel()"></div>
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-toggle
|
||||
[beforeText]="'Hide create tower button'"
|
||||
[afterText]="'Show create tower button'"
|
||||
[default]="!dataService.active.userData?.hideCreateTowerButton"
|
||||
(value)="dataService.active.userData.hideCreateTowerButton = !$event"
|
||||
></app-toggle>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<p *ngIf="dataService.active?.towers.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
|
||||
|
||||
<button (click)="deletePage()">Delete current page</button>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
@include card();
|
||||
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
|
||||
position: relative;
|
||||
box-shadow: $shadow;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.header {
|
||||
@include center-child();
|
||||
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--medium-font-size);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { DataService } from '../../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
export class SettingsComponent {
|
||||
constructor(public modalService: ModalService, public dataService: DataService) {}
|
||||
|
||||
async deletePage() {
|
||||
try {
|
||||
await this.modalService.showRemovePage(this.dataService.active.name);
|
||||
this.dataService.remove();
|
||||
this.modalService.submit();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/app/components/pages/page/page.component.html
Normal file
35
src/app/components/pages/page/page.component.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<button
|
||||
*ngIf="page && page.towers.length < 5 && !dataService.active?.userData?.hideCreateTowerButton"
|
||||
(click)="createTower()"
|
||||
>
|
||||
Create tower
|
||||
</button>
|
||||
|
||||
<section class="towers" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropDrag($event)">
|
||||
<app-tower
|
||||
*ngFor="let tower of page?.towers"
|
||||
[tower]="tower"
|
||||
[dateRange]="dateRange"
|
||||
cdkDrag
|
||||
(cdkDragStarted)="startDrag(page.towers.indexOf(tower))"
|
||||
></app-tower>
|
||||
</section>
|
||||
|
||||
<img
|
||||
*ngIf="isDragging"
|
||||
src="assets/trash.svg"
|
||||
alt="trashcan"
|
||||
(pointerenter)="trashEnter()"
|
||||
(pointerleave)="trashExit()"
|
||||
(pointerup)="removeTower()"
|
||||
/>
|
||||
|
||||
<div class="double-slider-container">
|
||||
<app-double-slider
|
||||
*ngIf="!isDragging && dates.length >= MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER"
|
||||
[values]="dates"
|
||||
[labels]="dateLabels"
|
||||
(lowerBound)="startDate = $event"
|
||||
(upperBound)="endDate = $event"
|
||||
></app-double-slider>
|
||||
</div>
|
||||
53
src/app/components/pages/page/page.component.scss
Normal file
53
src/app/components/pages/page/page.component.scss
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.towers {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
flex: 1 0 auto;
|
||||
|
||||
transition: box-shadow $short-animation-time;
|
||||
|
||||
&.cdk-drop-list-dragging {
|
||||
*:not(.cdk-drag-placeholder) {
|
||||
transition: transform $long-animation-time cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@include inner-spacing(var(--medium-padding), $horizontal: true);
|
||||
}
|
||||
|
||||
.double-slider-container {
|
||||
@media (max-height: $min-height) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include square(47.33333px);
|
||||
padding: 47.3333px 0;
|
||||
margin: auto;
|
||||
|
||||
position: relative;
|
||||
z-index: 1500;
|
||||
|
||||
transition: transform $long-animation-time;
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/app/components/pages/page/page.component.ts
Normal file
98
src/app/components/pages/page/page.component.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Page } from '../../../model/page';
|
||||
import { ModalService } from '../../../services/modal.service';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page',
|
||||
templateUrl: './page.component.html',
|
||||
styleUrls: ['./page.component.scss']
|
||||
})
|
||||
export class PageComponent {
|
||||
private _page: Page;
|
||||
@Input() set page(value: Page) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._page = value;
|
||||
value.subscribe(() => this.updateDates());
|
||||
this.updateDates();
|
||||
}
|
||||
|
||||
get page(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
readonly MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER = 3;
|
||||
|
||||
isDragging = false;
|
||||
draggedTowerIndex: number;
|
||||
nearTrashcan = false;
|
||||
|
||||
dates: Date[] = [];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
|
||||
get dateLabels(): string[] {
|
||||
return this.dates.map(d => d.toLocaleDateString());
|
||||
}
|
||||
|
||||
get dateRange(): { from: Date; to: Date } {
|
||||
return this.dates.length >= this.MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER
|
||||
? {
|
||||
from: this.startDate,
|
||||
to: this.endDate
|
||||
}
|
||||
: {
|
||||
from: new Date(0, 0),
|
||||
to: new Date(10000, 0)
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private modalService: ModalService, public dataService: DataService) {}
|
||||
|
||||
createTower() {
|
||||
this.page.addTower();
|
||||
}
|
||||
|
||||
dropDrag(event: any) {
|
||||
this.page.moveTower(event);
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
startDrag(id: number) {
|
||||
this.draggedTowerIndex = id;
|
||||
this.isDragging = true;
|
||||
}
|
||||
|
||||
trashEnter() {
|
||||
this.nearTrashcan = true;
|
||||
window.document.querySelector('.cdk-drag-preview').className += ' trash-highlight';
|
||||
}
|
||||
|
||||
trashExit() {
|
||||
this.nearTrashcan = false;
|
||||
const elem = window.document.querySelector('.cdk-drag-preview');
|
||||
elem.className = elem.className
|
||||
.split(' ')
|
||||
.slice(0, -1)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
async removeTower() {
|
||||
try {
|
||||
const tower = this.page.towers[this.draggedTowerIndex];
|
||||
await this.modalService.showRemoveTower(tower);
|
||||
this.page.removeTower(tower);
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
private updateDates() {
|
||||
this.dates = this.page.towers
|
||||
.reduce((all, t) => [...t.blocks.map(b => b.created), ...all], [])
|
||||
.sort((d1, d2) => d1.getTime() - d2.getTime());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<div [ngStyle]="{ 'background-color': block.color }" (click)="handleClick()"></div>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
@import '../../../../../../styles';
|
||||
|
||||
:host {
|
||||
position: relative;
|
||||
width: calc(100% / 6);
|
||||
padding-bottom: calc(100% / 6);
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include gravitate();
|
||||
}
|
||||
}
|
||||
32
src/app/components/pages/page/tower/block/block.component.ts
Normal file
32
src/app/components/pages/page/tower/block/block.component.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Block } from '../../../../../model/block';
|
||||
import { ModalService } from '../../../../../services/modal.service';
|
||||
import { Tower } from '../../../../../model/tower';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
templateUrl: './block.component.html',
|
||||
styleUrls: ['./block.component.scss']
|
||||
})
|
||||
export class BlockComponent {
|
||||
@Input() block: Block;
|
||||
@Input() tower: Tower;
|
||||
|
||||
constructor(private modalService: ModalService) {}
|
||||
|
||||
async handleClick() {
|
||||
try {
|
||||
const { selected, description, isDone } = await this.modalService.showEditBlock({
|
||||
options: this.tower.tags,
|
||||
default: this.block.tag,
|
||||
description: this.block.description,
|
||||
isDone: this.block.isDone
|
||||
});
|
||||
this.block.tag = selected;
|
||||
this.block.description = description;
|
||||
this.block.isDone = isDone;
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="container">
|
||||
<p class="header" (click)="isOpen = !isOpen">
|
||||
{{ tasks.length == 0 ? 'no' : '' }}
|
||||
<strong>
|
||||
{{ tasks.length == 0 ? '' : tasks.length }}
|
||||
</strong>
|
||||
{{ tasks.length == 1 ? 'task' : 'tasks' }}
|
||||
</p>
|
||||
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
|
||||
<p
|
||||
*ngFor="let task of tasks"
|
||||
(click)="handleClick(task)"
|
||||
[innerText]="task.description ? task.description : 'unknown'"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
@import '../../../../../../styles';
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.container {
|
||||
@include card();
|
||||
box-shadow: $shadow-border;
|
||||
padding: var(--small-padding);
|
||||
margin: var(--small-padding);
|
||||
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
font-size: var(--medium-font-size);
|
||||
}
|
||||
|
||||
.all-task {
|
||||
@include inner-spacing(var(--small-padding));
|
||||
|
||||
:first-child {
|
||||
margin-top: var(--small-padding);
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow-y: hidden;
|
||||
|
||||
height: 0;
|
||||
transition: height $long-animation-time;
|
||||
|
||||
p {
|
||||
max-width: 60px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/app/components/pages/page/tower/tasks/tasks.component.ts
Normal file
41
src/app/components/pages/page/tower/tasks/tasks.component.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { Block } from '../../../../../model/block';
|
||||
import { Tower } from '../../../../../model/tower';
|
||||
import { ModalService } from '../../../../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks',
|
||||
templateUrl: './tasks.component.html',
|
||||
styleUrls: ['./tasks.component.scss']
|
||||
})
|
||||
export class TasksComponent implements OnInit {
|
||||
@Input() tasks: Block[];
|
||||
@Input() tower: Tower;
|
||||
|
||||
@Input() isOpen = false;
|
||||
|
||||
@ViewChild('allTask') allTask: ElementRef;
|
||||
|
||||
constructor(private modalService: ModalService) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
async handleClick(block: Block) {
|
||||
try {
|
||||
const { selected, description, isDone } = await this.modalService.showEditBlock({
|
||||
options: this.tower.tags,
|
||||
default: block.tag,
|
||||
description: block.description,
|
||||
isDone: block.isDone
|
||||
});
|
||||
block.tag = selected;
|
||||
block.description = description;
|
||||
if (!block.isDone && isDone) {
|
||||
block.created = new Date();
|
||||
}
|
||||
block.isDone = isDone;
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/app/components/pages/page/tower/tower.component.html
Normal file
22
src/app/components/pages/page/tower/tower.component.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<div class="tower">
|
||||
<div class="container">
|
||||
<div class="tasks-container">
|
||||
<app-tasks [tasks]="tasks" [tower]="tower"></app-tasks>
|
||||
</div>
|
||||
|
||||
<img src="assets/plus-sign.svg" alt="add item" (click)="addBlock()" />
|
||||
|
||||
<div class="block-container {{ isFalling ? 'falling' : '' }}">
|
||||
<app-block *ngFor="let block of drawableBlocks" [block]="block" [tower]="tower"></app-block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="tower-name" class="hidden">Card name</label>
|
||||
<input
|
||||
id="tower-name"
|
||||
type="text"
|
||||
placeholder="…"
|
||||
[(ngModel)]="tower.name"
|
||||
[ngStyle]="{ color: tower.baseColor.toString() }"
|
||||
/>
|
||||
</div>
|
||||
140
src/app/components/pages/page/tower/tower.component.scss
Normal file
140
src/app/components/pages/page/tower/tower.component.scss
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
@import '../../../../../styles';
|
||||
|
||||
:host {
|
||||
cursor: pointer;
|
||||
|
||||
&.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.cdk-drag-preview,
|
||||
&:hover {
|
||||
div {
|
||||
.container {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.trash-highlight {
|
||||
.container {
|
||||
transform: scale(0.75);
|
||||
position: relative;
|
||||
|
||||
:before {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tower {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@include inner-spacing(var(--small-padding));
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
|
||||
@include card();
|
||||
|
||||
transition: transform $short-animation-time, box-shadow $long-animation-time;
|
||||
|
||||
@include inner-spacing(var(--medium-padding));
|
||||
|
||||
width: 100%;
|
||||
|
||||
:before {
|
||||
content: '';
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: red;
|
||||
|
||||
opacity: 0;
|
||||
border-radius: var(--border-radius);
|
||||
transition: opacity $short-animation-time;
|
||||
}
|
||||
|
||||
.tasks-container {
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
height: 56px;
|
||||
@media (max-width: $mobile-width) {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
opacity: 0.33;
|
||||
transition: opacity $long-animation-time;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-end;
|
||||
transform: scaleY(-1);
|
||||
flex: 1;
|
||||
|
||||
&.falling > *:last-child {
|
||||
animation: falling 1.5s cubic-bezier(0.5, 0, 1, 0) forwards;
|
||||
|
||||
@keyframes falling {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(500%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
font-size: var(--medium-font-size);
|
||||
text-align: center;
|
||||
@media (min-width: $mobile-width) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/app/components/pages/page/tower/tower.component.ts
Normal file
49
src/app/components/pages/page/tower/tower.component.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||
import { Tower } from '../../../../model/tower';
|
||||
import { ModalService } from '../../../../services/modal.service';
|
||||
import { Block } from '../../../../model/block';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tower',
|
||||
templateUrl: './tower.component.html',
|
||||
styleUrls: ['./tower.component.scss']
|
||||
})
|
||||
export class TowerComponent {
|
||||
@Input() set dateRange(value: { from: Date; to: Date }) {
|
||||
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
|
||||
return;
|
||||
}
|
||||
this._dateRange = value;
|
||||
}
|
||||
|
||||
get dateRange(): { from: Date; to: Date } {
|
||||
return this._dateRange;
|
||||
}
|
||||
|
||||
public constructor(private modalService: ModalService) {}
|
||||
|
||||
get drawableBlocks(): Block[] {
|
||||
return this.tower.blocks.filter(
|
||||
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
|
||||
);
|
||||
}
|
||||
|
||||
get tasks(): Block[] {
|
||||
return this.tower.blocks.filter(block => !block.isDone);
|
||||
}
|
||||
|
||||
@Input() tower: Tower;
|
||||
|
||||
_dateRange: { from: Date; to: Date };
|
||||
|
||||
isFalling = true;
|
||||
|
||||
public async addBlock() {
|
||||
try {
|
||||
const { selected: tag, description, isDone } = await this.modalService.showCreateBlock(this.tower.tags);
|
||||
this.tower.addBlock({ tag, description, isDone });
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/app/components/pages/pages.component.html
Normal file
18
src/app/components/pages/pages.component.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<div class="select-add-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-select-add
|
||||
[options]="dataService.pageNames"
|
||||
[default]="dataService.active?.name"
|
||||
(value)="selectPage($event)"
|
||||
[placeholder]="'Add a new page…'"
|
||||
></app-select-add>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<div class="page-container">
|
||||
<!-- wrapper for easier styling -->
|
||||
<app-page *ngIf="dataService.active !== null" [page]="dataService.active"></app-page>
|
||||
</div>
|
||||
<!-- wrapper for easier styling -->
|
||||
|
||||
<button (click)="openSettings()">Settings</button>
|
||||
21
src/app/components/pages/pages.component.scss
Normal file
21
src/app/components/pages/pages.component.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@import '../../../styles';
|
||||
|
||||
:host {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
||||
.select-add-container {
|
||||
width: 250px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
40
src/app/components/pages/pages.component.ts
Normal file
40
src/app/components/pages/pages.component.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Page } from '../../model/page';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { ModalService } from '../../services/modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pages',
|
||||
templateUrl: './pages.component.html',
|
||||
styleUrls: ['./pages.component.scss']
|
||||
})
|
||||
export class PagesComponent {
|
||||
@ViewChild('top') top: ElementRef;
|
||||
@ViewChild('page') page: ElementRef;
|
||||
@ViewChild('bottom') bottom: ElementRef;
|
||||
|
||||
constructor(public dataService: DataService, private modalService: ModalService) {}
|
||||
|
||||
async selectPage(selected: string) {
|
||||
if (!this.dataService.pageNames.includes(selected)) {
|
||||
const page = new Page({
|
||||
name: selected,
|
||||
towers: [],
|
||||
userData: {}
|
||||
});
|
||||
|
||||
this.dataService.push(page);
|
||||
page.addTower();
|
||||
}
|
||||
|
||||
await this.dataService.changeActiveByName(selected);
|
||||
}
|
||||
|
||||
async openSettings() {
|
||||
try {
|
||||
await this.modalService.showSettings();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<div class="container">
|
||||
<label for="date-selector-1">date selector 1</label>
|
||||
<label for="date-selector-2">date selector 2</label>
|
||||
<input id="date-selector-1" type="range" min="0" [max]="MAX - 1" [(ngModel)]="oneValue" />
|
||||
<input id="date-selector-2" type="range" min="0" [max]="MAX - 1" [(ngModel)]="otherValue" />
|
||||
<div class="value-container">
|
||||
<span
|
||||
*ngFor="let i of drawnLabelsIndices"
|
||||
[innerHTML]="drawnLabels[i]"
|
||||
[ngStyle]="{ transform: getOffset(i) }"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
$height: 70px;
|
||||
$width: 300px;
|
||||
$slider-size: 40px;
|
||||
|
||||
.container {
|
||||
width: $width;
|
||||
height: $height;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin: $slider-size / 2 auto 0 auto;
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
-webkit-appearance: none;
|
||||
outline: none;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
|
||||
height: $slider-size;
|
||||
width: $slider-size;
|
||||
border-radius: 1000px;
|
||||
|
||||
background-color: $light-color;
|
||||
transform-origin: center center;
|
||||
transform: translateY(-$slider-size / 2 + $line-height / 2);
|
||||
|
||||
transition: box-shadow $long-animation-time, transform $long-animation-time;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow;
|
||||
transform: translateY(-$slider-size / 2 + $line-height / 2) scale(1.1);
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
-webkit-appearance: none;
|
||||
|
||||
width: 100%;
|
||||
height: $line-height;
|
||||
background-color: $text-color;
|
||||
border-radius: 1000px;
|
||||
}
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.value-container {
|
||||
@include small-text();
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { range } from '../../../utils/range';
|
||||
|
||||
@Component({
|
||||
selector: 'app-double-slider',
|
||||
templateUrl: './double-slider.component.html',
|
||||
styleUrls: ['./double-slider.component.scss']
|
||||
})
|
||||
export class DoubleSliderComponent {
|
||||
@Input() labels: string[];
|
||||
|
||||
@Input() set values(values: any[]) {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._values = values;
|
||||
this.calculateLabels();
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
get values(): any[] {
|
||||
return this._values;
|
||||
}
|
||||
|
||||
get oneValue(): number {
|
||||
return this._oneValue;
|
||||
}
|
||||
|
||||
set oneValue(value: number) {
|
||||
this._oneValue = value;
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
get otherValue(): number {
|
||||
return this._otherValue;
|
||||
}
|
||||
|
||||
set otherValue(value: number) {
|
||||
this._otherValue = value;
|
||||
this.emitValue();
|
||||
}
|
||||
|
||||
private _values: any[];
|
||||
|
||||
@Output() lowerBound: EventEmitter<any> = new EventEmitter();
|
||||
@Output() upperBound: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
drawnLabels: string[];
|
||||
|
||||
readonly MAX = 100;
|
||||
|
||||
private _oneValue = 0;
|
||||
|
||||
private _otherValue: number = this.MAX - 1;
|
||||
|
||||
drawnLabelsIndices: Iterable<number>;
|
||||
|
||||
private calculateLabels() {
|
||||
const labelCount = 6;
|
||||
const jumpLength = Math.round(this.labels.length / labelCount);
|
||||
this.drawnLabels = this.labels.filter((_, index) => index % jumpLength === 0);
|
||||
this.drawnLabelsIndices = range({ max: this.drawnLabels.length });
|
||||
}
|
||||
|
||||
private indexFromValue(value: number): number {
|
||||
return Math.floor((value / this.MAX) * this.values.length);
|
||||
}
|
||||
|
||||
getOffset(index: number): string {
|
||||
const labelIndex = index / this.drawnLabels.length;
|
||||
const slider1Index = this.oneValue / this.MAX - 0.1;
|
||||
const slider2Index = this.otherValue / this.MAX - 0.1;
|
||||
|
||||
const dist = (a, b) => Math.abs(a - b);
|
||||
|
||||
const labelSliderDistance = Math.min(dist(labelIndex, slider1Index), dist(labelIndex, slider2Index));
|
||||
|
||||
const ACTIVE_ZONE = 0.2;
|
||||
const BASE_TRANSFORM = 'translateX(-50%) rotate(-45deg) translateY(100%)';
|
||||
|
||||
if (labelSliderDistance > ACTIVE_ZONE) {
|
||||
return BASE_TRANSFORM;
|
||||
}
|
||||
|
||||
return `translateY(${Math.pow((ACTIVE_ZONE - labelSliderDistance) / ACTIVE_ZONE, 1) * 30}px) ${BASE_TRANSFORM}`;
|
||||
}
|
||||
|
||||
private emitValue() {
|
||||
if (this.oneValue < this.otherValue) {
|
||||
this.lowerBound.emit(this.values[this.indexFromValue(this.oneValue)]);
|
||||
this.upperBound.emit(this.values[this.indexFromValue(this.otherValue)]);
|
||||
} else {
|
||||
this.lowerBound.emit(this.values[this.indexFromValue(this.otherValue)]);
|
||||
this.upperBound.emit(this.values[this.indexFromValue(this.oneValue)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<div class="select-add {{ onlyShadowBorder ? 'shadow-border' : '' }}">
|
||||
<div #top class="top" (click)="toggle()">
|
||||
<p [innerHTML]="selected ? selected : placeholder"></p>
|
||||
<img src="assets/arrow.svg" [className]="isOpen ? 'upside-down' : ''" alt="arrow" />
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<div #bottom class="bottom {{ isOpen ? 'open' : '' }}">
|
||||
<p *ngFor="let option of otherOptions" [innerHTML]="option" (click)="select(option)"></p>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
*ngIf="options.length <= maxItemCount"
|
||||
placeholder="Add a value…"
|
||||
[(ngModel)]="newOption"
|
||||
(keyup)="handleKeys($event)"
|
||||
/>
|
||||
|
||||
<button *ngIf="options.length <= maxItemCount" (click)="addNewOption()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="background {{ isOpen || alwaysDropShadow ? 'active' : '' }}"
|
||||
[ngStyle]="{ height: backgroundHeight }"
|
||||
></div>
|
||||
</div>
|
||||
117
src/app/components/shared/select-add/select-add.component.scss
Normal file
117
src/app/components/shared/select-add/select-add.component.scss
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
$inner-padding: var(--medium-padding);
|
||||
.select-add {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.top,
|
||||
.bottom {
|
||||
padding: $inner-padding;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
@include sub-title-text();
|
||||
}
|
||||
|
||||
img {
|
||||
@include square(16px);
|
||||
transition: transform $long-animation-time;
|
||||
|
||||
&.upside-down {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
|
||||
position: absolute;
|
||||
overflow-y: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
pointer-events: all;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
|
||||
padding-top: 0;
|
||||
|
||||
@include inner-spacing($inner-padding);
|
||||
|
||||
transform: translateY(-100%);
|
||||
visibility: hidden;
|
||||
|
||||
&.open {
|
||||
visibility: visible;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
transition: transform $long-animation-time;
|
||||
|
||||
p {
|
||||
@include sub-title-text();
|
||||
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@include card();
|
||||
|
||||
z-index: 3;
|
||||
|
||||
transition: box-shadow $long-animation-time, height $long-animation-time;
|
||||
|
||||
&.active {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.background {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow-border {
|
||||
.background.active {
|
||||
box-shadow: $shadow-border;
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow-border:hover {
|
||||
.background {
|
||||
box-shadow: $shadow-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/app/components/shared/select-add/select-add.component.ts
Normal file
66
src/app/components/shared/select-add/select-add.component.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-select-add',
|
||||
templateUrl: './select-add.component.html',
|
||||
styleUrls: ['./select-add.component.scss']
|
||||
})
|
||||
export class SelectAddComponent {
|
||||
@Input() placeholder = 'Add a new value…';
|
||||
@Input() maxItemCount = 7;
|
||||
@Input() options: string[];
|
||||
@Input() alwaysDropShadow = false;
|
||||
@Input() onlyShadowBorder = false;
|
||||
|
||||
@Input() set default(value: string) {
|
||||
this.selected = value;
|
||||
if (value) {
|
||||
this.value.emit(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Output() value: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
@ViewChild('top') top: ElementRef;
|
||||
@ViewChild('bottom') bottom: ElementRef;
|
||||
|
||||
selected: string;
|
||||
newOption: string;
|
||||
isOpen = false;
|
||||
|
||||
get otherOptions(): string[] {
|
||||
return this.options.filter(a => a !== this.selected);
|
||||
}
|
||||
|
||||
handleKeys(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
this.addNewOption();
|
||||
}
|
||||
}
|
||||
|
||||
get backgroundHeight(): string {
|
||||
if (this.isOpen && this.top && this.bottom) {
|
||||
const topHeight = this.top.nativeElement.clientHeight;
|
||||
const bottomHeight = this.bottom.nativeElement.clientHeight;
|
||||
return `${topHeight + bottomHeight}px`;
|
||||
}
|
||||
return `100%`;
|
||||
}
|
||||
|
||||
addNewOption() {
|
||||
if (this.newOption) {
|
||||
this.select(this.newOption);
|
||||
this.newOption = '';
|
||||
}
|
||||
}
|
||||
|
||||
select(option: string) {
|
||||
this.selected = option;
|
||||
this.value.emit(this.selected);
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
}
|
||||
5
src/app/components/shared/toggle/toggle.component.html
Normal file
5
src/app/components/shared/toggle/toggle.component.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<span [className]="!on ? 'active' : ''" (click)="on = false" [innerText]="beforeText"></span>
|
||||
|
||||
<input type="checkbox" [(ngModel)]="on" [className]="on ? 'on' : ''" />
|
||||
|
||||
<span [className]="on ? 'active' : ''" (click)="on = true" [innerText]="afterText"></span>
|
||||
66
src/app/components/shared/toggle/toggle.component.scss
Normal file
66
src/app/components/shared/toggle/toggle.component.scss
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
@import '../../../../styles';
|
||||
|
||||
:host {
|
||||
$size: 30px;
|
||||
|
||||
@include center-child();
|
||||
@include inner-spacing(var(--medium-padding), $horizontal: true);
|
||||
|
||||
span {
|
||||
@include medium-text();
|
||||
max-width: 3 * $size;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
|
||||
width: 2 * $size;
|
||||
height: $size;
|
||||
|
||||
border-radius: 1000px;
|
||||
box-shadow: $shadow-border;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
|
||||
@include square($size);
|
||||
|
||||
border-radius: 1000px;
|
||||
background-color: $text-color;
|
||||
|
||||
transition: box-shadow $long-animation-time, left $long-animation-time, transform $long-animation-time;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
box-shadow: $shadow;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
&.on:hover:after {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
&.on:after {
|
||||
left: $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/app/components/shared/toggle/toggle.component.ts
Normal file
27
src/app/components/shared/toggle/toggle.component.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle',
|
||||
templateUrl: './toggle.component.html',
|
||||
styleUrls: ['./toggle.component.scss']
|
||||
})
|
||||
export class ToggleComponent {
|
||||
@Input() beforeText: string;
|
||||
@Input() afterText: string;
|
||||
|
||||
@Output() value: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
@Input() set default(value: boolean) {
|
||||
this.on = value;
|
||||
}
|
||||
|
||||
private _on = false;
|
||||
set on(value: boolean) {
|
||||
this._on = value;
|
||||
this.value.emit(value);
|
||||
}
|
||||
|
||||
get on(): boolean {
|
||||
return this._on;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue