locale
This commit is contained in:
parent
fd276b2c07
commit
58e37620e7
@ -1,2 +1,6 @@
|
|||||||
export * from './lib/language.service';
|
export * from './lib/app-sl.translation';
|
||||||
|
export * from './lib/app.namespace';
|
||||||
|
export * from './lib/app.t';
|
||||||
export * from './lib/language-selector.component';
|
export * from './lib/language-selector.component';
|
||||||
|
export * from './lib/language.service';
|
||||||
|
export * from './lib/translate.pipe';
|
||||||
|
|||||||
52
client/libs/shared/i18n/src/lib/app-sl.translation.ts
Normal file
52
client/libs/shared/i18n/src/lib/app-sl.translation.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { createAppTranslation } from './app.namespace';
|
||||||
|
|
||||||
|
export default createAppTranslation('sl', {
|
||||||
|
taskManager: 'Upravljanje nalog',
|
||||||
|
navbar: {
|
||||||
|
users: 'Uporabniki',
|
||||||
|
tasks: 'Naloge',
|
||||||
|
},
|
||||||
|
task: '{count, plural, =0 {Ni nalog} one {# naloga} two {# nalogi} other {# nalog}}',
|
||||||
|
comment:
|
||||||
|
'{count, plural, =0 {Ni komentarjev} one {# komentar} two {# komentarja} other {# komentarji}}',
|
||||||
|
searchUser: {
|
||||||
|
title: 'Iskanje uporabnikov',
|
||||||
|
placeholder: 'Vnesite ime ali e-pošto uporabnika',
|
||||||
|
},
|
||||||
|
addUser: 'Dodaj uporabnika',
|
||||||
|
userDetails: 'Podrobnosti uporabnika',
|
||||||
|
userId: 'ID uporabnika',
|
||||||
|
addNewTask: 'Dodaj novo nalogo',
|
||||||
|
title: 'Naslov',
|
||||||
|
description: 'Opis',
|
||||||
|
addTask: 'Dodaj nalogo',
|
||||||
|
userTaskList: 'Seznam nalog uporabnika',
|
||||||
|
backToUser: 'Nazaj na seznam uporabnikov',
|
||||||
|
userNotFound: 'Uporabnik ni najden',
|
||||||
|
loading: 'Nalaganje...',
|
||||||
|
titleIsRequired: 'Naslov je obvezen',
|
||||||
|
descriptionIsRequired: 'Opis je obvezen',
|
||||||
|
taskAdded: 'Naloga uspešno dodana',
|
||||||
|
taskDeleted: 'Naloga uspešno izbrisana',
|
||||||
|
userAdded: 'Uporabnik uspešno dodan',
|
||||||
|
noTasksFound: 'Ni najdenih nalog za tega uporabnika',
|
||||||
|
noTaskFound: 'Ni najdenih nalog',
|
||||||
|
noDescription: 'Brez opisa',
|
||||||
|
unknownUser: 'Neznan uporabnik',
|
||||||
|
unknownDate: 'Neznan datum',
|
||||||
|
todayAt: 'Danes ob {time}',
|
||||||
|
yesterdayAt: 'Včeraj ob {time}',
|
||||||
|
daysAgo: '{count} dni nazaj',
|
||||||
|
noComments: 'Še ni komentarjev',
|
||||||
|
addComment: 'Dodaj komentar',
|
||||||
|
yourComment: 'Vaš komentar',
|
||||||
|
shareYourThoughts: 'Delite svoje misli',
|
||||||
|
commentIsRequired: 'Komentar je obvezen',
|
||||||
|
postAs: 'Objavi kot',
|
||||||
|
selectUser: 'Izberite uporabnika',
|
||||||
|
postComment: 'Objavi komentar',
|
||||||
|
posting: 'Objavljanje...',
|
||||||
|
taskTitle: 'Naslov naloge',
|
||||||
|
taskDescription: 'Opis naloge',
|
||||||
|
cancel: 'Prekliči',
|
||||||
|
});
|
||||||
58
client/libs/shared/i18n/src/lib/app.namespace.ts
Normal file
58
client/libs/shared/i18n/src/lib/app.namespace.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { createNamespace } from '@mmstack/translate';
|
||||||
|
|
||||||
|
const ns = createNamespace('app', {
|
||||||
|
taskManager: 'Task Manager',
|
||||||
|
navbar: {
|
||||||
|
users: 'Users',
|
||||||
|
tasks: 'Tasks',
|
||||||
|
},
|
||||||
|
task: '{count, plural, =0 {No tasks} one {# task} other {# tasks}}',
|
||||||
|
comment:
|
||||||
|
'{count, plural, =0 {No comments} one {# comment} other {# comments}}',
|
||||||
|
searchUser: {
|
||||||
|
title: 'Search Users',
|
||||||
|
placeholder: 'Enter user name or email',
|
||||||
|
},
|
||||||
|
addUser: 'Add User',
|
||||||
|
userDetails: 'User Details',
|
||||||
|
userId: 'User Id',
|
||||||
|
addNewTask: 'Add New Task',
|
||||||
|
title: 'Title',
|
||||||
|
description: 'Description',
|
||||||
|
addTask: 'Add Task',
|
||||||
|
userTaskList: 'User Task List',
|
||||||
|
backToUser: 'Back to Users',
|
||||||
|
userNotFound: 'User not found',
|
||||||
|
loading: 'Loading...',
|
||||||
|
titleIsRequired: 'Title is required',
|
||||||
|
descriptionIsRequired: 'Description is required',
|
||||||
|
taskAdded: 'Task added successfully',
|
||||||
|
taskDeleted: 'Task deleted successfully',
|
||||||
|
userAdded: 'User added successfully',
|
||||||
|
noTasksFound: 'No tasks found for this user',
|
||||||
|
noTaskFound: 'No tasks found',
|
||||||
|
noDescription: 'No description',
|
||||||
|
unknownUser: 'Unknown User',
|
||||||
|
unknownDate: 'Unknown date',
|
||||||
|
todayAt: 'Today at {time}',
|
||||||
|
yesterdayAt: 'Yesterday at {time}',
|
||||||
|
daysAgo: '{count} days ago',
|
||||||
|
noComments: 'No comments yet',
|
||||||
|
addComment: 'Add Comment',
|
||||||
|
yourComment: 'Your comment',
|
||||||
|
shareYourThoughts: 'Share your thoughts',
|
||||||
|
commentIsRequired: 'Comment is required',
|
||||||
|
postAs: 'Post as',
|
||||||
|
selectUser: 'Select user',
|
||||||
|
postComment: 'Post Comment',
|
||||||
|
posting: 'Posting...',
|
||||||
|
taskTitle: 'Task Title',
|
||||||
|
taskDescription: 'Task Description',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ns.translation;
|
||||||
|
|
||||||
|
export type AppLocale = (typeof ns)['translation'];
|
||||||
|
|
||||||
|
export const createAppTranslation = ns.createTranslation;
|
||||||
12
client/libs/shared/i18n/src/lib/app.t.ts
Normal file
12
client/libs/shared/i18n/src/lib/app.t.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { registerNamespace } from '@mmstack/translate';
|
||||||
|
|
||||||
|
const r = registerNamespace(
|
||||||
|
() => import('./app.namespace').then((m) => m.default),
|
||||||
|
{
|
||||||
|
sl: () => import('./app-sl.translation').then((m) => m.default),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const injectAppT = r.injectNamespaceT;
|
||||||
|
export const resolveAppTranslations = r.resolveNamespaceTranslation;
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { LanguageService } from './language.service';
|
import { LocaleStore } from './language.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lib-language-selector',
|
selector: 'lib-language-selector',
|
||||||
@ -19,10 +19,10 @@ import { LanguageService } from './language.service';
|
|||||||
<mat-icon>translate</mat-icon>
|
<mat-icon>translate</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #languageMenu="matMenu">
|
<mat-menu #languageMenu="matMenu">
|
||||||
<button mat-menu-item (click)="languageService.setLanguage('en')">
|
<button mat-menu-item (click)="languageService.setLocale('en')">
|
||||||
<span>English</span>
|
<span>English</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="languageService.setLanguage('sl')">
|
<button mat-menu-item (click)="languageService.setLocale('sl')">
|
||||||
<span>Slovenian</span>
|
<span>Slovenian</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
@ -40,5 +40,5 @@ import { LanguageService } from './language.service';
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class LanguageSelectorComponent {
|
export class LanguageSelectorComponent {
|
||||||
protected languageService = inject(LanguageService);
|
protected languageService = inject(LocaleStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,32 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { inject, Injectable, signal } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
export type SupportedLanguage = 'en' | 'sl';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class LanguageService {
|
export class LocaleStore {
|
||||||
private readonly LANGUAGE_KEY = 'selectedLanguage';
|
private readonly router = inject(Router);
|
||||||
|
locale = signal('en');
|
||||||
|
|
||||||
currentLanguage = signal<SupportedLanguage>(this.#getInitialLanguage());
|
getCurrentLocale() {
|
||||||
|
return this.locale();
|
||||||
#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 {
|
setLocale(locale: string) {
|
||||||
this.currentLanguage.set(lang);
|
if (this.locale() === locale) return;
|
||||||
localStorage.setItem(this.LANGUAGE_KEY, lang);
|
|
||||||
// This is where you would typically trigger translation changes
|
this.locale.set(locale);
|
||||||
// in a real implementation
|
|
||||||
console.log(`Language set to: ${lang}`);
|
const urlSegments = this.router.url.split('/');
|
||||||
|
const routePath = urlSegments.slice(2).join('/');
|
||||||
|
|
||||||
|
const newUrl = `/${locale}/${routePath}`;
|
||||||
|
|
||||||
|
window.location.href = newUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLocale() {
|
||||||
|
const newLocale = this.locale() === 'en' ? 'sl' : 'en';
|
||||||
|
this.setLocale(newLocale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
client/libs/shared/i18n/src/lib/translate.pipe.ts
Normal file
9
client/libs/shared/i18n/src/lib/translate.pipe.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Pipe } from '@angular/core';
|
||||||
|
import { BaseTranslatePipe } from '@mmstack/translate';
|
||||||
|
import { type AppLocale } from './app.namespace';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'translate',
|
||||||
|
})
|
||||||
|
export class AppTranslatePipe extends BaseTranslatePipe<AppLocale> {}
|
||||||
|
|
||||||
@ -1,10 +1,19 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import {
|
||||||
|
AppTranslatePipe,
|
||||||
|
LanguageSelectorComponent,
|
||||||
|
LocaleStore,
|
||||||
|
} from '@org/shared/i18n';
|
||||||
import { ThemeToggleComponent } from '@org/shared/theme';
|
import { ThemeToggleComponent } from '@org/shared/theme';
|
||||||
import { LanguageSelectorComponent } from '@org/shared/i18n';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lib-navbar',
|
selector: 'lib-navbar',
|
||||||
@ -15,42 +24,40 @@ import { LanguageSelectorComponent } from '@org/shared/i18n';
|
|||||||
RouterModule,
|
RouterModule,
|
||||||
ThemeToggleComponent,
|
ThemeToggleComponent,
|
||||||
LanguageSelectorComponent,
|
LanguageSelectorComponent,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<mat-toolbar color="primary" class="navbar">
|
<mat-toolbar color="primary" class="navbar">
|
||||||
<span class="logo">
|
<span class="logo">
|
||||||
<mat-icon>dashboard</mat-icon>
|
<mat-icon>dashboard</mat-icon>
|
||||||
<span class="brand">TaskManager</span>
|
<span class="brand">{{ 'app.taskManager' | translate }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a
|
<a
|
||||||
mat-button
|
mat-button
|
||||||
routerLink="/users"
|
[routerLink]="usersLink()"
|
||||||
routerLinkActive="active-link"
|
routerLinkActive="active-link"
|
||||||
class="nav-button"
|
class="nav-button"
|
||||||
>
|
>
|
||||||
<mat-icon>people</mat-icon>
|
<mat-icon>people</mat-icon>
|
||||||
<span>Users</span>
|
<span>{{ 'app.navbar.users' | translate }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
mat-button
|
mat-button
|
||||||
routerLink="/tasks"
|
[routerLink]="tasksLink()"
|
||||||
routerLinkActive="active-link"
|
routerLinkActive="active-link"
|
||||||
class="nav-button"
|
class="nav-button"
|
||||||
>
|
>
|
||||||
<mat-icon>task</mat-icon>
|
<mat-icon>task</mat-icon>
|
||||||
<span>Tasks</span>
|
<span>{{ 'app.navbar.tasks' | translate }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<!-- Theme toggle button -->
|
|
||||||
<lib-theme-toggle></lib-theme-toggle>
|
<lib-theme-toggle></lib-theme-toggle>
|
||||||
|
|
||||||
<!-- Language selector button -->
|
|
||||||
<lib-language-selector></lib-language-selector>
|
<lib-language-selector></lib-language-selector>
|
||||||
</div>
|
</div>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
@ -92,19 +99,9 @@ import { LanguageSelectorComponent } from '@org/shared/i18n';
|
|||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
.nav-button.active-link {
|
.nav-button.active-link {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
background-color: var(--mat-sys-outline-variant);
|
||||||
position: relative;
|
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 {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@ -114,11 +111,14 @@ import { LanguageSelectorComponent } from '@org/shared/i18n';
|
|||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
.action-button:hover {
|
.action-button:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: var(--hover-color);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class NavbarComponent {
|
export class NavbarComponent {
|
||||||
// All functionality has been moved to separate components
|
readonly #localeStore = inject(LocaleStore);
|
||||||
|
|
||||||
|
readonly usersLink = computed(() => `/${this.#localeStore.locale()}/users`);
|
||||||
|
readonly tasksLink = computed(() => `/${this.#localeStore.locale()}/tasks`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,16 +23,19 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { TasksService, User, UsersService } from '@org/shared/api';
|
import { TasksService, User, UsersService } from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||||
import { ToastService } from '@org/shared/toast';
|
import { ToastService } from '@org/shared/toast';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lib-add-task-dialog',
|
selector: 'lib-add-task-dialog',
|
||||||
template: `
|
template: `
|
||||||
<h2 mat-dialog-title class="dialog-title">Add New Task</h2>
|
<h2 mat-dialog-title class="dialog-title">
|
||||||
|
{{ 'app.addNewTask' | translate }}
|
||||||
|
</h2>
|
||||||
<mat-dialog-content class="dialog-content">
|
<mat-dialog-content class="dialog-content">
|
||||||
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
|
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Assigned to</mat-label>
|
<mat-label>{{ 'app.selectUser' | translate }}</mat-label>
|
||||||
<mat-select formControlName="userId" required>
|
<mat-select formControlName="userId" required>
|
||||||
@for (user of users(); track user.id) {
|
@for (user of users(); track user.id) {
|
||||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||||
@ -40,32 +43,32 @@ import { ToastService } from '@org/shared/toast';
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
@if (taskForm.get('userId')?.hasError('required') &&
|
@if (taskForm.get('userId')?.hasError('required') &&
|
||||||
taskForm.get('userId')?.touched) {
|
taskForm.get('userId')?.touched) {
|
||||||
<mat-error>Please select a user</mat-error>
|
<mat-error>{{ 'app.userNotFound' | translate }}</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Title</mat-label>
|
<mat-label>{{ 'app.taskTitle' | translate }}</mat-label>
|
||||||
<input matInput formControlName="title" placeholder="Task title" />
|
<input matInput formControlName="title" placeholder="Task title" />
|
||||||
@if (taskForm.get('title')?.hasError('required') &&
|
@if (taskForm.get('title')?.hasError('required') &&
|
||||||
taskForm.get('title')?.touched) {
|
taskForm.get('title')?.touched) {
|
||||||
<mat-error>Title is required</mat-error>
|
<mat-error>{{ 'app.titleIsRequired' | translate }}</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Description</mat-label>
|
<mat-label>{{ 'app.taskDescription' | translate }}</mat-label>
|
||||||
<textarea
|
<textarea
|
||||||
matInput
|
matInput
|
||||||
formControlName="description"
|
formControlName="description"
|
||||||
placeholder="Task description"
|
placeholder="{{ 'app.noDescription' | translate }}"
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-dialog-actions align="end" class="dialog-actions">
|
<mat-dialog-actions align="end" class="dialog-actions">
|
||||||
<button mat-stroked-button mat-dialog-close type="button">
|
<button mat-stroked-button mat-dialog-close type="button">
|
||||||
Cancel
|
{{ 'app.cancel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
@ -74,7 +77,7 @@ import { ToastService } from '@org/shared/toast';
|
|||||||
[disabled]="taskForm.invalid"
|
[disabled]="taskForm.invalid"
|
||||||
>
|
>
|
||||||
<mat-icon>add_task</mat-icon>
|
<mat-icon>add_task</mat-icon>
|
||||||
<span class="button-text">Add Task</span>
|
<span class="button-text">{{ 'app.addTask' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
</form>
|
</form>
|
||||||
@ -120,6 +123,7 @@ import { ToastService } from '@org/shared/toast';
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
|||||||
import { MatExpansionModule } from '@angular/material/expansion';
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe, injectAppT } from '@org/shared/i18n';
|
||||||
import { TaskCommentComponent } from './task-comment.component';
|
import { TaskCommentComponent } from './task-comment.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -25,6 +26,7 @@ import { TaskCommentComponent } from './task-comment.component';
|
|||||||
MatBadgeModule,
|
MatBadgeModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
TaskCommentComponent,
|
TaskCommentComponent,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
@ -69,7 +71,7 @@ import { TaskCommentComponent } from './task-comment.component';
|
|||||||
<div class="comments-section">
|
<div class="comments-section">
|
||||||
<h3>
|
<h3>
|
||||||
<mat-icon>forum</mat-icon>
|
<mat-icon>forum</mat-icon>
|
||||||
Comments
|
{{ 'app.comment' | translate : { count: taskComments().length } }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<lib-task-comment
|
<lib-task-comment
|
||||||
@ -192,6 +194,7 @@ import { TaskCommentComponent } from './task-comment.component';
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TaskCardComponent {
|
export class TaskCardComponent {
|
||||||
|
readonly t = injectAppT();
|
||||||
readonly task = input.required<TaskResponse>();
|
readonly task = input.required<TaskResponse>();
|
||||||
readonly users = input<User[]>([]);
|
readonly users = input<User[]>([]);
|
||||||
readonly comments = input<CommentResponse[]>([]);
|
readonly comments = input<CommentResponse[]>([]);
|
||||||
@ -205,7 +208,6 @@ export class TaskCardComponent {
|
|||||||
readonly taskComments = signal<CommentResponse[]>([]);
|
readonly taskComments = signal<CommentResponse[]>([]);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Update task comments whenever comments or task changes
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const taskId = this.task()?.tasks?.id;
|
const taskId = this.task()?.tasks?.id;
|
||||||
if (taskId && this.comments()) {
|
if (taskId && this.comments()) {
|
||||||
@ -220,11 +222,11 @@ export class TaskCardComponent {
|
|||||||
|
|
||||||
getUserName(userId: number): string {
|
getUserName(userId: number): string {
|
||||||
const user = this.users().find((u) => u.id === userId);
|
const user = this.users().find((u) => u.id === userId);
|
||||||
return user ? user.name : 'Unknown User';
|
return user ? user.name : this.t('app.unknownUser');
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDate(timestamp?: string): string {
|
formatDate(timestamp?: string): string {
|
||||||
if (!timestamp) return 'Unknown date';
|
if (!timestamp) return this.t('app.unknownDate');
|
||||||
|
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -232,17 +234,21 @@ export class TaskCardComponent {
|
|||||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (diffDays === 0) {
|
if (diffDays === 0) {
|
||||||
return (
|
return this.t('app.todayAt', {
|
||||||
'Today at ' +
|
time: date.toLocaleTimeString([], {
|
||||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
hour: '2-digit',
|
||||||
);
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else if (diffDays === 1) {
|
} else if (diffDays === 1) {
|
||||||
return (
|
return this.t('app.yesterdayAt', {
|
||||||
'Yesterday at ' +
|
time: date.toLocaleTimeString([], {
|
||||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
hour: '2-digit',
|
||||||
);
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else if (diffDays < 7) {
|
} else if (diffDays < 7) {
|
||||||
return `${diffDays} days ago`;
|
return this.t('app.daysAgo', { count: diffDays.toString() });
|
||||||
} else {
|
} else {
|
||||||
return date.toLocaleDateString([], {
|
return date.toLocaleDateString([], {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { CommentResponse, User } from '@org/shared/api';
|
import { CommentResponse, User } from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe, injectAppT } from '@org/shared/i18n';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lib-task-comment',
|
selector: 'lib-task-comment',
|
||||||
@ -29,6 +30,7 @@ import { CommentResponse, User } from '@org/shared/api';
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
@ -47,7 +49,7 @@ import { CommentResponse, User } from '@org/shared/api';
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="no-comments">No comments yet.</p>
|
<p class="no-comments">{{ 'app.noComments' | translate }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@ -55,35 +57,37 @@ import { CommentResponse, User } from '@org/shared/api';
|
|||||||
(ngSubmit)="submitComment()"
|
(ngSubmit)="submitComment()"
|
||||||
class="comment-form"
|
class="comment-form"
|
||||||
>
|
>
|
||||||
<h4>Add a comment</h4>
|
<h4>{{ 'app.addComment' | translate }}</h4>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Your comment</mat-label>
|
<mat-label>{{ 'app.yourComment' | translate }}</mat-label>
|
||||||
<textarea
|
<textarea
|
||||||
matInput
|
matInput
|
||||||
formControlName="content"
|
formControlName="content"
|
||||||
placeholder="Share your thoughts..."
|
placeholder="{{ 'app.shareYourThoughts' | translate }}"
|
||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
@if (commentForm.get('content')?.hasError('required') &&
|
@if (commentForm.get('content')?.hasError('required') &&
|
||||||
commentForm.get('content')?.touched) {
|
commentForm.get('content')?.touched) {
|
||||||
<mat-error>Comment text is required</mat-error>
|
<mat-error>{{ 'app.commentIsRequired' | translate }}</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Post as</mat-label>
|
<mat-label>{{ 'app.postAs' | translate }}</mat-label>
|
||||||
<select matNativeControl formControlName="userId" required>
|
<select matNativeControl formControlName="userId" required>
|
||||||
<option value="" disabled selected>Select user</option>
|
<option value="" disabled selected>
|
||||||
|
{{ 'app.selectUser' | translate }}
|
||||||
|
</option>
|
||||||
@for (user of users(); track user.id) {
|
@for (user of users(); track user.id) {
|
||||||
<option [value]="user.id">{{ user.name }}</option>
|
<option [value]="user.id">{{ user.name }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@if (commentForm.get('userId')?.hasError('required') &&
|
@if (commentForm.get('userId')?.hasError('required') &&
|
||||||
commentForm.get('userId')?.touched) {
|
commentForm.get('userId')?.touched) {
|
||||||
<mat-error>User is required</mat-error>
|
<mat-error>{{ 'app.selectUser' | translate }}</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@ -98,7 +102,11 @@ import { CommentResponse, User } from '@org/shared/api';
|
|||||||
} @else {
|
} @else {
|
||||||
<mat-icon>send</mat-icon>
|
<mat-icon>send</mat-icon>
|
||||||
}
|
}
|
||||||
<span>{{ submitting() ? 'Posting...' : 'Post Comment' }}</span>
|
<span>{{
|
||||||
|
submitting()
|
||||||
|
? ('app.posting' | translate)
|
||||||
|
: ('app.postComment' | translate)
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -214,6 +222,7 @@ import { CommentResponse, User } from '@org/shared/api';
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TaskCommentComponent {
|
export class TaskCommentComponent {
|
||||||
|
readonly t = injectAppT();
|
||||||
readonly taskId = input.required<number>();
|
readonly taskId = input.required<number>();
|
||||||
readonly comments = input<CommentResponse[]>([]);
|
readonly comments = input<CommentResponse[]>([]);
|
||||||
readonly users = input<User[]>([]);
|
readonly users = input<User[]>([]);
|
||||||
@ -240,7 +249,7 @@ export class TaskCommentComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatDate(timestamp?: string): string {
|
formatDate(timestamp?: string): string {
|
||||||
if (!timestamp) return 'Unknown date';
|
if (!timestamp) return this.t('app.unknownDate');
|
||||||
|
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -248,17 +257,21 @@ export class TaskCommentComponent {
|
|||||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (diffDays === 0) {
|
if (diffDays === 0) {
|
||||||
return (
|
return this.t('app.todayAt', {
|
||||||
'Today at ' +
|
time: date.toLocaleTimeString([], {
|
||||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
hour: '2-digit',
|
||||||
);
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else if (diffDays === 1) {
|
} else if (diffDays === 1) {
|
||||||
return (
|
return this.t('app.yesterdayAt', {
|
||||||
'Yesterday at ' +
|
time: date.toLocaleTimeString([], {
|
||||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
hour: '2-digit',
|
||||||
);
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else if (diffDays < 7) {
|
} else if (diffDays < 7) {
|
||||||
return `${diffDays} days ago`;
|
return this.t('app.daysAgo', { count: diffDays.toString() });
|
||||||
} else {
|
} else {
|
||||||
return date.toLocaleDateString([], {
|
return date.toLocaleDateString([], {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { TasksService } from '@org/shared/api';
|
import { TasksService } from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||||
import { ToastService } from '@org/shared/toast';
|
import { ToastService } from '@org/shared/toast';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -30,17 +31,20 @@ import { ToastService } from '@org/shared/toast';
|
|||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<mat-card class="task-form-card">
|
<mat-card class="task-form-card">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title class="form-title">Add New Task</mat-card-title>
|
<mat-card-title class="form-title">{{
|
||||||
|
'app.addNewTask' | translate
|
||||||
|
}}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
|
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Title</mat-label>
|
<mat-label>{{ 'app.title' | translate }}</mat-label>
|
||||||
<input matInput formControlName="title" placeholder="Task title" />
|
<input matInput formControlName="title" placeholder="Task title" />
|
||||||
@if (taskForm.get('title')?.hasError('required') &&
|
@if (taskForm.get('title')?.hasError('required') &&
|
||||||
taskForm.get('title')?.touched) {
|
taskForm.get('title')?.touched) {
|
||||||
@ -49,7 +53,7 @@ import { ToastService } from '@org/shared/toast';
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Description</mat-label>
|
<mat-label>{{ 'app.description' | translate }}</mat-label>
|
||||||
<textarea
|
<textarea
|
||||||
matInput
|
matInput
|
||||||
formControlName="description"
|
formControlName="description"
|
||||||
@ -66,7 +70,7 @@ import { ToastService } from '@org/shared/toast';
|
|||||||
[disabled]="taskForm.invalid"
|
[disabled]="taskForm.invalid"
|
||||||
>
|
>
|
||||||
<mat-icon>add_task</mat-icon>
|
<mat-icon>add_task</mat-icon>
|
||||||
<span class="button-text">Add Task</span>
|
<span class="button-text">{{ 'app.addTask' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
UsersService,
|
UsersService,
|
||||||
} from '@org/shared/api';
|
} from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||||
import { ToastService } from '@org/shared/toast';
|
import { ToastService } from '@org/shared/toast';
|
||||||
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
||||||
import { AddTaskDialogComponent } from './add-task-dialog.component';
|
import { AddTaskDialogComponent } from './add-task-dialog.component';
|
||||||
@ -36,12 +37,13 @@ import { TaskCardComponent } from './task-card.component';
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
TaskCardComponent,
|
TaskCardComponent,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<mat-toolbar color="primary" class="page-header">
|
<mat-toolbar color="primary" class="page-header">
|
||||||
<span>Tasks</span>
|
<span>{{ 'app.navbar.tasks' | translate }}</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button mat-icon-button matTooltip="Refresh data" (click)="loadData()">
|
<button mat-icon-button matTooltip="Refresh data" (click)="loadData()">
|
||||||
<mat-icon>refresh</mat-icon>
|
<mat-icon>refresh</mat-icon>
|
||||||
@ -55,13 +57,12 @@ import { TaskCardComponent } from './task-card.component';
|
|||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
|
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
|
||||||
<p>Loading tasks...</p>
|
<p>{{ 'app.loading' | translate }}</p>
|
||||||
</div>
|
</div>
|
||||||
} @else if (tasks().length === 0) {
|
} @else if (tasks().length === 0) {
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<mat-icon class="empty-icon">assignment</mat-icon>
|
<mat-icon class="empty-icon">assignment</mat-icon>
|
||||||
<h2>No Tasks Found</h2>
|
<h2>{{ 'app.noTaskFound' | translate }}</h2>
|
||||||
<p>There are currently no tasks in the system.</p>
|
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="task-container">
|
<div class="task-container">
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common';
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@ -9,6 +11,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppTranslatePipe, LocaleStore } from '@org/shared/i18n';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lib-user-card',
|
selector: 'lib-user-card',
|
||||||
@ -18,6 +21,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
@ -32,9 +36,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
</button>
|
</button>
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>
|
<mat-card-title>
|
||||||
<a [routerLink]="['/users', user().id]" class="user-link">{{
|
<a [routerLink]="userLink()" class="user-link">{{ user().name }}</a>
|
||||||
user().name
|
|
||||||
}}</a>
|
|
||||||
</mat-card-title>
|
</mat-card-title>
|
||||||
<mat-card-subtitle>{{ user().email }}</mat-card-subtitle>
|
<mat-card-subtitle>{{ user().email }}</mat-card-subtitle>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
@ -42,11 +44,11 @@ import { RouterModule } from '@angular/router';
|
|||||||
<div class="user-meta">
|
<div class="user-meta">
|
||||||
<span>
|
<span>
|
||||||
<mat-icon class="meta-icon">assignment</mat-icon>
|
<mat-icon class="meta-icon">assignment</mat-icon>
|
||||||
{{ taskCount() }} Tasks
|
{{ 'app.task' | translate : { count: taskCount() } }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<mat-icon class="meta-icon">comment</mat-icon>
|
<mat-icon class="meta-icon">comment</mat-icon>
|
||||||
{{ commentCount() }} Comments
|
{{ 'app.comment' | translate : { count: commentCount() } }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
@ -98,8 +100,14 @@ import { RouterModule } from '@angular/router';
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UserCardComponent {
|
export class UserCardComponent {
|
||||||
|
private readonly localeStore = inject(LocaleStore);
|
||||||
|
|
||||||
readonly user = input.required<{ id: number; name: string; email: string }>();
|
readonly user = input.required<{ id: number; name: string; email: string }>();
|
||||||
readonly taskCount = input<number>(0);
|
readonly taskCount = input<number>(0);
|
||||||
readonly commentCount = input<number>(0);
|
readonly commentCount = input<number>(0);
|
||||||
readonly deleteUser = output<number>();
|
readonly deleteUser = output<number>();
|
||||||
|
|
||||||
|
readonly userLink = computed(
|
||||||
|
() => `/${this.localeStore.locale()}/users/${this.user().id}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
UsersService,
|
UsersService,
|
||||||
} from '@org/shared/api';
|
} from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||||
import { UserTaskListComponent } from './user-task-list.component';
|
import { UserTaskListComponent } from './user-task-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -31,11 +32,12 @@ import { UserTaskListComponent } from './user-task-list.component';
|
|||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
UserTaskListComponent,
|
UserTaskListComponent,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<mat-toolbar color="primary">
|
<mat-toolbar color="primary">
|
||||||
<span>User Details</span>
|
<span>{{ 'app.userDetails' | translate }}</span>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
@ -57,8 +59,14 @@ import { UserTaskListComponent } from './user-task-list.component';
|
|||||||
</div>
|
</div>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p><strong>User ID:</strong> {{ user()?.id }}</p>
|
<p>
|
||||||
<p><strong>Tasks:</strong> {{ userTasks().length }}</p>
|
<strong>{{ 'app.userId' | translate }}:</strong> {{ user()?.id }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{{
|
||||||
|
'app.task' | translate : { count: userTasks().length }
|
||||||
|
}}</strong>
|
||||||
|
</p>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions>
|
<mat-card-actions>
|
||||||
<button
|
<button
|
||||||
@ -68,7 +76,7 @@ import { UserTaskListComponent } from './user-task-list.component';
|
|||||||
class="back-button"
|
class="back-button"
|
||||||
>
|
>
|
||||||
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
||||||
<span>Back to Users</span>
|
<span>{{ 'app.backToUser' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-card-actions>
|
</mat-card-actions>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -86,7 +94,7 @@ import { UserTaskListComponent } from './user-task-list.component';
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>error_outline</mat-icon
|
>error_outline</mat-icon
|
||||||
>
|
>
|
||||||
<p>User not found</p>
|
<p>{{ 'app.userNotFound' | translate }}</p>
|
||||||
<button
|
<button
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -94,7 +102,7 @@ import { UserTaskListComponent } from './user-task-list.component';
|
|||||||
class="back-button"
|
class="back-button"
|
||||||
>
|
>
|
||||||
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
||||||
<span>Back to Users</span>
|
<span> {{ 'app.backToUser' | translate }} </span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||||
import {
|
import {
|
||||||
CommentStoreService,
|
CommentStoreService,
|
||||||
TasksStoreService,
|
TasksStoreService,
|
||||||
@ -43,23 +44,24 @@ type UserWithCounts = User & { taskCount: number; commentCount: number };
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
UserCardComponent,
|
UserCardComponent,
|
||||||
|
AppTranslatePipe,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<mat-form-field appearance="outline" class="full-width">
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
<mat-label>Search Users</mat-label>
|
<mat-label>{{ 'app.searchUser.title' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
[formControl]="searchControl"
|
[formControl]="searchControl"
|
||||||
placeholder="type name or email"
|
placeholder="{{ 'app.searchUser.placeholder' | translate }}"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="primary" (click)="openAddUserDialog()">
|
<button mat-raised-button color="primary" (click)="openAddUserDialog()">
|
||||||
<mat-icon>person_add</mat-icon> Add User
|
<mat-icon>person_add</mat-icon> {{ 'app.addUser' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -8,17 +8,24 @@ import {
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatListModule } from '@angular/material/list';
|
||||||
import { TaskResponse } from '@org/shared/api';
|
import { TaskResponse } from '@org/shared/api';
|
||||||
|
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||||
import { TaskFormComponent } from '@org/web/tasks';
|
import { TaskFormComponent } from '@org/web/tasks';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lib-user-task-list',
|
selector: 'lib-user-task-list',
|
||||||
imports: [CommonModule, MatIconModule, MatListModule, TaskFormComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatListModule,
|
||||||
|
TaskFormComponent,
|
||||||
|
AppTranslatePipe,
|
||||||
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="tasks-section">
|
<div class="tasks-section">
|
||||||
<h2>
|
<h2>
|
||||||
<mat-icon aria-hidden="true">assignment</mat-icon>
|
<mat-icon aria-hidden="true">assignment</mat-icon>
|
||||||
<span>User Tasks</span>
|
<span>{{ 'app.userTaskList' | translate }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@if (tasks().length) {
|
@if (tasks().length) {
|
||||||
@ -32,7 +39,8 @@ import { TaskFormComponent } from '@org/web/tasks';
|
|||||||
<h3 class="task-title">{{ task.tasks.title }}</h3>
|
<h3 class="task-title">{{ task.tasks.title }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div matListItemLine class="task-description">
|
<div matListItemLine class="task-description">
|
||||||
{{ task.tasks.description || 'No description' }}
|
{{ task.tasks.description }} ||
|
||||||
|
{{ 'app.noDescription' | translate }}
|
||||||
</div>
|
</div>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
}
|
}
|
||||||
@ -40,7 +48,7 @@ import { TaskFormComponent } from '@org/web/tasks';
|
|||||||
} @else {
|
} @else {
|
||||||
<div class="no-tasks">
|
<div class="no-tasks">
|
||||||
<mat-icon aria-hidden="true">assignment_late</mat-icon>
|
<mat-icon aria-hidden="true">assignment_late</mat-icon>
|
||||||
<p>No tasks found for this user.</p>
|
<p>{{ 'app.noTasksFound' | translate }}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
client/package-lock.json
generated
8
client/package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"@angular/platform-browser": "~20.0.0",
|
"@angular/platform-browser": "~20.0.0",
|
||||||
"@angular/platform-browser-dynamic": "~20.0.0",
|
"@angular/platform-browser-dynamic": "~20.0.0",
|
||||||
"@angular/router": "~20.0.0",
|
"@angular/router": "~20.0.0",
|
||||||
|
"@formatjs/intl": "^3.1.6",
|
||||||
"@mmstack/primitives": "^20.0.1",
|
"@mmstack/primitives": "^20.0.1",
|
||||||
"@mmstack/translate": "^20.0.1",
|
"@mmstack/translate": "^20.0.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
@ -2692,7 +2693,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||||
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "2.2.7",
|
"@formatjs/fast-memoize": "2.2.7",
|
||||||
"@formatjs/intl-localematcher": "0.6.1",
|
"@formatjs/intl-localematcher": "0.6.1",
|
||||||
@ -2705,7 +2705,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||||
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
@ -2715,7 +2714,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
||||||
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/ecma402-abstract": "2.3.4",
|
"@formatjs/ecma402-abstract": "2.3.4",
|
||||||
"@formatjs/icu-skeleton-parser": "1.8.14",
|
"@formatjs/icu-skeleton-parser": "1.8.14",
|
||||||
@ -2727,7 +2725,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
||||||
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/ecma402-abstract": "2.3.4",
|
"@formatjs/ecma402-abstract": "2.3.4",
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
@ -2738,7 +2735,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.6.tgz",
|
||||||
"integrity": "sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==",
|
"integrity": "sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/ecma402-abstract": "2.3.4",
|
"@formatjs/ecma402-abstract": "2.3.4",
|
||||||
"@formatjs/fast-memoize": "2.2.7",
|
"@formatjs/fast-memoize": "2.2.7",
|
||||||
@ -2760,7 +2756,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
||||||
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
@ -11282,7 +11277,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
||||||
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/ecma402-abstract": "2.3.4",
|
"@formatjs/ecma402-abstract": "2.3.4",
|
||||||
"@formatjs/fast-memoize": "2.2.7",
|
"@formatjs/fast-memoize": "2.2.7",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"@angular/platform-browser": "~20.0.0",
|
"@angular/platform-browser": "~20.0.0",
|
||||||
"@angular/platform-browser-dynamic": "~20.0.0",
|
"@angular/platform-browser-dynamic": "~20.0.0",
|
||||||
"@angular/router": "~20.0.0",
|
"@angular/router": "~20.0.0",
|
||||||
|
"@formatjs/intl": "^3.1.6",
|
||||||
"@mmstack/primitives": "^20.0.1",
|
"@mmstack/primitives": "^20.0.1",
|
||||||
"@mmstack/translate": "^20.0.1",
|
"@mmstack/translate": "^20.0.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { provideIntlConfig } from '@mmstack/translate';
|
||||||
import { API, apiUrls } from '@org/shared/api';
|
import { API, apiUrls } from '@org/shared/api';
|
||||||
import { appRoutes } from './app.routes';
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provide: API,
|
provide: API,
|
||||||
useValue: apiUrls,
|
useValue: apiUrls,
|
||||||
},
|
},
|
||||||
// No need to provide ThemeService and LanguageService as they use providedIn: 'root'
|
provideIntlConfig({
|
||||||
|
defaultLocale: 'en',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,23 +1,39 @@
|
|||||||
import { Route } from '@angular/router';
|
import { inject, LOCALE_ID } from '@angular/core';
|
||||||
import { TaskListComponent } from '@org/web/tasks';
|
import { ActivatedRouteSnapshot, Route } from '@angular/router';
|
||||||
import { UserDetailsComponent, UserListComponent } from '@org/web/users';
|
import { LocaleStore, resolveAppTranslations } from '@org/shared/i18n';
|
||||||
|
|
||||||
export const appRoutes: Route[] = [
|
export const appRoutes: Route[] = [
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: ':locale',
|
||||||
component: UserListComponent,
|
resolve: {
|
||||||
|
localeId: (route: ActivatedRouteSnapshot) => {
|
||||||
|
const store = inject(LocaleStore);
|
||||||
|
|
||||||
|
const locale = route.params['locale'] || 'en';
|
||||||
|
|
||||||
|
store.locale.set(locale);
|
||||||
|
|
||||||
|
return locale;
|
||||||
|
},
|
||||||
|
resolveAppTranslations,
|
||||||
|
},
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: LOCALE_ID,
|
||||||
|
useFactory: (store: LocaleStore) => {
|
||||||
|
return store.getCurrentLocale();
|
||||||
|
},
|
||||||
|
deps: [LocaleStore],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./locale-shell.component').then((m) => m.LocaleShellComponent),
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./locale-shell.routes').then((m) => m.shellRoutes),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'users/:id',
|
path: '',
|
||||||
component: UserDetailsComponent,
|
redirectTo: '/en',
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'tasks',
|
|
||||||
component: TaskListComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '**',
|
|
||||||
redirectTo: 'users',
|
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,25 +1,9 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NavbarComponent } from '@org/shared/navbar';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
imports: [RouterModule, NavbarComponent],
|
imports: [RouterModule],
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: `
|
template: `<router-outlet />`,
|
||||||
<lib-navbar />
|
|
||||||
<div class="content-container">
|
|
||||||
<router-outlet />
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
.content-container {
|
|
||||||
padding: 1.5rem;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class App {}
|
export class App {}
|
||||||
|
|||||||
25
client/src/app/locale-shell.component.ts
Normal file
25
client/src/app/locale-shell.component.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { NavbarComponent } from '@org/shared/navbar';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-locale-shell',
|
||||||
|
imports: [RouterModule, NavbarComponent],
|
||||||
|
template: `
|
||||||
|
<lib-navbar />
|
||||||
|
<div class="content-container">
|
||||||
|
<router-outlet />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
.content-container {
|
||||||
|
padding: 1.5rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LocaleShellComponent {}
|
||||||
24
client/src/app/locale-shell.routes.ts
Normal file
24
client/src/app/locale-shell.routes.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Route } from '@angular/router';
|
||||||
|
|
||||||
|
export const shellRoutes: Route[] = [
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@org/web/users').then((m) => m.UserListComponent),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users/:id',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@org/web/users').then((m) => m.UserDetailsComponent),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tasks',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('@org/web/tasks').then((m) => m.TaskListComponent),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: 'users',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -5,7 +5,7 @@
|
|||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"module": "preserve",
|
"module": "preserve",
|
||||||
"lib": ["es2020", "dom"],
|
"lib": ["es2022", "dom"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@org/shared/api": ["libs/shared/api/src/index.ts"],
|
"@org/shared/api": ["libs/shared/api/src/index.ts"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user