feat: first commit
This commit is contained in:
commit
ac41514ba3
|
|
@ -0,0 +1,17 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
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
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# 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
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"plugins": ["@angular-eslint/template-parser"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.component.html"],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "pnpm"
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"planning-FE": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/assets",
|
||||
"output": "assets"
|
||||
}
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["src/assets/style"]
|
||||
},
|
||||
"styles": ["src/assets/styles/_loader.scss"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "planning-FE:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "planning-FE:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"attributePosition": "auto",
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 120,
|
||||
"lineEnding": "lf"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noConsole": "error",
|
||||
"noExplicitAny": "error",
|
||||
"noDebugger": "error",
|
||||
"noDoubleEquals": "error"
|
||||
},
|
||||
"style": {
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"semicolons": "always",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 200
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "planning-fe",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "ng serve --proxy-config proxy.conf.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.1.0",
|
||||
"@angular/compiler": "^20.1.0",
|
||||
"@angular/core": "^20.1.0",
|
||||
"@angular/forms": "^20.1.0",
|
||||
"@angular/platform-browser": "^20.1.0",
|
||||
"@angular/router": "^20.1.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
||||
"bootstrap": "^5.3.8",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "^20.3.0",
|
||||
"@angular-eslint/template-parser": "^20.3.0",
|
||||
"@angular/animations": "^20.3.3",
|
||||
"@angular/build": "^20.1.5",
|
||||
"@angular/cli": "^20.1.5",
|
||||
"@angular/compiler-cli": "^20.1.0",
|
||||
"@biomejs/biome": "2.1.4",
|
||||
"ngx-toastr": "^19.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.2"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,112 @@
|
|||
import { Directive, OnDestroy } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { takeWhile } from "rxjs";
|
||||
|
||||
type CustomFormControl<TBody> = FormControl & {
|
||||
_validators?: RegExp[];
|
||||
_dependsOn?: {
|
||||
key: keyof TBody;
|
||||
value: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
export interface IFormFieldData<TBody> {
|
||||
name: keyof TBody;
|
||||
value: string | number;
|
||||
validators?: RegExp[];
|
||||
dependsOn?: {
|
||||
key: keyof TBody;
|
||||
value: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export abstract class IForm<TBody> implements OnDestroy {
|
||||
private _isAlive = true;
|
||||
|
||||
protected formBody!: TBody;
|
||||
protected form!: FormGroup;
|
||||
protected formValid!: boolean;
|
||||
|
||||
protected setForm(...fields: IFormFieldData<TBody>[]) {
|
||||
const controls: Record<keyof TBody, CustomFormControl<TBody>> = {} as Record<
|
||||
keyof TBody,
|
||||
CustomFormControl<TBody>
|
||||
>;
|
||||
|
||||
for (const field of fields) {
|
||||
controls[field.name] = new FormControl(field.value);
|
||||
controls[field.name]._validators = field.validators || [];
|
||||
controls[field.name]._dependsOn = field.dependsOn || [];
|
||||
}
|
||||
|
||||
this.form = new FormGroup(controls, { updateOn: "change" });
|
||||
|
||||
Object.keys(this.form.controls)?.forEach((key) => {
|
||||
this.form.controls[key].valueChanges.pipe(takeWhile(() => this._isAlive)).subscribe(() => {
|
||||
this.formValid = this._isControlValidAndForm();
|
||||
});
|
||||
});
|
||||
|
||||
this.formValid = false;
|
||||
}
|
||||
|
||||
public asd(): boolean {
|
||||
return this._isControlValidAndForm();
|
||||
}
|
||||
|
||||
private _isControlValidAndForm(): boolean {
|
||||
let allFieldsValid = true;
|
||||
|
||||
Object.keys(this.form.controls).forEach((key) => {
|
||||
const dependencies = (this.form.controls[key] as CustomFormControl<TBody>)._dependsOn;
|
||||
let skipCheck = false;
|
||||
|
||||
dependencies?.forEach((matcher) => {
|
||||
const formControlValue = (this.form.controls[matcher.key as string] as CustomFormControl<TBody>).value; //[1,2,3]
|
||||
if (matcher.value === true) {
|
||||
if (!formControlValue) {
|
||||
skipCheck = true;
|
||||
}
|
||||
} else {
|
||||
if (formControlValue) {
|
||||
skipCheck = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipCheck) {
|
||||
const fieldValue = this.form.get(key)?.value;
|
||||
|
||||
(this.form.controls[key] as CustomFormControl<TBody>)._validators?.forEach((regexp) => {
|
||||
if (!regexp.test(fieldValue)) {
|
||||
allFieldsValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return this.form.valid && allFieldsValid;
|
||||
}
|
||||
|
||||
protected prepareFormBody(): Partial<TBody> {
|
||||
const rawValue = this.form.getRawValue();
|
||||
this.formBody = {} as TBody;
|
||||
|
||||
for (const key in rawValue) {
|
||||
if (Object.hasOwn(rawValue, key)) {
|
||||
const value = rawValue[key];
|
||||
|
||||
if (value !== null && value !== undefined && value !== "") {
|
||||
this.formBody[key as keyof TBody] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.formBody;
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this._isAlive = false;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,27 @@
|
|||
@forward 'sass:map';
|
||||
|
||||
$colors: (
|
||||
dark: #313131,
|
||||
light: #fefefe,
|
||||
error: #b63737,
|
||||
green: #2ecc71,
|
||||
green-dark: #2E7D32
|
||||
);
|
||||
|
||||
$font: (
|
||||
default: 18px,
|
||||
large: 24px,
|
||||
ultra: 64px
|
||||
);
|
||||
|
||||
$spacing: (
|
||||
xxs: 4px,
|
||||
tiny: 8px,
|
||||
normal: 16px,
|
||||
);
|
||||
|
||||
$border: (
|
||||
size: 1px,
|
||||
radius: 8px,
|
||||
circle: 50%
|
||||
);
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
@use 'sass:map';
|
||||
|
||||
@use "bootstrap/scss/bootstrap";
|
||||
@use 'ngx-toastr/toastr';
|
||||
|
||||
@use 'core' as *;
|
||||
@use './components/root.scss';
|
||||
@use './components/modal.scss';
|
||||
@use './components/layout';
|
||||
|
||||
@font-face {
|
||||
font-family: 'regular';
|
||||
src: url('./Ubuntu-Light.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'bold';
|
||||
src: url('./Ubuntu-Bold.ttf');
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'regular', Arial, Helvetica, sans-serif;
|
||||
background-color: map.get($colors, dark);
|
||||
color: map.get($colors, light);
|
||||
font-size: map.get($font, default);
|
||||
width: 100svw;
|
||||
height: 100svh;
|
||||
}
|
||||
|
||||
.title,
|
||||
.bold {
|
||||
font-family: 'bold', Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
place-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: map.get($spacing, tiny) map.get($spacing, normal);
|
||||
border: map.get($border, size) solid map.get($colors, light);
|
||||
border-radius: map.get($border, radius);
|
||||
|
||||
&.__primary {
|
||||
cursor: pointer;
|
||||
border-color: map.get($colors, green);
|
||||
|
||||
&:hover {
|
||||
background-color: map.get($colors, green-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@use '../core' as *;
|
||||
@use 'sass:map';
|
||||
|
||||
.row>* {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
@use '../core' as *;
|
||||
@use 'sass:map';
|
||||
|
||||
$-header-height: 64px;
|
||||
$-header-footer: 64px;
|
||||
$-content-height: calc(100% - $-header-height - $-header-footer);
|
||||
|
||||
.custom-modal-container {
|
||||
.custom-modal-header {
|
||||
height: $-header-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: map.get($spacing, normal);
|
||||
border-bottom: map.get($border, size) solid map.get($colors, dark);
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
font-size: map.get($font, large);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
place-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: map.get($spacing, xxs);
|
||||
border: map.get($border, size) solid map.get($colors, dark);
|
||||
border-radius: map.get($border, circle);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
height: $-content-height;
|
||||
padding: map.get($spacing, normal);
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
height: $-header-footer;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
@use '../core' as *;
|
||||
@use 'sass:map';
|
||||
|
||||
$-header-height: 64px;
|
||||
$page-container-height: calc(100% - $-header-height);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: map.get($spacing, normal);
|
||||
height: $-header-height;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
height: $page-container-height;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
place-content: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: map.get($font, ultra);
|
||||
color: map.get($colors, error);
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
@if (!loadingInit) {
|
||||
@if (initError) {
|
||||
<div class="error bold">Il server non è online</div>
|
||||
} @else {
|
||||
<div class="header">
|
||||
<div class="button __primary" (click)="test()">Aggiungi entry</div>
|
||||
<div class="button __primary">Impostazioni</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">CIAO</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from "@angular/core";
|
||||
import { AddEntryModalComponent } from "@modal/add-entry/add-entry.component";
|
||||
import { api } from "@static/api.class";
|
||||
import { modal } from "@static/modal.class";
|
||||
import { finalize } from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
templateUrl: "./root.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RootComponent implements OnInit {
|
||||
protected loadingInit = true;
|
||||
protected isLoading = true;
|
||||
protected initError = false;
|
||||
|
||||
private _cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
this._cdr.detach();
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
this._cdr.detectChanges();
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
api.checkConnection()
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loadingInit = false;
|
||||
this._update();
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.initError = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected test(): void {
|
||||
modal.open(AddEntryModalComponent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { i18n, i18nPath } from "@static/i18n";
|
||||
|
||||
export const GLOBAL_CONFIG = {
|
||||
defaultDateTimeLocale: "it-IT",
|
||||
};
|
||||
|
||||
export type translateKey = i18nPath<typeof i18n.allLanguages>;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export const IT_IT = {
|
||||
toast: {
|
||||
error: {
|
||||
create_game_already_exists: "A savefile with this name already exists.",
|
||||
},
|
||||
},
|
||||
dropdown: {
|
||||
type: {
|
||||
income: "Entrata",
|
||||
outcome: "Uscita",
|
||||
investment: "Investimento",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export const VALIDATORS = {
|
||||
REQUIRED: /^.{1,}$/,
|
||||
MIN_1_CHARS: /^.{1,}$/,
|
||||
MAX_50_CHARS: /^.{0,50}$/,
|
||||
ONLY_POSITIVE_NUMBERS: /^(?:(?!0+(?:\.0+)?$)(?:\d+|\d*\.\d+))$/,
|
||||
VALID_HEX_COLOR: /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/,
|
||||
DATE: /^(?:(?:31-(?:0?[13578]|1[02]))|(?:29|30-(?:0?[1,3-9]|1[0-2])))-(?:19|20)\d\d$|^29-0?2-(?:(?:19|20)(?:[02468][048]|[13579][26]))$|^(?:0?[1-9]|1\d|2[0-8])-(?:0?[1-9]|1[0-2])-(?:19|20)\d\d$/,
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PlanningFE</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { provideHttpClient } from "@angular/common/http";
|
||||
import {
|
||||
provideAppInitializer,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
provideZonelessChangeDetection,
|
||||
} from "@angular/core";
|
||||
import { bootstrapApplication } from "@angular/platform-browser";
|
||||
import { RootComponent } from "@components/root/root.component";
|
||||
import { api } from "@static/api.class";
|
||||
import { i18n } from "@static/i18n";
|
||||
import { modal } from "@static/modal.class";
|
||||
import { toast } from "@static/toast.static";
|
||||
import { provideToastr } from "ngx-toastr";
|
||||
|
||||
bootstrapApplication(RootComponent, {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZonelessChangeDetection(),
|
||||
provideHttpClient(),
|
||||
provideToastr(),
|
||||
provideAppInitializer(() => {
|
||||
api.init();
|
||||
modal.init();
|
||||
toast.init();
|
||||
i18n.init();
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<div class="custom-modal-container">
|
||||
<div class="custom-modal-header">
|
||||
<div class="title">Aggiunta entry</div>
|
||||
<div class="close" (click)="close()">X</div>
|
||||
</div>
|
||||
<div class="custom-modal-content" [formGroup]="form">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="title">Nome</div>
|
||||
<input type="text" [formControlName]="'name'" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="title">Tipo</div>
|
||||
<select [formControlName]="'type'">
|
||||
@for (entry of typeDropdownDatasource; track entry.id) {
|
||||
<option [ngValue]="entry.id">
|
||||
{{ translate(entry.value) }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="title">Quantità</div>
|
||||
<input type="number" [formControlName]="'amount'" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="title">Colore</div>
|
||||
<input type="text" [formControlName]="'color'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- TODO: checkbox + changeEvent change isRecurrent to @if @else other fields -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<div class="button" [class.__primary]="formValid" [class.__disabled]="!formValid">Aggiungi</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { IForm } from "@abstract/form.abstract";
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { translateKey } from "@config/global";
|
||||
import { VALIDATORS } from "@const/validators.const";
|
||||
import { dataSourceList } from "@model/datasource.model";
|
||||
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
|
||||
import { i18n } from "@static/i18n";
|
||||
|
||||
type AddEntryType = {
|
||||
name: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
color: string;
|
||||
recurrent: boolean;
|
||||
|
||||
fixedDate: string;
|
||||
|
||||
recurrentMonths: number[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "new-entry-modal",
|
||||
templateUrl: "./add-entry.component.html",
|
||||
imports: [ReactiveFormsModule],
|
||||
})
|
||||
export class AddEntryModalComponent extends IForm<Partial<AddEntryType>> {
|
||||
protected activeModal = inject(NgbActiveModal);
|
||||
protected typeDropdownDatasource = dataSourceList.type;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.setForm(
|
||||
{
|
||||
name: "name",
|
||||
value: "",
|
||||
validators: [VALIDATORS.MIN_1_CHARS, VALIDATORS.MAX_50_CHARS],
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
value: "",
|
||||
validators: [VALIDATORS.REQUIRED],
|
||||
},
|
||||
{
|
||||
name: "amount",
|
||||
value: "",
|
||||
validators: [VALIDATORS.ONLY_POSITIVE_NUMBERS],
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
value: "",
|
||||
validators: [VALIDATORS.VALID_HEX_COLOR],
|
||||
},
|
||||
{
|
||||
name: "recurrent",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "fixedDate",
|
||||
value: "12-12-2020",
|
||||
validators: [VALIDATORS.DATE],
|
||||
dependsOn: [
|
||||
{
|
||||
key: "recurrent",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "recurrentMonths",
|
||||
value: "",
|
||||
validators: [VALIDATORS.REQUIRED],
|
||||
dependsOn: [
|
||||
{
|
||||
key: "recurrent",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "startDate",
|
||||
value: "",
|
||||
validators: [VALIDATORS.DATE],
|
||||
dependsOn: [
|
||||
{
|
||||
key: "recurrent",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "endDate",
|
||||
value: "",
|
||||
validators: [VALIDATORS.DATE],
|
||||
dependsOn: [
|
||||
{
|
||||
key: "recurrent",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected close<TResult>(res?: TResult): void {
|
||||
this.asd();
|
||||
this.activeModal.close(res ?? null);
|
||||
}
|
||||
|
||||
protected translate(key: translateKey): string {
|
||||
return i18n.now(key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { translateKey } from "@config/global";
|
||||
|
||||
export type TypeDataSource = {
|
||||
id: string;
|
||||
value: translateKey;
|
||||
};
|
||||
|
||||
export const dataSourceList: {
|
||||
type: TypeDataSource[];
|
||||
} = {
|
||||
type: [
|
||||
{ id: "income", value: "dropdown.type.income" },
|
||||
{ id: "outcome", value: "dropdown.type.outcome" },
|
||||
{ id: "investment", value: "dropdown.type.investment" },
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/** biome-ignore-all lint/complexity/noStaticOnlyClass: static */
|
||||
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { inject } from "@angular/core";
|
||||
|
||||
export class api {
|
||||
public static http: HttpClient;
|
||||
|
||||
public static init(): void {
|
||||
api.http = inject(HttpClient);
|
||||
}
|
||||
|
||||
public static checkConnection() {
|
||||
return api.http.get(`/api/actuator/health`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/** biome-ignore-all lint/complexity/noStaticOnlyClass: static */
|
||||
/** biome-ignore-all lint/suspicious/noExplicitAny: needed */
|
||||
|
||||
import { IT_IT } from "@config/i18n/it_it";
|
||||
import { logger } from "./logger.static";
|
||||
|
||||
export type i18nPath<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]: K extends string
|
||||
? `${K}${"" | (i18nPath<T[K]> extends never ? "" : `.${i18nPath<T[K]>}`)}`
|
||||
: never;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
export class i18n {
|
||||
public static it = IT_IT;
|
||||
|
||||
public static currentLang: "it";
|
||||
public static allLanguages = IT_IT;
|
||||
|
||||
public static init(): void {
|
||||
i18n.currentLang = "it";
|
||||
}
|
||||
|
||||
public static now(key: i18nPath<typeof i18n.allLanguages>): string {
|
||||
const langObject = i18n[i18n.currentLang];
|
||||
|
||||
const parts = key.split(".");
|
||||
let result: { [key: string]: any } | string = langObject;
|
||||
|
||||
for (const part of parts) {
|
||||
if (result && typeof result === "object" && part in result) {
|
||||
result = result[part];
|
||||
} else {
|
||||
logger.error(`Translation key not found for current language: ${key}`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof result === "string") {
|
||||
return result;
|
||||
}
|
||||
|
||||
logger.error(`Translation key did not resolve to a string: ${key}`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/** biome-ignore-all lint/suspicious/noConsole: static logger */
|
||||
/** biome-ignore-all lint/complexity/noStaticOnlyClass: static logger */
|
||||
|
||||
export class logger {
|
||||
public static log<T>(message: T): void {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
public static error(message: string): void {
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/** biome-ignore-all lint/complexity/noStaticOnlyClass: static */
|
||||
/** biome-ignore-all lint/suspicious/noExplicitAny: static */
|
||||
|
||||
import { inject, Type } from "@angular/core";
|
||||
import { NgbModal, NgbModalOptions } from "@ng-bootstrap/ng-bootstrap";
|
||||
|
||||
type ModalOption = NgbModalOptions & {
|
||||
hasCloseIcon?: boolean;
|
||||
modalTitle?: string;
|
||||
};
|
||||
|
||||
const defaultOptions: Partial<NgbModalOptions> = {
|
||||
size: "lg",
|
||||
centered: true,
|
||||
modalDialogClass: "custom-modal-container",
|
||||
};
|
||||
|
||||
export class modal {
|
||||
public static ngbModal: NgbModal;
|
||||
|
||||
public static init(): void {
|
||||
modal.ngbModal = inject(NgbModal);
|
||||
}
|
||||
|
||||
public static open<TComponent, TResult>(
|
||||
component: Type<TComponent>,
|
||||
data?: any,
|
||||
options?: ModalOption,
|
||||
): Promise<TResult> {
|
||||
const effectiveOptions: NgbModalOptions = {
|
||||
...defaultOptions,
|
||||
...(options || {}),
|
||||
};
|
||||
|
||||
const modalRef = modal.ngbModal.open(component, effectiveOptions);
|
||||
|
||||
modalRef.componentInstance.modalTitle = options?.modalTitle;
|
||||
modalRef.componentInstance.hasCloseIcon = options?.hasCloseIcon;
|
||||
|
||||
Object.keys(data || {}).forEach((item) => {
|
||||
modalRef.componentInstance[item] = data[item];
|
||||
});
|
||||
|
||||
return modalRef.result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/** biome-ignore-all lint/complexity/noStaticOnlyClass: static */
|
||||
|
||||
import { inject } from "@angular/core";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { i18n, i18nPath } from "./i18n";
|
||||
|
||||
export class toast {
|
||||
public static toastService: ToastrService;
|
||||
|
||||
public static init(): void {
|
||||
toast.toastService = inject(ToastrService);
|
||||
}
|
||||
|
||||
public static ok(message: i18nPath<typeof i18n.allLanguages.toast>): void {
|
||||
toast.toastService.success(i18n.now(`toast.${message}`));
|
||||
}
|
||||
|
||||
public static error(message: i18nPath<typeof i18n.allLanguages.toast>): void {
|
||||
toast.toastService.error(i18n.now(`toast.${message}`));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "./",
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve",
|
||||
"paths": {
|
||||
"@pipes/*": ["src/pipes/*"],
|
||||
"@components/*": ["src/components/*"],
|
||||
"@services/*": ["src/services/*"],
|
||||
"@config/*": ["src/config/*"],
|
||||
"@abstract/*": ["src/abstract/*"],
|
||||
"@ui/*": ["src/ui/*"],
|
||||
"@const/*": ["src/const/*"],
|
||||
"@modal/*": ["src/modal/*"],
|
||||
"@model/*": ["src/model/*"],
|
||||
"@directives/*": ["src/directives/*"],
|
||||
"@static/*": ["src/static/*"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue