This commit is contained in:
Andras Schmelczer 2026-05-28 21:24:47 +01:00
commit f74ee43cb4
54 changed files with 16749 additions and 29973 deletions

View file

@ -0,0 +1,43 @@
import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core';
import { Block, HslColor } from '../../models';
import { getColorOfTag } from '../../utils/color';
/**
* A block rendered as a small COLORED SQUARE (1/6 of tower width).
* Only DONE blocks appear here; pending blocks appear in the tasks accordion.
* Clicking opens the block-edit modal in the parent tower component.
*/
@Component({
selector: 'lt-block',
standalone: true,
imports: [],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div [style.background-color]="color()" (click)="clicked.emit()"></div>
`,
styles: `
@import '../../../library/main';
:host {
position: relative;
width: calc(100% / 6);
padding-bottom: calc(100% / 6);
div {
position: absolute;
width: 100%;
height: 100%;
@include gravitate();
}
}
`,
})
export class BlockComponent {
readonly block = input.required<Block>();
readonly baseColor = input.required<HslColor>();
/** Emits when the square is clicked — parent opens the block-edit modal. */
readonly clicked = output<void>();
readonly color = computed(() => getColorOfTag(this.block().tag, this.baseColor()));
}

View file

@ -0,0 +1,521 @@
import {
Component,
ChangeDetectionStrategy,
input,
output,
inject,
signal,
computed,
effect,
viewChild,
ElementRef,
AfterViewInit,
HostListener,
untracked,
} from '@angular/core';
import { Block, HslColor } from '../../models';
import { SelectAddComponent } from '../shared/select-add/select-add.component';
import { ToggleComponent } from '../shared/toggle/toggle.component';
import { getColorOfTag } from '../../utils/color';
export interface BlockEditSave {
/** null = create a new block */
id: string | null;
tag: string;
description: string;
is_done: boolean;
}
interface EditedValue {
tag: string;
description: string;
is_done: boolean;
}
@Component({
selector: 'lt-block-edit',
standalone: true,
imports: [SelectAddComponent, ToggleComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section
#container
class="carousel"
(scroll)="onScroll()"
(click)="onBackdropClick($event)"
>
<div class="card placeholder"></div>
@for (b of blocks(); track b.id; let i = $index) {
<div
class="card"
[class.active]="activeIdx() === i + 1"
[class.near-active]="activeIdx() === i || activeIdx() === i + 2"
(click)="onCardClick(i + 1)"
>
<div class="mask"></div>
<div class="header">
<div class="exit" (click)="close.emit(); $event.stopPropagation()"></div>
<div
class="block-dot"
[style.background-color]="colorOfTagForBlock(b.id)"
></div>
<h1>{{ formatDate(b.created_at) }}</h1>
</div>
<div class="select-add-container">
<lt-select-add
[items]="tags()"
[selected]="editedFor(b.id).tag"
[alwaysDropShadow]="true"
[onlyShadowBorder]="true"
placeholder="Tag this item…"
(select)="updateTag(b.id, $event)"
(add)="updateTag(b.id, $event)"
/>
</div>
<textarea
placeholder="Write a description here…"
[value]="editedFor(b.id).description"
(input)="updateDescription(b.id, $any($event.target).value)"
(blur)="flushExisting(b.id)"
></textarea>
<div>
<lt-toggle
[checked]="editedFor(b.id).is_done"
(checkedChange)="updateDone(b.id, $event)"
offLabel="This task hasn't been finished yet"
onLabel="Goal already accomplished"
/>
</div>
<div class="bottom">
<button (click)="onDelete(b.id); $event.stopPropagation()">Delete</button>
</div>
</div>
}
<div
class="card create-card"
[class.active]="activeIdx() === blocks().length + 1"
[class.near-active]="activeIdx() === blocks().length"
(click)="onCardClick(blocks().length + 1)"
>
<div class="mask"></div>
<div class="header">
<div class="exit" (click)="close.emit(); $event.stopPropagation()"></div>
<div
class="block-dot"
[style.background-color]="colorOfNewTag()"
></div>
<h1>Create now</h1>
</div>
<div class="select-add-container">
<lt-select-add
[items]="tags()"
[selected]="newValue().tag"
[alwaysDropShadow]="true"
[onlyShadowBorder]="true"
placeholder="Set a category…"
(select)="updateNewTag($event)"
(add)="updateNewTag($event)"
/>
</div>
<textarea
placeholder="Write a description here…"
[value]="newValue().description"
(input)="updateNewDescription($any($event.target).value)"
></textarea>
<div>
<lt-toggle
[checked]="newValue().is_done"
(checkedChange)="updateNewDone($event)"
offLabel="This task hasn't been finished yet"
onLabel="Goal already accomplished"
/>
</div>
<div class="bottom">
<button
(click)="submitNew(); $event.stopPropagation()"
[disabled]="!newValue().tag"
>
Create and exit
</button>
</div>
</div>
<div class="card placeholder"></div>
</section>
`,
styles: `
@import '../../../library/main';
:host {
@include center-child();
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10001; // above modal backdrop (10000)
}
.carousel {
width: 100%;
height: 100%;
display: flex;
align-items: center;
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.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;
@include inner-spacing(var(--large-padding));
opacity: 0.6;
transition: opacity $long-animation-time;
&.near-active {
cursor: pointer;
opacity: 0.85;
}
&.active {
opacity: 1;
}
.mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10000;
@include card();
opacity: 1;
transition: opacity $long-animation-time;
pointer-events: none;
@media (max-width: $mobile-width) {
opacity: 0 !important;
}
}
&.active .mask {
opacity: 0;
}
&.near-active .mask {
opacity: 0.55;
}
&:first-child {
margin-left: var(--large-padding);
}
&.placeholder {
opacity: 0 !important;
width: 60vw;
max-width: 60vw;
box-shadow: none;
background: transparent;
}
.header {
@include center-child();
position: relative;
.exit {
position: absolute;
left: 0;
@include exit();
}
.block-dot {
@include square(12px);
margin-right: 10px;
border-radius: 2px;
}
}
.select-add-container {
// When the create card has no tag chosen, glow the dropdown red as a
// gentle "required" cue — matches the legacy ghost-button affordance.
&.required-empty lt-select-add {
box-shadow: 0 0 0 0.75px rgba(181, 63, 63, 0.5);
border-radius: var(--border-radius);
}
}
.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%);
}
}
}
`,
})
export class BlockEditComponent implements AfterViewInit {
readonly blocks = input.required<Block[]>();
readonly activeBlockId = input<string | null>(null);
readonly tags = input<string[]>([]);
readonly baseColor = input.required<HslColor>();
/** Default for `is_done` on the create card. */
readonly defaultDone = input<boolean>(true);
readonly save = output<BlockEditSave>();
readonly delete = output<string>();
readonly close = output<void>();
private readonly container =
viewChild<ElementRef<HTMLElement>>('container');
// Per-block edited values, keyed by block ID.
readonly editedValues = signal<Map<string, EditedValue>>(new Map());
// Pending new block being authored on the create card.
readonly newValue = signal<EditedValue>({
tag: '',
description: '',
is_done: true,
});
// 1-based index of the centered card. 0/N+2 are placeholders.
readonly activeIdx = signal(1);
private scrollToken = 0;
constructor() {
// Seed editedValues from input blocks (and re-seed if input changes).
effect(() => {
const bs = this.blocks();
const m = new Map<string, EditedValue>();
for (const b of bs) {
m.set(b.id, {
tag: b.tag,
description: b.description,
is_done: b.is_done,
});
}
untracked(() => this.editedValues.set(m));
});
// Seed the newValue tag from tags input on first run.
effect(() => {
const t = this.tags();
untracked(() => {
const cur = this.newValue();
if (!cur.tag && t.length > 0) {
this.newValue.set({ ...cur, tag: t[0], is_done: this.defaultDone() });
} else if (!cur.tag) {
this.newValue.set({ ...cur, is_done: this.defaultDone() });
}
});
});
}
ngAfterViewInit(): void {
// Position scroll on the focused card (or the create card if none).
queueMicrotask(() => {
const blocks = this.blocks();
const focusId = this.activeBlockId();
const focusIdx = focusId
? Math.max(0, blocks.findIndex((b) => b.id === focusId))
: blocks.length;
this.scrollToChild(focusIdx + 1, false);
});
}
// ── Helpers ────────────────────────────────────────────────────────────────
editedFor(id: string): EditedValue {
return (
this.editedValues().get(id) ?? {
tag: '',
description: '',
is_done: false,
}
);
}
colorOfTagForBlock(id: string): string {
const v = this.editedFor(id);
return v.tag ? getColorOfTag(v.tag, this.baseColor()) : 'transparent';
}
colorOfNewTag = computed(() => {
const t = this.newValue().tag;
return t ? getColorOfTag(t, this.baseColor()) : 'transparent';
});
formatDate(ts: number): string {
const d = new Date(ts * 1000);
return d.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
// ── Existing-card mutations (auto-save on each change) ─────────────────────
updateTag(id: string, tag: string): void {
this.patchEdited(id, { tag });
this.flushExisting(id);
}
updateDescription(id: string, description: string): void {
this.patchEdited(id, { description });
// Description flush deferred to (blur) to avoid PUT per keystroke.
}
updateDone(id: string, is_done: boolean): void {
this.patchEdited(id, { is_done });
this.flushExisting(id);
}
private patchEdited(id: string, patch: Partial<EditedValue>): void {
this.editedValues.update((m) => {
const v = m.get(id);
if (!v) return m;
const next = new Map(m);
next.set(id, { ...v, ...patch });
return next;
});
}
flushExisting(id: string): void {
const v = this.editedFor(id);
if (!v.tag) return; // Skip empty saves
this.save.emit({ id, tag: v.tag, description: v.description, is_done: v.is_done });
}
onDelete(id: string): void {
this.delete.emit(id);
}
// ── Create-card mutations ──────────────────────────────────────────────────
updateNewTag(tag: string): void {
this.newValue.update((v) => ({ ...v, tag }));
}
updateNewDescription(description: string): void {
this.newValue.update((v) => ({ ...v, description }));
}
updateNewDone(is_done: boolean): void {
this.newValue.update((v) => ({ ...v, is_done }));
}
submitNew(): void {
const v = this.newValue();
if (!v.tag) return;
this.save.emit({
id: null,
tag: v.tag,
description: v.description,
is_done: v.is_done,
});
this.close.emit();
}
// ── Scroll handling ────────────────────────────────────────────────────────
onCardClick(idx: number): void {
if (idx !== this.activeIdx()) {
this.scrollToChild(idx, true);
}
}
/** Close the carousel when the user clicks anywhere that isn't a real card. */
onBackdropClick(event: MouseEvent): void {
const target = event.target as HTMLElement | null;
if (target && !target.closest('.card:not(.placeholder)')) {
this.close.emit();
}
}
onScroll(): void {
const token = ++this.scrollToken;
setTimeout(() => {
if (token === this.scrollToken) this.adjustPosition();
}, 150);
}
@HostListener('window:resize')
onResize(): void {
this.scrollToChild(this.activeIdx(), false);
}
private scrollToChild(idx: number, smooth: boolean): void {
const container = this.container()?.nativeElement;
if (!container) return;
const card = container.children.item(idx) as HTMLElement | null;
if (!card) return;
const left =
card.offsetLeft - (container.clientWidth - card.offsetWidth) / 2;
container.scrollTo({ left, behavior: smooth ? 'smooth' : 'auto' });
this.activeIdx.set(idx);
}
private adjustPosition(): void {
const container = this.container()?.nativeElement;
if (!container) return;
const center = container.scrollLeft + container.clientWidth / 2;
let nearestIdx = 1;
let minDist = Infinity;
// children[0] and children[last] are the placeholders — skip.
for (let i = 1; i < container.children.length - 1; i++) {
const child = container.children.item(i) as HTMLElement;
const c = child.offsetLeft + child.offsetWidth / 2;
const d = Math.abs(c - center);
if (d < minDist) {
minDist = d;
nearestIdx = i;
}
}
if (nearestIdx !== this.activeIdx()) {
this.scrollToChild(nearestIdx, true);
}
}
}

View file

@ -1,11 +0,0 @@
<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>

View file

@ -1,29 +0,0 @@
@import '../../../styles';
section {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
@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);
}
}

