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.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 { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { LanguageService } from './language.service';
|
||||
import { LocaleStore } from './language.service';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-language-selector',
|
||||
@ -19,10 +19,10 @@ import { LanguageService } from './language.service';
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
<mat-menu #languageMenu="matMenu">
|
||||
<button mat-menu-item (click)="languageService.setLanguage('en')">
|
||||
<button mat-menu-item (click)="languageService.setLocale('en')">
|
||||
<span>English</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="languageService.setLanguage('sl')">
|
||||
<button mat-menu-item (click)="languageService.setLocale('sl')">
|
||||
<span>Slovenian</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
@ -40,5 +40,5 @@ import { LanguageService } from './language.service';
|
||||
],
|
||||
})
|
||||
export class LanguageSelectorComponent {
|
||||
protected languageService = inject(LanguageService);
|
||||
protected languageService = inject(LocaleStore);
|
||||
}
|
||||
|
||||
@ -1,39 +1,32 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export type SupportedLanguage = 'en' | 'sl';
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LanguageService {
|
||||
private readonly LANGUAGE_KEY = 'selectedLanguage';
|
||||
export class LocaleStore {
|
||||
private readonly router = inject(Router);
|
||||
locale = signal('en');
|
||||
|
||||
currentLanguage = signal<SupportedLanguage>(this.#getInitialLanguage());
|
||||
|
||||
#getInitialLanguage(): SupportedLanguage {
|
||||
// Try to get language from localStorage
|
||||
const storedLang = localStorage.getItem(
|
||||
this.LANGUAGE_KEY
|
||||
) as SupportedLanguage | null;
|
||||
if (storedLang && (storedLang === 'en' || storedLang === 'sl')) {
|
||||
return storedLang;
|
||||
getCurrentLocale() {
|
||||
return this.locale();
|
||||
}
|
||||
|
||||
// Try to detect from browser settings
|
||||
const browserLang = navigator.language.substring(0, 2).toLowerCase();
|
||||
if (browserLang === 'sl') {
|
||||
return 'sl';
|
||||
setLocale(locale: string) {
|
||||
if (this.locale() === locale) return;
|
||||
|
||||
this.locale.set(locale);
|
||||
|
||||
const urlSegments = this.router.url.split('/');
|
||||
const routePath = urlSegments.slice(2).join('/');
|
||||
|
||||
const newUrl = `/${locale}/${routePath}`;
|
||||
|
||||
window.location.href = newUrl;
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
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 { MatIcon } from '@angular/material/icon';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import {
|
||||
AppTranslatePipe,
|
||||
LanguageSelectorComponent,
|
||||
LocaleStore,
|
||||
} from '@org/shared/i18n';
|
||||
import { ThemeToggleComponent } from '@org/shared/theme';
|
||||
import { LanguageSelectorComponent } from '@org/shared/i18n';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-navbar',
|
||||
@ -15,42 +24,40 @@ import { LanguageSelectorComponent } from '@org/shared/i18n';
|
||||
RouterModule,
|
||||
ThemeToggleComponent,
|
||||
LanguageSelectorComponent,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<mat-toolbar color="primary" class="navbar">
|
||||
<span class="logo">
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
<span class="brand">TaskManager</span>
|
||||
<span class="brand">{{ 'app.taskManager' | translate }}</span>
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
|
||||
<div class="nav-links">
|
||||
<a
|
||||
mat-button
|
||||
routerLink="/users"
|
||||
[routerLink]="usersLink()"
|
||||
routerLinkActive="active-link"
|
||||
class="nav-button"
|
||||
>
|
||||
<mat-icon>people</mat-icon>
|
||||
<span>Users</span>
|
||||
<span>{{ 'app.navbar.users' | translate }}</span>
|
||||
</a>
|
||||
<a
|
||||
mat-button
|
||||
routerLink="/tasks"
|
||||
[routerLink]="tasksLink()"
|
||||
routerLinkActive="active-link"
|
||||
class="nav-button"
|
||||
>
|
||||
<mat-icon>task</mat-icon>
|
||||
<span>Tasks</span>
|
||||
<span>{{ 'app.navbar.tasks' | translate }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<!-- Theme toggle button -->
|
||||
<lib-theme-toggle></lib-theme-toggle>
|
||||
|
||||
<!-- Language selector button -->
|
||||
<lib-language-selector></lib-language-selector>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
@ -92,19 +99,9 @@ import { LanguageSelectorComponent } from '@org/shared/i18n';
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.nav-button.active-link {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
background-color: var(--mat-sys-outline-variant);
|
||||
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;
|
||||
@ -114,11 +111,14 @@ import { LanguageSelectorComponent } from '@org/shared/i18n';
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.action-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
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 { MatSelectModule } from '@angular/material/select';
|
||||
import { TasksService, User, UsersService } from '@org/shared/api';
|
||||
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||
import { ToastService } from '@org/shared/toast';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-add-task-dialog',
|
||||
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">
|
||||
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
|
||||
<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>
|
||||
@for (user of users(); track user.id) {
|
||||
<mat-option [value]="user.id">{{ user.name }}</mat-option>
|
||||
@ -40,32 +43,32 @@ import { ToastService } from '@org/shared/toast';
|
||||
</mat-select>
|
||||
@if (taskForm.get('userId')?.hasError('required') &&
|
||||
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 appearance="outline" class="full-width">
|
||||
<mat-label>Title</mat-label>
|
||||
<mat-label>{{ 'app.taskTitle' | translate }}</mat-label>
|
||||
<input matInput formControlName="title" placeholder="Task title" />
|
||||
@if (taskForm.get('title')?.hasError('required') &&
|
||||
taskForm.get('title')?.touched) {
|
||||
<mat-error>Title is required</mat-error>
|
||||
<mat-error>{{ 'app.titleIsRequired' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Description</mat-label>
|
||||
<mat-label>{{ 'app.taskDescription' | translate }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="description"
|
||||
placeholder="Task description"
|
||||
placeholder="{{ 'app.noDescription' | translate }}"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-dialog-actions align="end" class="dialog-actions">
|
||||
<button mat-stroked-button mat-dialog-close type="button">
|
||||
Cancel
|
||||
{{ 'app.cancel' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
@ -74,7 +77,7 @@ import { ToastService } from '@org/shared/toast';
|
||||
[disabled]="taskForm.invalid"
|
||||
>
|
||||
<mat-icon>add_task</mat-icon>
|
||||
<span class="button-text">Add Task</span>
|
||||
<span class="button-text">{{ 'app.addTask' | translate }}</span>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@ -120,6 +123,7 @@ import { ToastService } from '@org/shared/toast';
|
||||
MatIconModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
|
||||
@ -13,6 +13,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
||||
import { AppTranslatePipe, injectAppT } from '@org/shared/i18n';
|
||||
import { TaskCommentComponent } from './task-comment.component';
|
||||
|
||||
@Component({
|
||||
@ -25,6 +26,7 @@ import { TaskCommentComponent } from './task-comment.component';
|
||||
MatBadgeModule,
|
||||
MatButtonModule,
|
||||
TaskCommentComponent,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
@ -69,7 +71,7 @@ import { TaskCommentComponent } from './task-comment.component';
|
||||
<div class="comments-section">
|
||||
<h3>
|
||||
<mat-icon>forum</mat-icon>
|
||||
Comments
|
||||
{{ 'app.comment' | translate : { count: taskComments().length } }}
|
||||
</h3>
|
||||
|
||||
<lib-task-comment
|
||||
@ -192,6 +194,7 @@ import { TaskCommentComponent } from './task-comment.component';
|
||||
],
|
||||
})
|
||||
export class TaskCardComponent {
|
||||
readonly t = injectAppT();
|
||||
readonly task = input.required<TaskResponse>();
|
||||
readonly users = input<User[]>([]);
|
||||
readonly comments = input<CommentResponse[]>([]);
|
||||
@ -205,7 +208,6 @@ export class TaskCardComponent {
|
||||
readonly taskComments = signal<CommentResponse[]>([]);
|
||||
|
||||
constructor() {
|
||||
// Update task comments whenever comments or task changes
|
||||
effect(() => {
|
||||
const taskId = this.task()?.tasks?.id;
|
||||
if (taskId && this.comments()) {
|
||||
@ -220,11 +222,11 @@ export class TaskCardComponent {
|
||||
|
||||
getUserName(userId: number): string {
|
||||
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 {
|
||||
if (!timestamp) return 'Unknown date';
|
||||
if (!timestamp) return this.t('app.unknownDate');
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
@ -232,17 +234,21 @@ export class TaskCardComponent {
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return (
|
||||
'Today at ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
);
|
||||
return this.t('app.todayAt', {
|
||||
time: date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
});
|
||||
} else if (diffDays === 1) {
|
||||
return (
|
||||
'Yesterday at ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
);
|
||||
return this.t('app.yesterdayAt', {
|
||||
time: date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
});
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
return this.t('app.daysAgo', { count: diffDays.toString() });
|
||||
} else {
|
||||
return date.toLocaleDateString([], {
|
||||
day: 'numeric',
|
||||
|
||||
@ -18,6 +18,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { CommentResponse, User } from '@org/shared/api';
|
||||
import { AppTranslatePipe, injectAppT } from '@org/shared/i18n';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-task-comment',
|
||||
@ -29,6 +30,7 @@ import { CommentResponse, User } from '@org/shared/api';
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
@ -47,7 +49,7 @@ import { CommentResponse, User } from '@org/shared/api';
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="no-comments">No comments yet.</p>
|
||||
<p class="no-comments">{{ 'app.noComments' | translate }}</p>
|
||||
}
|
||||
|
||||
<form
|
||||
@ -55,35 +57,37 @@ import { CommentResponse, User } from '@org/shared/api';
|
||||
(ngSubmit)="submitComment()"
|
||||
class="comment-form"
|
||||
>
|
||||
<h4>Add a comment</h4>
|
||||
<h4>{{ 'app.addComment' | translate }}</h4>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Your comment</mat-label>
|
||||
<mat-label>{{ 'app.yourComment' | translate }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="content"
|
||||
placeholder="Share your thoughts..."
|
||||
placeholder="{{ 'app.shareYourThoughts' | translate }}"
|
||||
rows="2"
|
||||
></textarea>
|
||||
@if (commentForm.get('content')?.hasError('required') &&
|
||||
commentForm.get('content')?.touched) {
|
||||
<mat-error>Comment text is required</mat-error>
|
||||
<mat-error>{{ 'app.commentIsRequired' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Post as</mat-label>
|
||||
<mat-label>{{ 'app.postAs' | translate }}</mat-label>
|
||||
<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) {
|
||||
<option [value]="user.id">{{ user.name }}</option>
|
||||
}
|
||||
</select>
|
||||
@if (commentForm.get('userId')?.hasError('required') &&
|
||||
commentForm.get('userId')?.touched) {
|
||||
<mat-error>User is required</mat-error>
|
||||
<mat-error>{{ 'app.selectUser' | translate }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@ -98,7 +102,11 @@ import { CommentResponse, User } from '@org/shared/api';
|
||||
} @else {
|
||||
<mat-icon>send</mat-icon>
|
||||
}
|
||||
<span>{{ submitting() ? 'Posting...' : 'Post Comment' }}</span>
|
||||
<span>{{
|
||||
submitting()
|
||||
? ('app.posting' | translate)
|
||||
: ('app.postComment' | translate)
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -214,6 +222,7 @@ import { CommentResponse, User } from '@org/shared/api';
|
||||
],
|
||||
})
|
||||
export class TaskCommentComponent {
|
||||
readonly t = injectAppT();
|
||||
readonly taskId = input.required<number>();
|
||||
readonly comments = input<CommentResponse[]>([]);
|
||||
readonly users = input<User[]>([]);
|
||||
@ -240,7 +249,7 @@ export class TaskCommentComponent {
|
||||
}
|
||||
|
||||
formatDate(timestamp?: string): string {
|
||||
if (!timestamp) return 'Unknown date';
|
||||
if (!timestamp) return this.t('app.unknownDate');
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
@ -248,17 +257,21 @@ export class TaskCommentComponent {
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return (
|
||||
'Today at ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
);
|
||||
return this.t('app.todayAt', {
|
||||
time: date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
});
|
||||
} else if (diffDays === 1) {
|
||||
return (
|
||||
'Yesterday at ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
);
|
||||
return this.t('app.yesterdayAt', {
|
||||
time: date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
});
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
return this.t('app.daysAgo', { count: diffDays.toString() });
|
||||
} else {
|
||||
return date.toLocaleDateString([], {
|
||||
day: 'numeric',
|
||||
|
||||
@ -18,6 +18,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TasksService } from '@org/shared/api';
|
||||
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||
import { ToastService } from '@org/shared/toast';
|
||||
|
||||
@Component({
|
||||
@ -30,17 +31,20 @@ import { ToastService } from '@org/shared/toast';
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<mat-card class="task-form-card">
|
||||
<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-content>
|
||||
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
|
||||
<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" />
|
||||
@if (taskForm.get('title')?.hasError('required') &&
|
||||
taskForm.get('title')?.touched) {
|
||||
@ -49,7 +53,7 @@ import { ToastService } from '@org/shared/toast';
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Description</mat-label>
|
||||
<mat-label>{{ 'app.description' | translate }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="description"
|
||||
@ -66,7 +70,7 @@ import { ToastService } from '@org/shared/toast';
|
||||
[disabled]="taskForm.invalid"
|
||||
>
|
||||
<mat-icon>add_task</mat-icon>
|
||||
<span class="button-text">Add Task</span>
|
||||
<span class="button-text">{{ 'app.addTask' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
User,
|
||||
UsersService,
|
||||
} from '@org/shared/api';
|
||||
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||
import { ToastService } from '@org/shared/toast';
|
||||
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
||||
import { AddTaskDialogComponent } from './add-task-dialog.component';
|
||||
@ -36,12 +37,13 @@ import { TaskCardComponent } from './task-card.component';
|
||||
MatButtonModule,
|
||||
MatTooltipModule,
|
||||
TaskCardComponent,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="page-container">
|
||||
<mat-toolbar color="primary" class="page-header">
|
||||
<span>Tasks</span>
|
||||
<span>{{ 'app.navbar.tasks' | translate }}</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button matTooltip="Refresh data" (click)="loadData()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
@ -55,13 +57,12 @@ import { TaskCardComponent } from './task-card.component';
|
||||
@if (loading()) {
|
||||
<div class="loading-container">
|
||||
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
|
||||
<p>Loading tasks...</p>
|
||||
<p>{{ 'app.loading' | translate }}</p>
|
||||
</div>
|
||||
} @else if (tasks().length === 0) {
|
||||
<div class="empty-state">
|
||||
<mat-icon class="empty-icon">assignment</mat-icon>
|
||||
<h2>No Tasks Found</h2>
|
||||
<p>There are currently no tasks in the system.</p>
|
||||
<h2>{{ 'app.noTaskFound' | translate }}</h2>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="task-container">
|
||||
|
||||
@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
@ -9,6 +11,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppTranslatePipe, LocaleStore } from '@org/shared/i18n';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-user-card',
|
||||
@ -18,6 +21,7 @@ import { RouterModule } from '@angular/router';
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
@ -32,9 +36,7 @@ import { RouterModule } from '@angular/router';
|
||||
</button>
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<a [routerLink]="['/users', user().id]" class="user-link">{{
|
||||
user().name
|
||||
}}</a>
|
||||
<a [routerLink]="userLink()" class="user-link">{{ user().name }}</a>
|
||||
</mat-card-title>
|
||||
<mat-card-subtitle>{{ user().email }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
@ -42,11 +44,11 @@ import { RouterModule } from '@angular/router';
|
||||
<div class="user-meta">
|
||||
<span>
|
||||
<mat-icon class="meta-icon">assignment</mat-icon>
|
||||
{{ taskCount() }} Tasks
|
||||
{{ 'app.task' | translate : { count: taskCount() } }}
|
||||
</span>
|
||||
<span>
|
||||
<mat-icon class="meta-icon">comment</mat-icon>
|
||||
{{ commentCount() }} Comments
|
||||
{{ 'app.comment' | translate : { count: commentCount() } }}
|
||||
</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -98,8 +100,14 @@ import { RouterModule } from '@angular/router';
|
||||
],
|
||||
})
|
||||
export class UserCardComponent {
|
||||
private readonly localeStore = inject(LocaleStore);
|
||||
|
||||
readonly user = input.required<{ id: number; name: string; email: string }>();
|
||||
readonly taskCount = input<number>(0);
|
||||
readonly commentCount = input<number>(0);
|
||||
readonly deleteUser = output<number>();
|
||||
|
||||
readonly userLink = computed(
|
||||
() => `/${this.localeStore.locale()}/users/${this.user().id}`
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
User,
|
||||
UsersService,
|
||||
} from '@org/shared/api';
|
||||
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||
import { UserTaskListComponent } from './user-task-list.component';
|
||||
|
||||
@Component({
|
||||
@ -31,11 +32,12 @@ import { UserTaskListComponent } from './user-task-list.component';
|
||||
MatProgressSpinnerModule,
|
||||
MatToolbarModule,
|
||||
UserTaskListComponent,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<mat-toolbar color="primary">
|
||||
<span>User Details</span>
|
||||
<span>{{ 'app.userDetails' | translate }}</span>
|
||||
</mat-toolbar>
|
||||
|
||||
@if (loading()) {
|
||||
@ -57,8 +59,14 @@ import { UserTaskListComponent } from './user-task-list.component';
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p><strong>User ID:</strong> {{ user()?.id }}</p>
|
||||
<p><strong>Tasks:</strong> {{ userTasks().length }}</p>
|
||||
<p>
|
||||
<strong>{{ 'app.userId' | translate }}:</strong> {{ user()?.id }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{
|
||||
'app.task' | translate : { count: userTasks().length }
|
||||
}}</strong>
|
||||
</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button
|
||||
@ -68,7 +76,7 @@ import { UserTaskListComponent } from './user-task-list.component';
|
||||
class="back-button"
|
||||
>
|
||||
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
||||
<span>Back to Users</span>
|
||||
<span>{{ 'app.backToUser' | translate }}</span>
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
@ -86,7 +94,7 @@ import { UserTaskListComponent } from './user-task-list.component';
|
||||
aria-hidden="true"
|
||||
>error_outline</mat-icon
|
||||
>
|
||||
<p>User not found</p>
|
||||
<p>{{ 'app.userNotFound' | translate }}</p>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
@ -94,7 +102,7 @@ import { UserTaskListComponent } from './user-task-list.component';
|
||||
class="back-button"
|
||||
>
|
||||
<mat-icon aria-hidden="true">arrow_back</mat-icon>
|
||||
<span>Back to Users</span>
|
||||
<span> {{ 'app.backToUser' | translate }} </span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
||||
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||
import {
|
||||
CommentStoreService,
|
||||
TasksStoreService,
|
||||
@ -43,23 +44,24 @@ type UserWithCounts = User & { taskCount: number; commentCount: number };
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
UserCardComponent,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="container">
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Search Users</mat-label>
|
||||
<mat-label>{{ 'app.searchUser.title' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="searchControl"
|
||||
placeholder="type name or email"
|
||||
placeholder="{{ 'app.searchUser.placeholder' | translate }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@ -8,17 +8,24 @@ import {
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { TaskResponse } from '@org/shared/api';
|
||||
import { AppTranslatePipe } from '@org/shared/i18n';
|
||||
import { TaskFormComponent } from '@org/web/tasks';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-user-task-list',
|
||||
imports: [CommonModule, MatIconModule, MatListModule, TaskFormComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
MatListModule,
|
||||
TaskFormComponent,
|
||||
AppTranslatePipe,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="tasks-section">
|
||||
<h2>
|
||||
<mat-icon aria-hidden="true">assignment</mat-icon>
|
||||
<span>User Tasks</span>
|
||||
<span>{{ 'app.userTaskList' | translate }}</span>
|
||||
</h2>
|
||||
|
||||
@if (tasks().length) {
|
||||
@ -32,7 +39,8 @@ import { TaskFormComponent } from '@org/web/tasks';
|
||||
<h3 class="task-title">{{ task.tasks.title }}</h3>
|
||||
</div>
|
||||
<div matListItemLine class="task-description">
|
||||
{{ task.tasks.description || 'No description' }}
|
||||
{{ task.tasks.description }} ||
|
||||
{{ 'app.noDescription' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
}
|
||||
@ -40,7 +48,7 @@ import { TaskFormComponent } from '@org/web/tasks';
|
||||
} @else {
|
||||
<div class="no-tasks">
|
||||
<mat-icon aria-hidden="true">assignment_late</mat-icon>
|
||||
<p>No tasks found for this user.</p>
|
||||
<p>{{ 'app.noTasksFound' | translate }}</p>
|
||||
</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-dynamic": "~20.0.0",
|
||||
"@angular/router": "~20.0.0",
|
||||
"@formatjs/intl": "^3.1.6",
|
||||
"@mmstack/primitives": "^20.0.1",
|
||||
"@mmstack/translate": "^20.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
@ -2692,7 +2693,6 @@
|
||||
"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",
|
||||
@ -2705,7 +2705,6 @@
|
||||
"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"
|
||||
}
|
||||
@ -2715,7 +2714,6 @@
|
||||
"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",
|
||||
@ -2727,7 +2725,6 @@
|
||||
"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"
|
||||
@ -2738,7 +2735,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",
|
||||
@ -2760,7 +2756,6 @@
|
||||
"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"
|
||||
}
|
||||
@ -11282,7 +11277,6 @@
|
||||
"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",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"@angular/platform-browser": "~20.0.0",
|
||||
"@angular/platform-browser-dynamic": "~20.0.0",
|
||||
"@angular/router": "~20.0.0",
|
||||
"@formatjs/intl": "^3.1.6",
|
||||
"@mmstack/primitives": "^20.0.1",
|
||||
"@mmstack/translate": "^20.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideIntlConfig } from '@mmstack/translate';
|
||||
import { API, apiUrls } from '@org/shared/api';
|
||||
import { appRoutes } from './app.routes';
|
||||
|
||||
@ -21,6 +22,8 @@ export const appConfig: ApplicationConfig = {
|
||||
provide: API,
|
||||
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 { TaskListComponent } from '@org/web/tasks';
|
||||
import { UserDetailsComponent, UserListComponent } from '@org/web/users';
|
||||
import { inject, LOCALE_ID } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Route } from '@angular/router';
|
||||
import { LocaleStore, resolveAppTranslations } from '@org/shared/i18n';
|
||||
|
||||
export const appRoutes: Route[] = [
|
||||
{
|
||||
path: 'users',
|
||||
component: UserListComponent,
|
||||
path: ':locale',
|
||||
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',
|
||||
component: UserDetailsComponent,
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
component: TaskListComponent,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'users',
|
||||
path: '',
|
||||
redirectTo: '/en',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,25 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NavbarComponent } from '@org/shared/navbar';
|
||||
|
||||
@Component({
|
||||
imports: [RouterModule, NavbarComponent],
|
||||
imports: [RouterModule],
|
||||
selector: 'app-root',
|
||||
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);
|
||||
}
|
||||
`,
|
||||
],
|
||||
template: `<router-outlet />`,
|
||||
})
|
||||
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,
|
||||
"target": "es2022",
|
||||
"module": "preserve",
|
||||
"lib": ["es2020", "dom"],
|
||||
"lib": ["es2022", "dom"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@org/shared/api": ["libs/shared/api/src/index.ts"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user