This commit is contained in:
Gal Podlipnik 2025-07-14 20:40:45 +02:00
parent be67268546
commit fd276b2c07
37 changed files with 710 additions and 104 deletions

View File

@ -0,0 +1,7 @@
# I18n
This library provides internationalization and language selection functionality for the application.
## Running unit tests
Run `nx test i18n` to execute the unit tests.

View File

@ -0,0 +1,5 @@
/* eslint-disable */
export default {
name: 'i18n',
extends: ['../../../eslint.base.config.mjs'],
};

View File

@ -0,0 +1,22 @@
/* eslint-disable */
export default {
displayName: 'i18n',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/shared/i18n',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@ -0,0 +1,21 @@
{
"name": "i18n",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/i18n/src",
"prefix": "lib",
"tags": [],
"projectType": "library",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/i18n/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
}
}

View File

@ -0,0 +1,2 @@
export * from './lib/language.service';
export * from './lib/language-selector.component';

View File

@ -0,0 +1,44 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { LanguageService } from './language.service';
@Component({
selector: 'lib-language-selector',
standalone: true,
imports: [MatButtonModule, MatIcon, MatMenuModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button
mat-icon-button
[matMenuTriggerFor]="languageMenu"
class="action-button"
aria-label="Select language"
>
<mat-icon>translate</mat-icon>
</button>
<mat-menu #languageMenu="matMenu">
<button mat-menu-item (click)="languageService.setLanguage('en')">
<span>English</span>
</button>
<button mat-menu-item (click)="languageService.setLanguage('sl')">
<span>Slovenian</span>
</button>
</mat-menu>
`,
styles: [
`
.action-button {
border-radius: 50%;
transition: background-color 0.2s ease;
}
.action-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`,
],
})
export class LanguageSelectorComponent {
protected languageService = inject(LanguageService);
}

View File

@ -0,0 +1,39 @@
import { Injectable, signal } from '@angular/core';
export type SupportedLanguage = 'en' | 'sl';
@Injectable({
providedIn: 'root',
})
export class LanguageService {
private readonly LANGUAGE_KEY = 'selectedLanguage';
currentLanguage = signal<SupportedLanguage>(this.#getInitialLanguage());
#getInitialLanguage(): SupportedLanguage {
// Try to get language from localStorage
const storedLang = localStorage.getItem(
this.LANGUAGE_KEY
) as SupportedLanguage | null;
if (storedLang && (storedLang === 'en' || storedLang === 'sl')) {
return storedLang;
}
// Try to detect from browser settings
const browserLang = navigator.language.substring(0, 2).toLowerCase();
if (browserLang === 'sl') {
return 'sl';
}
// Default to English
return 'en';
}
setLanguage(lang: SupportedLanguage): void {
this.currentLanguage.set(lang);
localStorage.setItem(this.LANGUAGE_KEY, lang);
// This is where you would typically trigger translation changes
// in a real implementation
console.log(`Language set to: ${lang}`);
}
}

View File

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true
},
"exclude": [
"src/test-setup.ts",
"**/*.spec.ts",
"jest.config.ts",
"**/*.test.ts"
]
}

View File

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -3,10 +3,19 @@ import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router';
import { ThemeToggleComponent } from '@org/shared/theme';
import { LanguageSelectorComponent } from '@org/shared/i18n';
@Component({
selector: 'lib-navbar',
imports: [MatToolbarModule, MatIcon, MatButtonModule, RouterModule],
imports: [
MatToolbarModule,
MatIcon,
MatButtonModule,
RouterModule,
ThemeToggleComponent,
LanguageSelectorComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<mat-toolbar color="primary" class="navbar">
@ -15,8 +24,35 @@ import { RouterModule } from '@angular/router';
<span class="brand">TaskManager</span>
</span>
<span class="spacer"></span>
<a mat-button routerLink="/users" routerLinkActive="active-link">Users</a>
<a mat-button routerLink="/tasks" routerLinkActive="active-link">Tasks</a>
<div class="nav-links">
<a
mat-button
routerLink="/users"
routerLinkActive="active-link"
class="nav-button"
>
<mat-icon>people</mat-icon>
<span>Users</span>
</a>
<a
mat-button
routerLink="/tasks"
routerLinkActive="active-link"
class="nav-button"
>
<mat-icon>task</mat-icon>
<span>Tasks</span>
</a>
</div>
<div class="action-buttons">
<!-- Theme toggle button -->
<lib-theme-toggle></lib-theme-toggle>
<!-- Language selector button -->
<lib-language-selector></lib-language-selector>
</div>
</mat-toolbar>
`,
styles: [
@ -24,6 +60,8 @@ import { RouterModule } from '@angular/router';
.navbar {
padding: 0 2rem;
min-height: 64px;
display: flex;
align-items: center;
}
.logo {
display: flex;
@ -39,7 +77,48 @@ import { RouterModule } from '@angular/router';
.spacer {
flex: 1 1 auto;
}
.nav-links {
display: flex;
gap: 0.5rem;
margin-right: 1rem;
}
.nav-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 1rem;
border-radius: 8px;
font-weight: 500;
transition: background-color 0.2s ease;
}
.nav-button.active-link {
background-color: rgba(255, 255, 255, 0.15);
position: relative;
}
.nav-button.active-link::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
right: 10%;
height: 3px;
background-color: white;
border-radius: 3px 3px 0 0;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.action-button {
border-radius: 50%;
transition: background-color 0.2s ease;
}
.action-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`,
],
})
export class NavbarComponent {}
export class NavbarComponent {
// All functionality has been moved to separate components
}

View File

@ -0,0 +1,7 @@
# Theme
This library provides theme management functionality for the application.
## Running unit tests
Run `nx test theme` to execute the unit tests.

View File

@ -0,0 +1,5 @@
/* eslint-disable */
export default {
name: 'theme',
extends: ['../../../eslint.base.config.mjs'],
};

View File

@ -0,0 +1,22 @@
/* eslint-disable */
export default {
displayName: 'theme',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/shared/theme',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@ -0,0 +1,21 @@
{
"name": "theme",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/theme/src",
"prefix": "lib",
"tags": [],
"projectType": "library",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/theme/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
}
}

View File

@ -0,0 +1,2 @@
export * from './lib/theme.service';
export * from './lib/theme-toggle.component';

View File

@ -0,0 +1,39 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { ThemeToggleService } from './theme.service';
@Component({
selector: 'lib-theme-toggle',
standalone: true,
imports: [MatButtonModule, MatIcon],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button
mat-icon-button
(click)="themeService.toggleTheme()"
class="action-button"
[attr.aria-label]="
themeService.isDark() ? 'Switch to light theme' : 'Switch to dark theme'
"
>
<mat-icon>{{
themeService.isDark() ? 'light_mode' : 'dark_mode'
}}</mat-icon>
</button>
`,
styles: [
`
.action-button {
border-radius: 50%;
transition: background-color 0.2s ease;
}
.action-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`,
],
})
export class ThemeToggleComponent {
protected themeService = inject(ThemeToggleService);
}

View File

@ -0,0 +1,55 @@
import { OverlayContainer } from '@angular/cdk/overlay';
import { computed, DOCUMENT, effect, inject, Injectable } from '@angular/core';
import { stored } from '@mmstack/primitives';
function prefersDarkMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
@Injectable({
providedIn: 'root',
})
export class ThemeToggleService {
private readonly overlayCls =
inject(OverlayContainer).getContainerElement().classList;
private readonly dockument = inject(DOCUMENT);
private readonly theme = stored<'light' | 'dark'>(
prefersDarkMode() ? 'dark' : 'light',
{
key: 'user-theme',
syncTabs: true,
}
);
public isDark = computed(() => this.theme() === 'dark');
constructor() {
effect(() =>
this.isDark() ? this.addDarkThemeClass() : this.removeDarkThemeClass()
);
effect(() =>
this.dockument.body.setAttribute(
'style',
`color-scheme: ${this.isDark() ? 'dark' : 'light'}`
)
);
}
public toggleTheme() {
console.log(`Toggling theme to ${this.isDark() ? 'light' : 'dark'}`);
this.theme.set(this.isDark() ? 'light' : 'dark');
}
private addDarkThemeClass() {
this.overlayCls.add('dark-theme');
this.dockument.body.classList.add('dark-theme');
}
private removeDarkThemeClass() {
this.overlayCls.remove('dark-theme');
this.dockument.body.classList.remove('dark-theme');
}
}

View File

@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"skipLibCheck": true,
"experimentalDecorators": true,
"importHelpers": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts"
],
"include": ["src/**/*.ts"]
}