View file

@ -1,25 +1,94 @@
import { Component } from '@angular/core';
import { ModalService, ModalType } from '../../services/modal.service';
import { CancelService } from '../../services/cancel.service';
import {
Component,
ChangeDetectionStrategy,
output,
signal,
AfterViewInit,
OnDestroy,
viewChild,
ElementRef,
inject,
} from '@angular/core';
import { A11yModule } from '@angular/cdk/a11y';
import { ModalStateService } from '../../services/modal-state.service';
@Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.scss']
})
export class ModalComponent {
ModalType = ModalType;
selector: 'lt-modal',
standalone: true,
imports: [A11yModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section
class="modal"
[class.active]="active()"
(click)="onBackdropClick($event)"
>
<div class="modal__dialog" #dialog cdkTrapFocus cdkTrapFocusAutoCapture (keydown.escape)="onClose()">
<ng-content></ng-content>
</div>
</section>
`,
styles: `
@import '../../../library/main';
save: () => void = null;
section.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
@include center-child();
padding: var(--large-padding);
box-sizing: border-box;
background: $background-gradient;
transition: opacity 300ms;
opacity: 1;
constructor(public modalService: ModalService, private cancelService: CancelService) {
this.cancelService.subscribe(this, () => {
if (this.save) {
this.save();
this.save = null;
} else {
this.modalService.cancel();
&:not(.active) {
opacity: 0;
pointer-events: none;
}
});
button {
margin-top: var(--medium-padding);
}
}
`,
})
export class ModalComponent implements AfterViewInit, OnDestroy {
readonly close = output<void>();
// The active signal starts false; AfterViewInit flips it true on next tick
// so the 300ms opacity transition fires on entry (0 → 1).
readonly active = signal(false);
private readonly dialogRef = viewChild<ElementRef<HTMLElement>>('dialog');
private previousFocus: HTMLElement | null = null;
private escListener!: (e: KeyboardEvent) => void;
private readonly modalState = inject(ModalStateService);
ngAfterViewInit(): void {
this.previousFocus = document.activeElement as HTMLElement;
// Track open state so towers can be locked while any modal is mounted.
this.modalState.open();
// Defer one tick so the opacity transition runs (0 → 1).
setTimeout(() => this.active.set(true), 0);
}
ngOnDestroy(): void {
this.modalState.close();
this.previousFocus?.focus();
}
onBackdropClick(event: MouseEvent): void {
const dialog = this.dialogRef()?.nativeElement;
if (dialog && !dialog.contains(event.target as Node)) {
this.onClose();
}
}
onClose(): void {
this.close.emit();
}
}

View file

@ -1,85 +0,0 @@
<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>

View file

@ -1,175 +0,0 @@
@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;
}

View file

@ -1,183 +0,0 @@
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);
}
}

View file

@ -1,3 +0,0 @@
<p>
get-started works!
</p>

View file

@ -1,12 +0,0 @@
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() {}
}

View file

@ -1,13 +0,0 @@
<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>

View file

@ -1,30 +0,0 @@
@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();
}
}
}

View file

@ -1,11 +0,0 @@
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) {}
}

View file

@ -1,14 +0,0 @@
<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: tower.baseColor | color }">{{ tower.name ? tower.name : 'an unnamed tower' }}</span
>.
</p>
<button (click)="modalService.submit()">Remove</button>
</section>

View file

@ -1,30 +0,0 @@
@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();
}
}
}

View file

@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
import { Tower } from '../../../../model/tower';
@Component({
selector: 'app-remove-tower',
templateUrl: './remove-tower.component.html',
styleUrls: ['./remove-tower.component.scss']
})
export class RemoveTowerComponent {
constructor(public modalService: ModalService) {}
tower: Tower = this.modalService.active.input;
}

View file

@ -1,21 +0,0 @@
<div class="header">
<div class="exit" (click)="modalService.cancel()"></div>
<h1>Settings</h1>
</div>
<div>
<app-toggle
[beforeText]="'Hide create tower button'"
[afterText]="'Show create tower button'"
[default]="!page.userData.hideCreateTowerButton"
(value)="page.setHideCreateTowerButton(!$event)"
></app-toggle>
</div>
<p *ngIf="page.towers.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
<input id="token" type="text" [(ngModel)]="token" />
<button (click)="setNewToken()">Set token</button>
<button (click)="$event.stopPropagation() || deletePage()">Delete current page</button>

View file

@ -1,42 +0,0 @@
@import '../../../../../styles';
:host {
@include card();
width: 66vw;
max-width: 400px;
@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);
}
input[type='text'] {
text-align: center;
}
button {
display: block;
}
}

View file

@ -1,56 +0,0 @@
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, OnDestroy {
data: Data;
page: Page;
private dataSubscription: Subscription;
private pageSubscription: Subscription;
token: string;
constructor(public modalService: ModalService, private store: MapStoreService) {
this.token = store.userToken;
}
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.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();
}
}
}

Some files were not shown because too many files have changed in this diff Show more