diff --git a/client/libs/shared/i18n/README.md b/client/libs/shared/i18n/README.md new file mode 100644 index 0000000..e8ff393 --- /dev/null +++ b/client/libs/shared/i18n/README.md @@ -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. diff --git a/client/libs/shared/i18n/eslint.config.mjs b/client/libs/shared/i18n/eslint.config.mjs new file mode 100644 index 0000000..a84ed8c --- /dev/null +++ b/client/libs/shared/i18n/eslint.config.mjs @@ -0,0 +1,5 @@ +/* eslint-disable */ +export default { + name: 'i18n', + extends: ['../../../eslint.base.config.mjs'], +}; diff --git a/client/libs/shared/i18n/jest.config.ts b/client/libs/shared/i18n/jest.config.ts new file mode 100644 index 0000000..9bcce95 --- /dev/null +++ b/client/libs/shared/i18n/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'i18n', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/shared/i18n', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/client/libs/shared/i18n/project.json b/client/libs/shared/i18n/project.json new file mode 100644 index 0000000..03c2744 --- /dev/null +++ b/client/libs/shared/i18n/project.json @@ -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}"] + } + } +} diff --git a/client/libs/shared/i18n/src/index.ts b/client/libs/shared/i18n/src/index.ts new file mode 100644 index 0000000..962253d --- /dev/null +++ b/client/libs/shared/i18n/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/language.service'; +export * from './lib/language-selector.component'; diff --git a/client/libs/shared/i18n/src/lib/language-selector.component.spec.ts b/client/libs/shared/i18n/src/lib/language-selector.component.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/libs/shared/i18n/src/lib/language-selector.component.ts b/client/libs/shared/i18n/src/lib/language-selector.component.ts new file mode 100644 index 0000000..aeaca86 --- /dev/null +++ b/client/libs/shared/i18n/src/lib/language-selector.component.ts @@ -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: ` + + + + + + `, + 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); +} diff --git a/client/libs/shared/i18n/src/lib/language.service.spec.ts b/client/libs/shared/i18n/src/lib/language.service.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/libs/shared/i18n/src/lib/language.service.ts b/client/libs/shared/i18n/src/lib/language.service.ts new file mode 100644 index 0000000..cd38453 --- /dev/null +++ b/client/libs/shared/i18n/src/lib/language.service.ts @@ -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(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}`); + } +} diff --git a/client/libs/shared/i18n/tsconfig.json b/client/libs/shared/i18n/tsconfig.json new file mode 100644 index 0000000..25f7201 --- /dev/null +++ b/client/libs/shared/i18n/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/client/libs/shared/i18n/tsconfig.lib.json b/client/libs/shared/i18n/tsconfig.lib.json new file mode 100644 index 0000000..6154928 --- /dev/null +++ b/client/libs/shared/i18n/tsconfig.lib.json @@ -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" + ] +} diff --git a/client/libs/shared/i18n/tsconfig.spec.json b/client/libs/shared/i18n/tsconfig.spec.json new file mode 100644 index 0000000..f8bfbb6 --- /dev/null +++ b/client/libs/shared/i18n/tsconfig.spec.json @@ -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" + ] +} diff --git a/client/libs/shared/navbar/src/lib/navbar.component.ts b/client/libs/shared/navbar/src/lib/navbar.component.ts index 78e7d7f..dbc4fe7 100644 --- a/client/libs/shared/navbar/src/lib/navbar.component.ts +++ b/client/libs/shared/navbar/src/lib/navbar.component.ts @@ -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: ` @@ -15,8 +24,35 @@ import { RouterModule } from '@angular/router'; TaskManager - Users - Tasks + + + +
+ + + + + +
`, 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 +} diff --git a/client/libs/shared/theme/README.md b/client/libs/shared/theme/README.md new file mode 100644 index 0000000..5af9c88 --- /dev/null +++ b/client/libs/shared/theme/README.md @@ -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. diff --git a/client/libs/shared/theme/eslint.config.mjs b/client/libs/shared/theme/eslint.config.mjs new file mode 100644 index 0000000..d0432b1 --- /dev/null +++ b/client/libs/shared/theme/eslint.config.mjs @@ -0,0 +1,5 @@ +/* eslint-disable */ +export default { + name: 'theme', + extends: ['../../../eslint.base.config.mjs'], +}; diff --git a/client/libs/shared/theme/jest.config.ts b/client/libs/shared/theme/jest.config.ts new file mode 100644 index 0000000..7717729 --- /dev/null +++ b/client/libs/shared/theme/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'theme', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/shared/theme', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/client/libs/shared/theme/project.json b/client/libs/shared/theme/project.json new file mode 100644 index 0000000..3688f95 --- /dev/null +++ b/client/libs/shared/theme/project.json @@ -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}"] + } + } +} diff --git a/client/libs/shared/theme/src/index.ts b/client/libs/shared/theme/src/index.ts new file mode 100644 index 0000000..a742d88 --- /dev/null +++ b/client/libs/shared/theme/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/theme.service'; +export * from './lib/theme-toggle.component'; diff --git a/client/libs/shared/theme/src/lib/theme-toggle.component.ts b/client/libs/shared/theme/src/lib/theme-toggle.component.ts new file mode 100644 index 0000000..807b0fc --- /dev/null +++ b/client/libs/shared/theme/src/lib/theme-toggle.component.ts @@ -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: ` + + `, + 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); +} diff --git a/client/libs/shared/theme/src/lib/theme.service.ts b/client/libs/shared/theme/src/lib/theme.service.ts new file mode 100644 index 0000000..62a7f39 --- /dev/null +++ b/client/libs/shared/theme/src/lib/theme.service.ts @@ -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'); + } +} diff --git a/client/libs/shared/theme/tsconfig.json b/client/libs/shared/theme/tsconfig.json new file mode 100644 index 0000000..f24c32b --- /dev/null +++ b/client/libs/shared/theme/tsconfig.json @@ -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" + } + ] +} diff --git a/client/libs/shared/theme/tsconfig.lib.json b/client/libs/shared/theme/tsconfig.lib.json new file mode 100644 index 0000000..00a8aa8 --- /dev/null +++ b/client/libs/shared/theme/tsconfig.lib.json @@ -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"] +} diff --git a/client/libs/shared/theme/tsconfig.spec.json b/client/libs/shared/theme/tsconfig.spec.json new file mode 100644 index 0000000..ea7ad14 --- /dev/null +++ b/client/libs/shared/theme/tsconfig.spec.json @@ -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" + ] +} diff --git a/client/libs/web/tasks/src/lib/task-card.component.ts b/client/libs/web/tasks/src/lib/task-card.component.ts index 6bc0196..8e18c69 100644 --- a/client/libs/web/tasks/src/lib/task-card.component.ts +++ b/client/libs/web/tasks/src/lib/task-card.component.ts @@ -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 { diff --git a/client/libs/web/tasks/src/lib/task-comment.component.ts b/client/libs/web/tasks/src/lib/task-comment.component.ts index 5ad649e..043ef9b 100644 --- a/client/libs/web/tasks/src/lib/task-comment.component.ts +++ b/client/libs/web/tasks/src/lib/task-comment.component.ts @@ -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 { diff --git a/client/libs/web/tasks/src/lib/task-form.component.ts b/client/libs/web/tasks/src/lib/task-form.component.ts index 20dfb52..45fbbf0 100644 --- a/client/libs/web/tasks/src/lib/task-form.component.ts +++ b/client/libs/web/tasks/src/lib/task-form.component.ts @@ -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 { diff --git a/client/libs/web/tasks/src/lib/task-list.component.ts b/client/libs/web/tasks/src/lib/task-list.component.ts index 58ef7f7..34c1c61 100644 --- a/client/libs/web/tasks/src/lib/task-list.component.ts +++ b/client/libs/web/tasks/src/lib/task-list.component.ts @@ -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; } diff --git a/client/libs/web/users/src/lib/user-card.component.ts b/client/libs/web/users/src/lib/user-card.component.ts index e119fde..e2049d6 100644 --- a/client/libs/web/users/src/lib/user-card.component.ts +++ b/client/libs/web/users/src/lib/user-card.component.ts @@ -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; } diff --git a/client/libs/web/users/src/lib/user-details.component.ts b/client/libs/web/users/src/lib/user-details.component.ts index 8935ef7..dab7fd6 100644 --- a/client/libs/web/users/src/lib/user-details.component.ts +++ b/client/libs/web/users/src/lib/user-details.component.ts @@ -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 { diff --git a/client/libs/web/users/src/lib/user-list.component.ts b/client/libs/web/users/src/lib/user-list.component.ts index 3844ffd..b3bc740 100644 --- a/client/libs/web/users/src/lib/user-list.component.ts +++ b/client/libs/web/users/src/lib/user-list.component.ts @@ -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 { diff --git a/client/libs/web/users/src/lib/user-task-list.component.ts b/client/libs/web/users/src/lib/user-task-list.component.ts index 293a906..fd64041 100644 --- a/client/libs/web/users/src/lib/user-task-list.component.ts +++ b/client/libs/web/users/src/lib/user-task-list.component.ts @@ -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 { diff --git a/client/package-lock.json b/client/package-lock.json index 7229bd7..3e7a347 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index fc26731..ff9ca47 100644 --- a/client/package.json +++ b/client/package.json @@ -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" }, diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index bea083a..8ed3578 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -21,5 +21,6 @@ export const appConfig: ApplicationConfig = { provide: API, useValue: apiUrls, }, + // No need to provide ThemeService and LanguageService as they use providedIn: 'root' ], }; diff --git a/client/src/app/app.ts b/client/src/app/app.ts index c48caf1..be8fc71 100644 --- a/client/src/app/app.ts +++ b/client/src/app/app.ts @@ -7,8 +7,19 @@ import { NavbarComponent } from '@org/shared/navbar'; selector: 'app-root', template: ` - +
+ +
`, - styles: [``], + styles: [ + ` + .content-container { + padding: 1.5rem; + max-width: 1200px; + margin: 0 auto; + min-height: calc(100vh - 64px); + } + `, + ], }) -export class App { } +export class App {} diff --git a/client/src/styles.scss b/client/src/styles.scss index ae44edc..8bcce51 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -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); diff --git a/client/tsconfig.base.json b/client/tsconfig.base.json index 76f5bcf..7bdd942 100644 --- a/client/tsconfig.base.json +++ b/client/tsconfig.base.json @@ -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"]