View File

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"],
"moduleResolution": "node10"
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -87,13 +87,13 @@ import { TaskCommentComponent } from './task-comment.component';
`
.task-panel {
margin-bottom: 1rem;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
border-radius: var(--mat-sys-corner-small);
box-shadow: var(--mat-sys-level1);
transition: all 0.3s ease;
}
.task-panel:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
box-shadow: var(--mat-sys-level3);
}
.task-panel .mat-expansion-panel-header {
@ -147,7 +147,7 @@ import { TaskCommentComponent } from './task-comment.component';
.task-timestamp {
font-size: 0.8rem;
color: #757575;
color: var(--mat-sys-on-surface-variant);
}
.task-details {
@ -155,15 +155,15 @@ import { TaskCommentComponent } from './task-comment.component';
}
.task-description {
font-size: 1rem;
font-size: var(--mat-sys-body-large-size);
line-height: 1.6;
margin-bottom: 1.5rem;
white-space: pre-line;
color: #424242;
background-color: #f8f9fa;
color: var(--mat-sys-on-surface);
background-color: var(--mat-sys-surface-container-low);
padding: 1rem;
border-radius: 6px;
border-left: 4px solid #1976d2;
border-radius: var(--mat-sys-corner-small);
border-left: 4px solid var(--mat-sys-primary);
}
.comments-section {
@ -174,9 +174,9 @@ import { TaskCommentComponent } from './task-comment.component';
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.2rem;
font-size: var(--mat-sys-title-medium-size);
margin-bottom: 1rem;
color: #424242;
color: var(--mat-sys-on-surface);
}
.comments-section h3 mat-icon {

View File

@ -111,16 +111,16 @@ import { CommentResponse, User } from '@org/shared/api';
.comment-item {
padding: 1rem;
background-color: #f8f9fa;
border-radius: 8px;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
margin-bottom: 0.75rem;
border: 1px solid #e0e0e0;
border: 1px solid var(--mat-sys-outline-variant);
transition: transform 0.2s ease;
}
.comment-item:hover {
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
box-shadow: var(--mat-sys-level1);
}
.comment-header {
@ -131,45 +131,45 @@ import { CommentResponse, User } from '@org/shared/api';
}
.comment-author {
font-weight: 600;
color: #1976d2;
font-weight: var(--mat-sys-title-small-weight);
color: var(--mat-sys-primary);
}
.comment-time {
font-size: 0.8rem;
color: #757575;
font-size: var(--mat-sys-body-small-size);
color: var(--mat-sys-on-surface-variant);
}
.comment-content {
margin: 0;
white-space: pre-line;
color: #424242;
color: var(--mat-sys-on-surface);
line-height: 1.5;
}
.no-comments {
font-style: italic;
color: #757575;
color: var(--mat-sys-on-surface-variant);
text-align: center;
padding: 1.5rem;
background-color: #f5f5f5;
border-radius: 8px;
border: 1px dashed #e0e0e0;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
border: 1px dashed var(--mat-sys-outline-variant);
}
.comment-form {
background-color: #f8f9fa;
border-radius: 8px;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
padding: 1rem;
margin-top: 1rem;
border: 1px solid #e0e0e0;
border: 1px solid var(--mat-sys-outline-variant);
}
.comment-form h4 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.1rem;
color: #424242;
font-size: var(--mat-sys-title-medium-size);
color: var(--mat-sys-on-surface);
}
.form-row {

View File

@ -81,8 +81,8 @@ import { ToastService } from '@org/shared/toast';
}
.form-title {
font-weight: 700;
color: #1976d2;
font-weight: var(--mat-sys-label-large-weight-prominent);
color: var(--mat-sys-primary);
}
.full-width {

View File

@ -94,8 +94,8 @@ import { TaskCardComponent } from './task-card.component';
.page-header {
margin-bottom: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: var(--mat-sys-corner-small);
box-shadow: var(--mat-sys-level1);
}
.spacer {
@ -114,19 +114,19 @@ import { TaskCardComponent } from './task-card.component';
display: flex;
flex-direction: column;
align-items: center;
background-color: #f5f7fa;
border-radius: 8px;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
padding: 3rem 1.5rem;
margin: 1.5rem 0;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: var(--mat-sys-level1);
}
.empty-icon {
font-size: 3rem;
height: 3rem;
width: 3rem;
color: #bdbdbd;
color: var(--mat-sys-outline);
margin-bottom: 1rem;
}

View File

@ -56,20 +56,20 @@ import { RouterModule } from '@angular/router';
`
.user-card {
position: relative;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
background: var(--mat-sys-surface-container);
border-radius: var(--mat-sys-corner-medium);
box-shadow: var(--mat-sys-level1);
transition: all 0.2s ease;
padding-bottom: 0.5rem;
overflow: hidden;
}
.user-card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
box-shadow: var(--mat-sys-level3);
transform: translateY(-3px);
}
.user-link {
text-decoration: none;
color: #1976d2;
color: var(--mat-sys-primary);
font-weight: 600;
font-size: 1.1rem;
}

