Merge pull request #11 from farcasclaudiu/feature/angular19-migration

Migrate Angular 2 to Angular 19
This commit is contained in:
2026-04-16 23:28:41 +03:00
committed by GitHub
47 changed files with 18689 additions and 1023 deletions
+5 -1
View File
@@ -1,4 +1,4 @@
# Editor configuration, see http://editorconfig.org
# Editor configuration, see https://editorconfig.org
root = true
[*]
@@ -8,6 +8,10 @@ indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+24 -14
View File
@@ -1,35 +1,45 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# compiled output
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# dependencies
# Node
/node_modules
/bower_components
npm-debug.log
yarn-error.log
# IDEs and editors
/.idea
/.vscode
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# misc
/.sass-cache
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage/*
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# Firebase config (contains secrets)
src/environments/firebaseConfig.ts
#System Files
# System files
.DS_Store
Thumbs.db
+4
View File
@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}
+20
View File
@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}
+42
View File
@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}
-60
View File
@@ -1,60 +0,0 @@
{
"project": {
"version": "1.0.0-beta.21",
"name": "kanban2"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"test": "test.ts",
"tsconfig": "tsconfig.json",
"prefix": "app",
"mobile": false,
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [],
"environments": {
"source": "environments/environment.ts",
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"addons": [],
"packages": [],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"prefixInterfaces": false,
"inline": {
"style": false,
"template": false
},
"spec": {
"class": false,
"component": true,
"directive": true,
"module": false,
"pipe": true,
"service": true
}
}
}
+135
View File
@@ -0,0 +1,135 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"kanban2": {
"projectType": "application",
"schematics": {
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:component": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/kanban2",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
},
"src/favicon.ico"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1MB",
"maximumError": "1.5MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "kanban2:build:production"
},
"development": {
"buildTarget": "kanban2:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}
-14
View File
@@ -1,14 +0,0 @@
import { ValuationJsPage } from './app.po';
describe('kanban2 App', function() {
let page: ValuationJsPage;
beforeEach(() => {
page = new ValuationJsPage();
});
it('should display message saying app works', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('app works!');
});
});
-11
View File
@@ -1,11 +0,0 @@
import { browser, element, by } from 'protractor';
export class ValuationJsPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}
-16
View File
@@ -1,16 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "../dist/out-tsc-e2e",
"sourceMap": true,
"target": "es5",
"typeRoots": [
"../node_modules/@types"
]
}
}
+44
View File
@@ -0,0 +1,44 @@
// @ts-check
const eslint = require("@eslint/js");
const { defineConfig } = require("eslint/config");
const tseslint = require("typescript-eslint");
const angular = require("angular-eslint");
module.exports = defineConfig([
{
files: ["**/*.ts"],
extends: [
eslint.configs.recommended,
tseslint.configs.recommended,
tseslint.configs.stylistic,
angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
"@angular-eslint/directive-selector": [
"error",
{
type: "attribute",
prefix: "app",
style: "camelCase",
},
],
"@angular-eslint/component-selector": [
"error",
{
type: "element",
prefix: "app",
style: "kebab-case",
},
],
},
},
{
files: ["**/*.html"],
extends: [
angular.configs.templateRecommended,
angular.configs.templateAccessibility,
],
rules: {},
}
]);
-43
View File
@@ -1,43 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', 'angular-cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-remap-istanbul'),
require('angular-cli/plugins/karma')
],
files: [
{ pattern: './src/test.ts', watched: false }
],
preprocessors: {
'./src/test.ts': ['angular-cli']
},
mime: {
'text/x-typescript': ['ts','tsx']
},
remapIstanbulReporter: {
reports: {
html: 'coverage',
lcovonly: './coverage/coverage.lcov'
}
},
angularCli: {
config: './angular-cli.json',
environment: 'dev'
},
reporters: config.angularCli && config.angularCli.codeCoverage
? ['progress', 'karma-remap-istanbul']
: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
+17299
View File
File diff suppressed because it is too large Load Diff
+35 -39
View File
@@ -1,51 +1,47 @@
{
"name": "kanban2",
"version": "0.0.0",
"license": "MIT",
"angular-cli": {},
"scripts": {
"ng": "ng",
"start": "ng serve",
"lint": "tslint \"src/**/*.ts\"",
"test": "ng test",
"pree2e": "webdriver-manager update",
"e2e": "protractor"
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/common": "19.2.16",
"@angular/compiler": "2.4.7",
"@angular/core": "10.2.5",
"@angular/forms": "2.4.7",
"@angular/http": "2.4.7",
"@angular/platform-browser": "2.4.7",
"@angular/platform-browser-dynamic": "2.4.7",
"@angular/router": "3.4.7",
"angularfire2": "^2.0.0-beta.7",
"bootstrap": "^4.1.3",
"core-js": "2.4.1",
"firebase": "10.9.0",
"ng2-bootstrap": "^1.3.3",
"ng2-dnd": "^2.2.2",
"rxjs": "5.0.3",
"ts-helpers": "1.1.2",
"zone.js": "0.7.6"
"@angular/animations": "^19.2.21",
"@angular/cdk": "^19.2.19",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/fire": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
"bootstrap": "^5.3.8",
"firebase": "^11.10.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/compiler-cli": "2.4.7",
"@types/jasmine": "2.5.38",
"@types/node": "^6.0.60",
"angular-cli": "1.0.0-beta.28.3",
"codelyzer": "~1.0.0-beta.3",
"jasmine-core": "2.5.2",
"jasmine-spec-reporter": "2.5.0",
"karma": "6.3.16",
"karma-chrome-launcher": "2.0.0",
"karma-cli": "1.0.1",
"karma-jasmine": "1.0.2",
"karma-remap-istanbul": "0.2.1",
"protractor": "4.0.13",
"ts-node": "1.2.1",
"tslint": "4.2.0",
"typescript": "~2.1.6"
"@angular-devkit/build-angular": "^19.2.24",
"@angular/cli": "^19.2.24",
"@angular/compiler-cli": "^19.2.0",
"@types/jasmine": "~5.1.0",
"angular-eslint": "^21.0.1",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
},
"overrides": {
"serialize-javascript": "^7.0.5",
"tar": "^7.5.11"
}
}
-32
View File
@@ -1,32 +0,0 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
/*global jasmine */
var SpecReporter = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
useAllAngular2AppRoots: true,
beforeLaunch: function() {
require('ts-node').register({
project: 'e2e'
});
},
onPrepare: function() {
jasmine.getEnv().addReporter(new SpecReporter());
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+31
View File
@@ -0,0 +1,31 @@
:host {
display: block;
height: 100%;
}
.board-header {
padding: 16px 0 12px;
border-bottom: 3px solid #5ba4cf;
margin-bottom: 0;
}
.board-header h1 {
font-size: 1.4rem;
font-weight: 600;
color: #172b4d;
margin: 0;
}
.board {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px 0;
overflow-x: auto;
height: calc(100% - 60px);
}
.board-col {
flex: 0 0 320px;
max-height: 100%;
}
+9 -9
View File
@@ -1,12 +1,12 @@
<div class="container">
<h1 class="page-header">
{{title}}
</h1>
<div class="row">
<div *ngFor="let cardlist of cardlists" class="col-sm-4">
<cardlist [item]="cardlist">
</cardlist>
<div class="container board-header">
<h1>{{title}}</h1>
</div>
<div class="container board">
@for (cardlist of cardlists; track cardlist.$key) {
<div class="board-col">
<app-cardlist [item]="cardlist"
[connectedDropLists]="getConnectedDropLists(cardlist.$key!)">
</app-cardlist>
</div>
}
</div>
+24 -35
View File
@@ -1,61 +1,51 @@
import {Component, OnInit} from "@angular/core";
import {DataService} from "app/shared/data.service";
import {Observable} from "rxjs";
import {Project} from "app/models/project-info";
import {CardList} from "app/models/cardlist-info";
import {Card} from "app/models/card-info";
import { Component, OnInit, inject } from '@angular/core';
import { DataService } from './shared/data.service';
import { Project } from './models/project-info';
import { CardList } from './models/cardlist-info';
import { CardListComponent } from './cardlist/cardlist.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CardListComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'The Kanban Board';
projects: Project[];
cardlists: CardList[];
projects: Project[] = [];
cardlists: CardList[] = [];
constructor(private dataService: DataService) {
}
private dataService = inject(DataService);
ngOnInit(){
ngOnInit(): void {
this.dataService.getProjects()
.subscribe(data => {
this.projects = data;
let firstProject = this.projects[0];
//console.log(firstProject);
// this.addAddCardList(
// 'Done',
// firstProject.$key,
// 'green'
// );
});
this.dataService.getCardLists()
.subscribe(c => this.cardlists = c)
;
.subscribe(c => this.cardlists = c);
this.dataService.getCards();
this.dataService.getTasks();
//this.addProject("TestProject1");
}
addProject(name: string)
{
let created_at = new Date().toString();
let newProject:Project = new Project();
addProject(name: string): void {
const created_at = new Date().toString();
const newProject = new Project();
newProject.name = name;
newProject.created_at = created_at;
this.dataService.addProject(newProject);
}
addCardList(
name: string,
projectId: string,
color: string,
order: number)
{
let created_at = new Date().toString();
let newCardList:CardList = new CardList();
getConnectedDropLists(currentKey: string): string[] {
return this.cardlists
.filter(c => c.$key !== currentKey)
.map(c => c.$key!);
}
addCardList(name: string, projectId: string, color: string, order: number): void {
const created_at = new Date().toString();
const newCardList = new CardList();
newCardList.name = name;
newCardList.projectId = projectId;
newCardList.color = color;
@@ -63,5 +53,4 @@ export class AppComponent implements OnInit {
newCardList.created_at = created_at;
this.dataService.addCardList(newCardList);
}
}
-36
View File
@@ -1,36 +0,0 @@
import {BrowserModule} from "@angular/platform-browser";
import {NgModule} from "@angular/core";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {HttpModule} from "@angular/http";
import {AppComponent} from "./app.component";
import {authConfig, firebaseConfig} from "environments/firebaseConfig";
import {AngularFireModule} from "angularfire2";
import {AlertModule} from "ng2-bootstrap";
import {ModalModule} from 'ng2-bootstrap';
import {DataService} from "app/shared/data.service";
import {CardListComponent} from "app/cardlist/cardlist.component";
import {CardComponent} from "app/card/card.component";
import {DndModule} from 'ng2-dnd';
@NgModule({
declarations: [
AppComponent,
CardListComponent,
CardComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
AlertModule.forRoot(),
ModalModule.forRoot(),
DndModule.forRoot(),
AngularFireModule.initializeApp(firebaseConfig, authConfig)
],
providers: [DataService],
bootstrap: [AppComponent]
})
export class AppModule {
}
+227 -26
View File
@@ -1,38 +1,239 @@
/* Card Title */
.cardTitle {
margin-left: 10px;
font-size: 1em;
font-weight: bold;
}
.cardDesc{
margin-left: 10px;
}
.tasklist{
margin: 10px;
}
.newtask
{
font-size: 0.8em;
}
.link{
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.9rem;
font-weight: 600;
color: #172b4d;
}
.carret {
display: inline-block;
cursor: pointer;
color: #5e6c84;
font-size: 0.85rem;
padding: 2px;
border-radius: 3px;
}
.carret:hover {
background-color: rgba(9, 30, 66, 0.08);
}
.titleText {
display: inline-block;
flex: 1;
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
line-height: 1.3;
}
.titleText:hover {
background-color: rgba(9, 30, 66, 0.08);
}
.placeholder {
color: #b0b8c4;
font-style: italic;
font-weight: 400;
}
/* Card Description */
.cardDesc {
margin: 6px 0 0 18px;
font-size: 0.82rem;
color: #5e6c84;
cursor: pointer;
padding: 3px 5px;
border-radius: 4px;
line-height: 1.4;
}
.cardDesc:hover {
background-color: rgba(9, 30, 66, 0.08);
}
/* Edit inputs */
.edit-input {
width: 100%;
padding: 5px 7px;
border: 2px solid #5ba4cf;
border-radius: 4px;
font-size: inherit;
font-family: inherit;
box-sizing: border-box;
outline: none;
}
.title-input {
font-weight: 600;
}
.desc-input {
resize: vertical;
margin-top: 6px;
}
/* Task list */
.tasklist {
margin: 8px 0 4px 18px;
}
.task-item {
display: flex;
align-items: center;
gap: 6px;
padding: 3px 0;
font-size: 0.82rem;
color: #172b4d;
}
.task-item.completed .task-text {
text-decoration: line-through;
color: #b0b8c4;
}
.task-text {
flex: 1;
}
.task-trash {
color: #b0b8c4;
cursor: pointer;
font-size: 0.75rem;
padding: 2px 4px;
border-radius: 3px;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
}
.task-item:hover .task-trash {
opacity: 1;
}
.task-trash:hover {
color: #e44;
}
/* New task input */
.newtask-input {
margin-top: 6px;
margin-left: 18px;
}
.newtask-input input {
border: none;
border-bottom: 1px solid #dfe1e6;
border-radius: 0;
font-size: 0.82rem;
padding: 4px 2px;
background: transparent;
width: 100%;
box-sizing: border-box;
}
.newtask-input input:focus {
border-bottom-color: #5ba4cf;
outline: none;
}
/* Delete Modal */
.modal-backdrop-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
cursor: default;
}
.delete-modal-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
pointer-events: none;
cursor: default;
}
.delete-modal-overlay .create-card-modal {
pointer-events: auto;
width: 340px;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
overflow: hidden;
cursor: default;
}
.modal-header {
padding:9px 15px;
border-bottom:1px solid #eee;
background-color: #ff4040;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #e44;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
color: white;
font-weight: bold;
}
.modal-header h4 {
margin: 0;
font-size: 0.95rem;
}
.modal-header .close {
color: #fff;
opacity: 0.8;
font-size: 1rem;
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.15s;
}
.modal-header .close:hover {
background: rgba(255, 255, 255, 0.35);
opacity: 1;
}
.modal-header .close:hover {
opacity: 1;
}
.modal-body p {
margin: 0 0 12px;
color: #172b4d;
font-size: 0.9rem;
}
.modal-actions {
display: flex;
gap: 8px;
}
.btn-delete {
background: #e44;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-delete:hover {
background: #d33;
}
.btn-cancel {
background: #f4f5f7;
color: #5e6c84;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-cancel:hover {
background: #ebecf0;
}
+54 -26
View File
@@ -1,48 +1,76 @@
<div class="cardTitle inline">
<div class="carret" (click)="clickCarret()">
<i class="fa fa-caret-right" *ngIf="!item.isExpanded"></i>
<i class="fa fa-caret-down" *ngIf="item.isExpanded"></i>
<div class="cardTitle">
<div class="carret" (click)="clickCarret()" tabindex="0" role="button" (keydown.enter)="clickCarret()" (keydown.space)="clickCarret()">
@if (!item.isExpanded) {
<i class="fa fa-caret-right"></i>
}
@if (item.isExpanded) {
<i class="fa fa-caret-down"></i>
}
</div>
<div class="titleText">
@if (editingTitle) {
<input class="edit-input title-input"
[(ngModel)]="editTitle"
(blur)="saveTitle()"
(keydown.enter)="saveTitle()"
(keydown.escape)="cancelEditTitle()">
} @else {
<div class="titleText" tabindex="0" role="button" (click)="startEditTitle()" (keydown.enter)="startEditTitle()">
@if (item.name) {
{{item.name}}
} @else {
<span class="placeholder">Click to add title</span>
}
</div>
}
</div>
<div *ngIf="item.isExpanded">
<div class="cardDesc">
@if (item.isExpanded) {
<div>
@if (editingDesc) {
<textarea class="edit-input desc-input"
[(ngModel)]="editDesc"
(blur)="saveDesc()"
(keydown.escape)="cancelEditDesc()"
rows="3">
</textarea>
} @else {
<div class="cardDesc" tabindex="0" role="button" (click)="startEditDesc()" (keydown.enter)="startEditDesc()">
{{item.description}}
</div>
}
<div class="tasklist">
<div *ngFor="let task of tasks" class="newtask">
<input type="checkbox" [(ngModel)]="task.isCompleted" (ngModelChange)="changeTaskCompleted(task)" class="inline-block">
<span class="inline-block">
{{task.description}}
</span>
<span class="inline-block glyphicon glyphicon-trash link" (click)="deleteTask(task)"></span>
@for (task of tasks; track task.$key) {
<div class="task-item" [class.completed]="task.isCompleted">
<input type="checkbox" [(ngModel)]="task.isCompleted" (ngModelChange)="changeTaskCompleted(task)">
<span class="task-text">{{task.description}}</span>
<span class="task-trash fa fa-trash" (click)="deleteTask(task)" tabindex="0" role="button" (keydown.enter)="deleteTask(task)"></span>
</div>
}
</div>
<div class="newtask">
<div class="newtask-input">
<form (submit)="addNewTask()">
<input type="text" class="form-control newtask" id="newtask" placeholder="Add subtask and hit enter" [(ngModel)]="newtaskdesc" name="newtask">
<input type="text" id="newtask" placeholder="Add subtask and hit enter" [(ngModel)]="newtaskdesc" name="newtask">
</form>
</div>
</div>
}
<div bsModal #childModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
@if (showModal) {
<div class="modal-backdrop-overlay" (click)="hideModal()" (mousedown)="$event.stopPropagation()" tabindex="0" role="button" (keydown.enter)="hideModal()"></div>
<div class="delete-modal-overlay" (mousedown)="$event.stopPropagation()">
<div class="create-card-modal">
<div class="modal-header">
<h4 class="modal-title pull-left">Delete task</h4>
<button type="button" class="close pull-right" aria-label="Close" (click)="hideChildModal()">
<h4 class="modal-title pull-left">Delete subtask</h4>
<button type="button" class="close pull-right" aria-label="Close" (click)="hideModal()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Not implement!<br>
Works as per specs!<br>
Carret is not carrot :)
<div class="modal-body" style="padding: 16px;">
<p>Delete "{{taskToDelete?.description}}"?</p>
<div class="modal-actions">
<button type="button" class="btn-delete" (click)="confirmDeleteTask()">Delete</button>
<button type="button" class="btn-cancel" (click)="hideModal()">Cancel</button>
</div>
</div>
</div>
</div>
}
+86 -33
View File
@@ -1,40 +1,85 @@
import {Component, OnInit, Input, ViewChild} from "@angular/core";
import {DataService} from "app/shared/data.service";
import {Observable} from "rxjs";
import {CardList} from "app/models/cardlist-info";
import {Card} from "app/models/card-info";
import {Task} from "app/models/task-info";
import { ModalDirective } from 'ng2-bootstrap/modal';
import { Component, OnInit, Input, HostListener, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DataService } from '../shared/data.service';
import { Card } from '../models/card-info';
import { Task } from '../models/task-info';
@Component({
selector: 'card',
selector: 'app-card',
standalone: true,
imports: [FormsModule],
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit {
@ViewChild('childModal') public childModal:ModalDirective;
@Input() item: Card;
tasks : Task[]
@Input() item!: Card;
tasks: Task[] = [];
showModal = false;
taskToDelete: Task | null = null;
newtaskdesc = '';
newtaskdesc;
editingTitle = false;
editTitle = '';
editingDesc = false;
editDesc = '';
private dataService = inject(DataService);
constructor(private dataService: DataService) {
//console.log(this.item);
@HostListener('document:keydown.escape')
onEscape(): void {
if (this.showModal) {
this.hideModal();
}
}
ngOnInit() {
//console.log(this.item);
this.dataService.getTasksByCardId(this.item.$key)
ngOnInit(): void {
this.dataService.getTasksByCardId(this.item.$key!)
.subscribe(data => {
this.tasks = data;
})
});
}
addNewTask(){
//console.log('Add new subtask!');
let newTask = new Task();
newTask.cardId = this.item.$key;
startEditTitle(): void {
this.editTitle = this.item.name ?? '';
this.editingTitle = true;
}
saveTitle(): void {
if (!this.editingTitle) return;
this.editingTitle = false;
const trimmed = this.editTitle.trim();
if (trimmed && trimmed !== this.item.name) {
this.item.name = trimmed;
this.dataService.updateCard(this.item.$key!, this.item);
}
}
cancelEditTitle(): void {
this.editingTitle = false;
}
startEditDesc(): void {
this.editDesc = this.item.description ?? '';
this.editingDesc = true;
}
saveDesc(): void {
if (!this.editingDesc) return;
this.editingDesc = false;
const trimmed = this.editDesc.trim();
if (trimmed !== this.item.description) {
this.item.description = trimmed;
this.dataService.updateCard(this.item.$key!, this.item);
}
}
cancelEditDesc(): void {
this.editingDesc = false;
}
addNewTask(): void {
const newTask = new Task();
newTask.cardId = this.item.$key!;
newTask.description = this.newtaskdesc;
newTask.isCompleted = false;
newTask.order = 0;
@@ -45,21 +90,29 @@ export class CardComponent implements OnInit {
});
}
deleteTask(task){
//console.log(task);
this.childModal.show();
}
public hideChildModal():void {
this.childModal.hide();
deleteTask(task: Task): void {
this.taskToDelete = task;
this.showModal = true;
}
changeTaskCompleted(task){
//console.log(task);
this.dataService.updateTask(task.$key, task);
hideModal(): void {
this.showModal = false;
this.taskToDelete = null;
}
clickCarret(){
confirmDeleteTask(): void {
if (this.taskToDelete?.$key) {
this.dataService.deleteTask(this.taskToDelete.$key);
}
this.hideModal();
}
changeTaskCompleted(task: Task): void {
this.dataService.updateTask(task.$key!, task);
}
clickCarret(): void {
this.item.isExpanded = !this.item.isExpanded;
this.dataService.updateCard(this.item.$key,this.item);
this.dataService.updateCard(this.item.$key!, this.item);
}
}
+211 -66
View File
@@ -1,86 +1,231 @@
.card{
background-color: lightblue;
border: solid 1px;
border-left-width: 5px;
margin: 15px;
/* Column Panel */
.column-panel {
background: #ebecf0;
border-radius: 12px;
max-height: calc(100vh - 140px);
display: flex;
flex-direction: column;
}
.cardTitle{
margin-left: 10px;
font-size: 1em;
.column-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px 6px;
border-top: 3px solid #ccc;
border-radius: 12px 12px 0 0;
}
.cardDesc{
margin-left: 10px;
.column-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.createCard{
padding: 10px;
/*background-color: lightcyan;*/
.column-dot {
font-size: 0.55rem;
}
.fullScreen{
.column-title {
font-weight: 700;
font-size: 0.95rem;
color: #172b4d;
}
.column-count {
background: rgba(9, 30, 66, 0.08);
color: #5e6c84;
border-radius: 10px;
padding: 1px 8px;
font-size: 0.75rem;
font-weight: 600;
}
/* Add button */
.add-card-btn {
background: none;
border: none;
color: #5e6c84;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
transition: background-color 0.15s, color 0.15s;
}
.add-card-btn:hover {
background-color: rgba(9, 30, 66, 0.08);
color: #172b4d;
}
/* Card list */
.card-drop-zone {
flex: 1;
overflow-y: auto;
padding: 4px 8px 8px;
min-height: 60px;
}
.card-list {
list-style: none;
margin: 0;
padding: 0;
}
.card-item {
background: #fff;
border-radius: 8px;
border-left-width: 4px;
border-left-style: solid;
padding: 8px 10px;
margin-bottom: 8px;
cursor: grab;
box-shadow: 0 1px 2px rgba(9, 30, 66, 0.1);
transition: box-shadow 0.15s;
}
.card-item:hover {
box-shadow: 0 4px 12px rgba(9, 30, 66, 0.15);
}
/* Create Card Modal */
.modal-backdrop-overlay {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
clear: both;
background: black;
opacity: 0.3;
padding-left: 35%;
padding-right: 35%;
padding-top: 35%;
padding-bottom: 15%;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
}
.fullScreentransparent{
position: absolute;
clear: both;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: transparent;
.modal-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
pointer-events: none;
}
.link{
.create-card-modal {
background: #fff;
border-radius: 12px;
width: 400px;
max-width: 90vw;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
pointer-events: auto;
}
.modal-header-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
color: #fff;
}
.modal-title-text {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
.modal-close-btn {
cursor: pointer;
opacity: 0.85;
font-size: 1.1rem;
}
.createTitle{
font-weight: bold;
color: white;
}
.formfields{
margin-top: 10px;
}
.inline{
display: inline-block;
}
.listTitle{
font-weight: bold;
font-size: 1.2em;
.modal-close-btn:hover {
opacity: 1;
}
.dnd-drag-start {
-moz-transform:scale(0.8);
-webkit-transform:scale(0.8);
transform:scale(0.8);
opacity:0.7;
border: 2px dashed #000;
.modal-form {
padding: 16px;
}
.dnd-drag-enter {
opacity:0.7;
border: 2px dashed #000;
.modal-form label {
font-size: 0.8rem;
font-weight: 600;
color: #5e6c84;
display: block;
margin-bottom: 4px;
}
.dnd-drag-over {
border: 2px dashed #000;
.modal-form .form-control {
border: 2px solid #dfe1e6;
border-radius: 6px;
font-size: 0.9rem;
padding: 8px 10px;
transition: border-color 0.15s;
}
.modal-form .form-control:focus {
border-color: #5ba4cf;
box-shadow: 0 0 0 1px #5ba4cf;
outline: none;
}
.dnd-sortable-drag {
-moz-transform:scale(0.9);
-webkit-transform:scale(0.9);
transform:scale(0.9);
opacity:0.7;
border: 1px dashed #000;
.modal-form .form-group + .form-group {
margin-top: 12px;
}
.modal-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
.btn-create {
background: #5ba4cf;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-create:hover {
background: #4a93be;
}
.btn-cancel {
background: #f4f5f7;
color: #5e6c84;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: background-color 0.15s;
}
.btn-cancel:hover {
background: #ebecf0;
}
/* CDK Drag-Drop */
.cdk-drop-list-receiving .card-drop-zone {
background-color: rgba(91, 164, 207, 0.1);
border: 2px dashed rgba(91, 164, 207, 0.5);
border-radius: 6px;
}
.cdk-drag-preview {
opacity: 0.9;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.cdk-drag-placeholder {
opacity: 0.2;
border: 2px dashed #dfe1e6;
border-radius: 8px;
background: #f4f5f7;
}
.cdk-drag-animating {
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
}
.cdk-drop-list-dragging .cdk-drag {
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
}
+41 -36
View File
@@ -1,53 +1,58 @@
<div class="panel panel-info"
dnd-droppable
[allowDrop]="allowDropFunction()"
(onDropSuccess)="cardDropped($event)"
>
<div class="panel-heading">
<div>
<i class="fa fa-1 fa-circle inline" aria-hidden="true" [style.color]="item.color"></i>
<span class="inline listTitle">
{{item.name}}
</span>
<button type="button" class="btn btn-default btn-xs inline" (click)="showAddCard()">
<div class="column-panel"
cdkDropList
[id]="item.$key!"
[cdkDropListData]="cards"
[cdkDropListConnectedTo]="connectedDropLists"
(cdkDropListDropped)="onCardDrop($event)">
<div class="column-header" [style.border-top-color]="item.color">
<div class="column-header-left">
<i class="fa fa-circle column-dot" aria-hidden="true" [style.color]="item.color"></i>
<span class="column-title">{{item.name}}</span>
<span class="column-count">{{cards.length}}</span>
</div>
<button type="button" class="add-card-btn" (click)="showAddCard()" title="Add card">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
<div class="">
<ul class="list-group">
<li *ngFor="let card of cards" class="list-group-item panel card" [style.border-left-color]="item.color"
dnd-draggable
[dragData]="card"
[dragEnabled]="allowDragFunction(card)"
>
<card [item]="card">
</card>
<div class="card-drop-zone">
<ul class="card-list">
@for (card of cards; track card.$key) {
<li class="card-item"
[style.border-left-color]="item.color"
cdkDrag
[cdkDragData]="card">
<app-card [item]="card">
</app-card>
</li>
}
</ul>
</div>
</div>
<div class="fullScreen" *ngIf="toShowAddCard" (click)="cancelAddCard()">
</div>
<div class="fullScreentransparent" *ngIf="toShowAddCard">
<div class="panel panel-default createCard well">
<div class="panel-heading" [style.background-color]="item.color">
<h4 class="createTitle">New task - {{ item.name }}
<div class="pull-right link" (click)="cancelAddCard()">
@if (toShowAddCard) {
<div class="modal-backdrop-overlay" (click)="cancelAddCard()" tabindex="0" role="button" (keydown.enter)="cancelAddCard()"></div>
<div class="modal-overlay">
<div class="create-card-modal">
<div class="modal-header-bar" [style.background-color]="item.color">
<h4 class="modal-title-text">New task - {{ item.name }}</h4>
<div class="modal-close-btn" (click)="cancelAddCard()" tabindex="0" role="button" (keydown.enter)="cancelAddCard()">
<i class="fa fa-window-close"></i>
</div>
</h4>
</div>
<div class="form-group formfields">
<div class="modal-form">
<div class="form-group">
<label for="taskname">Name</label>
<input type="text" class="form-control" id="taskname" placeholder="task name" [(ngModel)]="cardname">
<input type="text" class="form-control" id="taskname" placeholder="Task name" [(ngModel)]="cardname">
</div>
<div class="form-group">
<label for="taskdescription">Description</label>
<textarea cols="39" rows="6" class="form-control" id="taskdescription" placeholder="description" [(ngModel)] = "carddescription"></textarea>
<!--<input type="text" class="form-control" id="taskdescription" placeholder="description" [(ngModel)]="carddescription">-->
<textarea class="form-control" id="taskdescription" rows="4" placeholder="Description" [(ngModel)]="carddescription"></textarea>
</div>
<div class="text-center">
<button type="button" class="btn btn-primary" (click)="saveAddCard()">CREATE</button>
<div class="modal-actions">
<button type="button" class="btn-create" (click)="saveAddCard()">Create</button>
<button type="button" class="btn-cancel" (click)="cancelAddCard()">Cancel</button>
</div>
</div>
</div>
</div>
}
+46 -73
View File
@@ -1,82 +1,55 @@
import {Component, OnInit, Input} from "@angular/core";
import {DataService} from "app/shared/data.service";
import {Observable} from "rxjs";
import {CardList} from "app/models/cardlist-info";
import {Card} from "app/models/card-info";
import {Task} from "app/models/task-info";
import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CdkDragDrop, transferArrayItem, moveItemInArray, CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';
import { DataService } from '../shared/data.service';
import { CardList } from '../models/cardlist-info';
import { Card } from '../models/card-info';
import { CardComponent } from '../card/card.component';
@Component({
selector: 'cardlist',
selector: 'app-cardlist',
standalone: true,
imports: [FormsModule, CdkDropList, CdkDrag, CardComponent],
templateUrl: './cardlist.component.html',
styleUrls: ['./cardlist.component.css']
})
export class CardListComponent implements OnInit {
@Input() item: CardList;
cards : Card[]
@Input() item!: CardList;
@Input() connectedDropLists: string[] = [];
@Output() cardDropped = new EventEmitter<void>();
cards: Card[] = [];
toShowAddCard:boolean;
editCard: Card;
cardname;
carddescription;
allowedDropFrom = [];
allowedDragTo = false;
toShowAddCard = false;
cardname = '';
carddescription = '';
private dataService = inject(DataService);
constructor(private dataService: DataService) {
}
ngOnInit() {
this.dataService.getCardsByListId(this.item.$key)
ngOnInit(): void {
this.dataService.getCardsByListId(this.item.$key!)
.subscribe(data => {
this.cards = data;
});
//fill allowed drop-from containers
this.dataService.getCardListsByOrder(this.item.order-1)
.subscribe(d => {
if(d.length>0)
this.allowedDropFrom.push(d[0].$key);
}
);
//fill if it has next containers
this.dataService.getCardListsByOrder(this.item.order+1)
.subscribe(d => {
if(d.length>0)
this.allowedDragTo = true;
}
);
}
showAddCard(){
showAddCard(): void {
this.cardname = '';
this.carddescription = '';
this.toShowAddCard = true;
}
cancelAddCard(){
this.toShowAddCard = false;
}
saveAddCard(){
//console.log('save card');
this.addCard(
this.cardname,
this.carddescription,
true,
this.item.$key,
0);
cancelAddCard(): void {
this.toShowAddCard = false;
}
saveAddCard(): void {
this.addCard(this.cardname, this.carddescription, true, this.item.$key!, 0);
this.toShowAddCard = false;
}
addCard(
name: string,
description: string,
isExpanded: boolean,
cardListId: string,
order: number
)
{
let created_at = new Date().toString();
let newCard:Card = new Card();
addCard(name: string, description: string, isExpanded: boolean, cardListId: string, order: number): void {
const created_at = new Date().toString();
const newCard = new Card();
newCard.name = name;
newCard.description = description;
newCard.cardListId = cardListId;
@@ -86,22 +59,22 @@ export class CardListComponent implements OnInit {
this.dataService.addCard(newCard);
}
cardDropped(ev){
let card:Card = ev.dragData;
if(card.cardListId !== this.item.$key){
card.cardListId = this.item.$key;
this.dataService.updateCard(card.$key, card);
onCardDrop(event: CdkDragDrop<Card[]>): void {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
// Update order for all cards in this list
this.cards.forEach((card, i) => {
card.cardListId = this.item.$key!;
card.order = i;
this.dataService.updateCard(card.$key!, card);
});
}
}
allowDragFunction(card: Card){
return this.allowedDragTo;
}
allowDropFunction(): any {
return (dragData: Card) => {
return this.allowedDropFrom.indexOf(dragData.cardListId) > -1;
};
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from './app.component';
export * from './app.module';
+1 -1
View File
@@ -1,7 +1,7 @@
export class Task {
$key?: string;
description?: string;
isCompleted: boolean;
isCompleted = false;
cardId?: string;
order?: number;
created_at?: string;
+110 -100
View File
@@ -1,122 +1,132 @@
import {Injectable, EventEmitter, Output} from "@angular/core";
import {AngularFire, FirebaseObjectObservable, FirebaseListObservable} from "angularfire2";
import {BehaviorSubject} from "rxjs/BehaviorSubject";
import {Observable, Subject, ReplaySubject, AsyncSubject} from "rxjs";
import {Project} from "../models/project-info";
import {CardList} from "../models/cardlist-info";
import {Card} from "../models/card-info";
import {Task} from "../models/task-info";
import { Injectable, inject, Injector, runInInjectionContext } from '@angular/core';
import { AngularFireDatabase, AngularFireList, QueryFn } from '@angular/fire/compat/database';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Project } from '../models/project-info';
import { CardList } from '../models/cardlist-info';
import { Card } from '../models/card-info';
import { Task } from '../models/task-info';
@Injectable()
@Injectable({
providedIn: 'root'
})
export class DataService {
projects: FirebaseListObservable<Project[]>;
cardlists: FirebaseListObservable<CardList[]>;
cards: FirebaseListObservable<Card[]>;
tasks: FirebaseListObservable<Task[]>;
private db = inject(AngularFireDatabase);
private injector = inject(Injector);
constructor(private af: AngularFire) {
//console.log("DataService");
private projectsRef: AngularFireList<Project>;
private cardlistsRef: AngularFireList<CardList>;
private cardsRef: AngularFireList<Card>;
private tasksRef: AngularFireList<Task>;
constructor() {
this.projectsRef = this.db.list('/projects');
this.cardlistsRef = this.db.list('/cardlist', ref => ref.orderByChild('order'));
this.cardsRef = this.db.list('/cards');
this.tasksRef = this.db.list('/tasks');
}
getProjects(){
this.projects = this.af.database.list('/projects') as
FirebaseListObservable<Project[]>;
return this.projects;
private stripKey<T extends { $key?: string }>(obj: T): Omit<T, '$key'> {
const copy = { ...obj };
delete copy.$key;
return copy;
}
addProject(project) {
return this.projects.push(project);
private queryList<T>(path: string, queryFn: QueryFn): Observable<T[]> {
return runInInjectionContext(this.injector, () => {
const ref = this.db.list(path, queryFn);
return ref.snapshotChanges().pipe(
map(changes =>
changes.map(c => ({ $key: c.payload.key, ...(c.payload.val() as object) } as T))
)
);
});
}
getCardLists(){
this.cardlists = this.af.database.list('/cardlist',{
query: {
orderByChild: 'order'
}}
) as
FirebaseListObservable<CardList[]>;
return this.cardlists;
}
getCardListsById(cardListId:string): FirebaseObjectObservable<CardList> {
return this.af.database.object(`/cardlist/${cardListId}`) as FirebaseObjectObservable<CardList>;
}
getCardListsByOrder(order:number): FirebaseListObservable<CardList[]> {
let _cardlist = this.af.database.list('/cardlist',{
query: {
orderByChild: 'order',
equalTo: order,
}}
) as FirebaseListObservable<CardList[]>;
return _cardlist;
}
getCachedCardListsById(cardListId:string):CardList {
return this.cardlists
.filter(d => d.$key == cardListId)
.map(d=> d.$key)
;
//.first();
}
getCardListsByProject(projectId: string){
let _cardlist = this.af.database.list('/cardlist',{
query: {
orderByChild: 'projectId',
equalTo: projectId,
}}
) as FirebaseListObservable<CardList[]>;
return _cardlist
}
addCardList(cardlist){
return this.cardlists.push(cardlist);
private snapshotsWithKey<T>(ref: AngularFireList<unknown>): Observable<T[]> {
return ref.snapshotChanges().pipe(
map(changes =>
changes.map(c => ({ $key: c.payload.key, ...(c.payload.val() as object) } as T))
)
);
}
// --- Projects ---
getCards(){
this.cards = this.af.database.list('/cards') as
FirebaseListObservable<Card[]>;
return this.cards;
}
getCardsByListId(listId:string){
this.cards = this.af.database.list('/cards',{
query: {
orderByChild: 'cardListId',
equalTo: listId,
}}
) as
FirebaseListObservable<Card[]>;
return this.cards;
}
addCard(card){
return this.cards.push(card);
}
updateCard(key, updCard){
return this.cards.update(key, updCard);
getProjects(): Observable<Project[]> {
return this.snapshotsWithKey<Project>(this.projectsRef);
}
addProject(project: Project) {
return this.projectsRef.push(this.stripKey(project));
}
// --- CardLists ---
getTasks(){
this.tasks = this.af.database.list('/tasks') as
FirebaseListObservable<Task[]>;
return this.cards;
getCardLists(): Observable<CardList[]> {
return this.snapshotsWithKey<CardList>(this.cardlistsRef);
}
getTasksByCardId(cardId:string){
let _tasks = this.af.database.list('/tasks',{
query: {
orderByChild: 'cardId',
equalTo: cardId,
}}
) as FirebaseListObservable<Task[]>;
return _tasks;
getCardListsById(cardListId: string): Observable<CardList | null> {
return this.db.object<CardList>(`/cardlist/${cardListId}`).snapshotChanges().pipe(
map(c => ({ $key: c.payload.key, ...c.payload.val() } as CardList))
);
}
addTask(task){
return this.tasks.push(task);
getCardListsByOrder(order: number): Observable<CardList[]> {
return this.queryList<CardList>('/cardlist', ref => ref.orderByChild('order').equalTo(order));
}
updateTask(key, updTask){
return this.tasks.update(key, updTask);
getCardListsByProject(projectId: string): Observable<CardList[]> {
return this.queryList<CardList>('/cardlist', ref => ref.orderByChild('projectId').equalTo(projectId));
}
addCardList(cardlist: CardList) {
return this.cardlistsRef.push(this.stripKey(cardlist));
}
// --- Cards ---
getCards(): Observable<Card[]> {
return this.snapshotsWithKey<Card>(this.cardsRef);
}
getCardsByListId(listId: string): Observable<Card[]> {
return this.queryList<Card>('/cards', ref => ref.orderByChild('cardListId').equalTo(listId)).pipe(
map(cards => cards.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)))
);
}
addCard(card: Card) {
return this.cardsRef.push(this.stripKey(card));
}
updateCard(key: string, updCard: Card) {
return this.cardsRef.update(key, this.stripKey(updCard));
}
// --- Tasks ---
getTasks(): Observable<Task[]> {
return this.snapshotsWithKey<Task>(this.tasksRef);
}
getTasksByCardId(cardId: string): Observable<Task[]> {
return this.queryList<Task>('/tasks', ref => ref.orderByChild('cardId').equalTo(cardId));
}
addTask(task: Task) {
return this.tasksRef.push(this.stripKey(task));
}
updateTask(key: string, updTask: Task) {
return this.tasksRef.update(key, this.stripKey(updTask));
}
deleteTask(key: string) {
return runInInjectionContext(this.injector, () => {
return this.db.object('/tasks/' + key).remove();
});
}
}
View File
-5
View File
@@ -1,8 +1,3 @@
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `angular-cli.json`.
export const environment = {
production: false
};
@@ -0,0 +1,9 @@
export const firebaseConfig = {
apiKey: '',
authDomain: '',
databaseURL: '',
storageBucket: '',
messagingSenderId: ''
};
export const recaptchaSiteKey = '';
-18
View File
@@ -1,18 +0,0 @@
import {AuthMethods, AuthProviders} from "angularfire2";
export const firebaseConfig = {
//get these from your created firebase project at https://console.firebase.google.com
// Paste all this from the Firebase console...
apiKey: "",
authDomain: "",
databaseURL: "",
storageBucket: "",
messagingSenderId: ""
};
export const authConfig = {
provider: AuthProviders.Password,
method: AuthMethods.Password
};
+2 -3
View File
@@ -1,15 +1,14 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The Kanban Board</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<app-root>Loading...</app-root>
<app-root></app-root>
</body>
</html>
+34 -6
View File
@@ -1,12 +1,40 @@
import './polyfills.ts';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { enableProdMode, importProvidersFrom } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFireDatabaseModule } from '@angular/fire/compat/database';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { initializeApp, getApps } from 'firebase/app';
import { initializeAppCheck, ReCaptchaV3Provider } from '@firebase/app-check';
import { environment } from './environments/environment';
import { AppModule } from './app/';
import { firebaseConfig, recaptchaSiteKey } from './environments/firebaseConfig';
import { AppComponent } from './app/app.component';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
// Initialize Firebase and App Check before Angular bootstrap
if (getApps().length === 0) {
const app = initializeApp(firebaseConfig);
if (recaptchaSiteKey) {
initializeAppCheck(app, {
provider: new ReCaptchaV3Provider(recaptchaSiteKey),
isTokenAutoRefreshEnabled: true
});
}
}
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
AngularFireModule.initializeApp(firebaseConfig),
AngularFireDatabaseModule,
DragDropModule
)
]
}).catch(err => console.error(err));
-19
View File
@@ -1,19 +0,0 @@
// This file includes polyfills needed by Angular 2 and is loaded before
// the app. You can add your own extra polyfills to this file.
import 'core-js/es6/symbol';
import 'core-js/es6/object';
import 'core-js/es6/function';
import 'core-js/es6/parse-int';
import 'core-js/es6/parse-float';
import 'core-js/es6/number';
import 'core-js/es6/math';
import 'core-js/es6/string';
import 'core-js/es6/date';
import 'core-js/es6/array';
import 'core-js/es6/regexp';
import 'core-js/es6/map';
import 'core-js/es6/set';
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
+7 -1
View File
@@ -1 +1,7 @@
/* You can add global styles to this file, and also import other style files */
/* Global styles */
html, body {
height: 100%;
margin: 0;
background-color: #f4f5f7;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
-32
View File
@@ -1,32 +0,0 @@
import './polyfills.ts';
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy.js';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
declare var __karma__: any;
declare var require: any;
// Prevent Karma from running prematurely.
__karma__.loaded = function () {};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
let context = require.context('./', true, /\.spec\.ts/);
// And load the modules.
context.keys().map(context);
// Finally, start Karma to run the tests.
__karma__.start();
-18
View File
@@ -1,18 +0,0 @@
{
"compilerOptions": {
"baseUrl": "",
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es6", "dom"],
"mapRoot": "./",
"module": "es6",
"moduleResolution": "node",
"outDir": "../dist/out-tsc",
"sourceMap": true,
"target": "es5",
"typeRoots": [
"../node_modules/@types"
]
}
}
-2
View File
@@ -1,2 +0,0 @@
// Typings reference file, you can add your own global typings here
// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}
+27
View File
@@ -0,0 +1,27 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
-114
View File
@@ -1,114 +0,0 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"indent": [
true,
"spaces"
],
"label-position": true,
"label-undefined": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
"static-before-instance",
"variables-before-functions"
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-inferrable-types": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector-prefix": [true, "app"],
"component-selector-prefix": [true, "app"],
"directive-selector-name": [true, "camelCase"],
"component-selector-name": [true, "kebab-case"],
"directive-selector-type": [true, "attribute"],
"component-selector-type": [true, "element"],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"templates-use-public": true,
"invoke-injectable": true
}
}