View File

@ -145,23 +145,23 @@ import { UserTaskListComponent } from './user-task-list.component';
}
mat-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: var(--mat-sys-corner-small);
box-shadow: var(--mat-sys-level1);
padding: 1rem;
height: fit-content;
}
.tasks-section {
background-color: #f9f9f9;
border-radius: 8px;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
padding: 1rem;
}
.tasks-section h2 {
font-size: 1.25rem;
font-size: var(--mat-sys-title-large-size);
margin-top: 0;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
border-bottom: 1px solid var(--mat-sys-outline-variant);
padding-bottom: 0.5rem;
display: flex;
align-items: center;
@ -170,34 +170,34 @@ import { UserTaskListComponent } from './user-task-list.component';
mat-list-item {
margin-bottom: 0.5rem;
border-left: 3px solid #3f51b5;
border-left: 3px solid var(--mat-sys-primary);
}
.task-title {
margin: 0;
font-weight: 500;
font-weight: var(--mat-sys-title-medium-weight);
}
.task-description {
white-space: pre-line;
color: rgba(0, 0, 0, 0.6);
color: var(--mat-sys-on-surface-variant);
}
.no-tasks {
text-align: center;
padding: 1.5rem;
color: rgba(0, 0, 0, 0.5);
background-color: white;
border-radius: 8px;
color: var(--mat-sys-on-surface-variant);
background-color: var(--mat-sys-surface-container);
border-radius: var(--mat-sys-corner-small);
}
lib-task-form {
display: block;
margin-top: 1.5rem;
padding: 1rem;
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
background-color: var(--mat-sys-surface-container);
border-radius: var(--mat-sys-corner-small);
box-shadow: var(--mat-sys-level1);
}
.back-button {

View File

@ -107,8 +107,8 @@ type UserWithCounts = User & { taskCount: number; commentCount: number };
.empty-state {
text-align: center;
padding: 2rem;
background-color: #f5f5f5;
border-radius: 4px;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
}
.user-grid {
display: grid;
@ -118,20 +118,20 @@ type UserWithCounts = User & { taskCount: number; commentCount: number };
}
.user-card {
position: relative;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
background: var(--mat-sys-surface-container);
border-radius: var(--mat-sys-corner-medium);
box-shadow: var(--mat-sys-level1);
transition: all 0.2s ease;
padding-bottom: 0.5rem;
overflow: hidden;
}
.user-card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
box-shadow: var(--mat-sys-level3);
transform: translateY(-3px);
}
.user-link {
text-decoration: none;
color: #1976d2;
color: var(--mat-sys-primary);
font-weight: 600;
font-size: 1.1rem;
}
@ -148,13 +148,13 @@ type UserWithCounts = User & { taskCount: number; commentCount: number };
display: flex;
gap: 1rem;
margin-top: 0.5rem;
font-size: 0.9rem;
color: #555;
font-size: var(--mat-sys-body-small-size);
color: var(--mat-sys-on-surface-variant);
}
.meta-icon {
vertical-align: middle;
margin-right: 0.2em;
color: #1976d2;
color: var(--mat-sys-primary);
}
@media (max-width: 600px) {
.container {

View File

@ -53,16 +53,16 @@ import { TaskFormComponent } from '@org/web/tasks';
styles: [
`
.tasks-section {
background-color: #f9f9f9;
border-radius: 8px;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
padding: 1rem;
}
.tasks-section h2 {
font-size: 1.25rem;
font-size: var(--mat-sys-title-large-size);
margin-top: 0;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
border-bottom: 1px solid var(--mat-sys-outline-variant);
padding-bottom: 0.5rem;
display: flex;
align-items: center;
@ -70,33 +70,33 @@ import { TaskFormComponent } from '@org/web/tasks';
}
mat-list {
border-radius: 4px;
border-radius: var(--mat-sys-corner-extra-small);
overflow: hidden;
background-color: white;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
background-color: var(--mat-sys-surface-container);
box-shadow: var(--mat-sys-level1);
}
mat-list-item {
margin-bottom: 0.5rem;
border-left: 3px solid #3f51b5;
border-left: 3px solid var(--mat-sys-primary);
}
.task-title {
margin: 0;
font-weight: 500;
font-weight: var(--mat-sys-title-medium-weight);
}
.task-description {
white-space: pre-line;
color: rgba(0, 0, 0, 0.6);
color: var(--mat-sys-on-surface-variant);
}
.no-tasks {
text-align: center;
padding: 1.5rem;
color: rgba(0, 0, 0, 0.5);
background-color: white;
border-radius: 8px;
color: var(--mat-sys-on-surface-variant);
background-color: var(--mat-sys-surface-container);
border-radius: var(--mat-sys-corner-small);
}
lib-task-form {

132
client/package-lock.json generated
View File

@ -19,6 +19,8 @@
"@angular/platform-browser": "~20.0.0",
"@angular/platform-browser-dynamic": "~20.0.0",
"@angular/router": "~20.0.0",
"@mmstack/primitives": "^20.0.1",
"@mmstack/translate": "^20.0.1",
"rxjs": "~7.8.0",
"zone.js": "~0.15.0"
},
@ -2685,6 +2687,84 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.1",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/icu-skeleton-parser": "1.8.14",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.6.tgz",
"integrity": "sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"intl-messageformat": "10.7.16",
"tslib": "^2.8.0"
},
"peerDependencies": {
"typescript": "^5.6.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"dev": true,
@ -3693,6 +3773,42 @@
"win32"
]
},
"node_modules/@mmstack/object": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/@mmstack/object/-/object-20.0.0.tgz",
"integrity": "sha512-cLhbkhU+S6jNm+7RNJ8DxvprBtX45pE5ymw58Pj3PoB+UOCA7pZPBDiubQQWqKBpoQJO0X/LyT/uTDV1YKs1Jg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
}
},
"node_modules/@mmstack/primitives": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@mmstack/primitives/-/primitives-20.0.1.tgz",
"integrity": "sha512-WAzQS+uf18Fjuz3HkWUnvJaB2nFt78/xV5f7vah9iR3DPe3IsWABngcEMNc1Wc+i4NHfytJQHfPlhSJXFMb3Gw==",
"license": "MIT",
"dependencies": {
"@mmstack/object": "^20.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "~20.0.3",
"@angular/core": "~20.0.3"
}
},
"node_modules/@mmstack/translate": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@mmstack/translate/-/translate-20.0.1.tgz",
"integrity": "sha512-kFKRedsLLcU5GdYcJBhmT6vzPYVQMFRKlrVbaP/gawk+U56i9ZWK58o1CWYggf5WslTsBa98fNnEJyu2/3trYg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": "~20.0.3",
"@formatjs/intl": "~3.1.6"
}
},
"node_modules/@modern-js/node-bundle-require": {
"version": "2.67.6",
"dev": true,
@ -8722,7 +8838,6 @@
},
"node_modules/decimal.js": {
"version": "10.6.0",
"dev": true,
"license": "MIT"
},
"node_modules/dedent": {
@ -11162,6 +11277,19 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/intl-messageformat": {
"version": "10.7.16",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"tslib": "^2.8.0"
}
},
"node_modules/ip-address": {
"version": "9.0.5",
"dev": true,
@ -17589,7 +17717,7 @@
},
"node_modules/typescript": {
"version": "5.8.3",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@ -19,6 +19,8 @@
"@angular/platform-browser": "~20.0.0",
"@angular/platform-browser-dynamic": "~20.0.0",
"@angular/router": "~20.0.0",
"@mmstack/primitives": "^20.0.1",
"@mmstack/translate": "^20.0.1",
"rxjs": "~7.8.0",
"zone.js": "~0.15.0"
},

View File

@ -21,5 +21,6 @@ export const appConfig: ApplicationConfig = {
provide: API,
useValue: apiUrls,
},
// No need to provide ThemeService and LanguageService as they use providedIn: 'root'
],
};

View File

@ -7,8 +7,19 @@ import { NavbarComponent } from '@org/shared/navbar';
selector: 'app-root',
template: `
<lib-navbar />
<div class="content-container">
<router-outlet />
</div>
`,
styles: [``],
styles: [
`
.content-container {
padding: 1.5rem;
max-width: 1200px;
margin: 0 auto;
min-height: calc(100vh - 64px);
}
`,
],
})
export class App { }
export class App {}

View File

@ -22,21 +22,6 @@ body.dark-theme {
transition: background-color 0.3s ease, color 0.3s ease;
}
.toast-success {
background-color: #4caf50;
color: white;
}
.toast-error {
background-color: #f44336;
color: white;
}
.toast-info {
background-color: #2196f3;
color: white;
}
body.dark-theme .mat-mdc-card {
--mdc-elevated-card-container-color: #424242;
color: rgba(255, 255, 255, 0.87);

View File

@ -9,8 +9,10 @@
"baseUrl": ".",
"paths": {
"@org/shared/api": ["libs/shared/api/src/index.ts"],
"@org/shared/i18n": ["libs/shared/i18n/src/index.ts"],
"@org/shared/navbar": ["libs/shared/navbar/src/index.ts"],
"@org/shared/stores": ["libs/shared/stores/src/index.ts"],
"@org/shared/theme": ["libs/shared/theme/src/index.ts"],
"@org/shared/toast": ["libs/shared/toast/src/index.ts"],
"@org/web/tasks": ["libs/web/tasks/src/index.ts"],
"@org/web/users": ["libs/web/users/src/index.ts"]