first commit
This commit is contained in:
commit
6166350e56
8
.vscode/mcp.json
vendored
Normal file
8
.vscode/mcp.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:9718/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
45
.vscode/settings.json
vendored
Normal file
45
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 2,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"eslint.options": {
|
||||
"extensions": [".js", ".ts", ".html"]
|
||||
},
|
||||
"eslint.validate": ["javascript", "typescript", "html"],
|
||||
"material-icon-theme.folders.associations": {
|
||||
"nav": "routes",
|
||||
"shell": "container",
|
||||
"ui": "components",
|
||||
"feature": "angular",
|
||||
"data-access": "api",
|
||||
"quote": "core"
|
||||
},
|
||||
"angular.enable-strict-mode-prompt": false,
|
||||
"search.exclude": {
|
||||
"": true,
|
||||
"**/node_modules": false
|
||||
},
|
||||
"search.useIgnoreFiles": false,
|
||||
"nxConsole.nxWorkspacePath": "client",
|
||||
"nxConsole.generateAiAgentRules": true
|
||||
}
|
||||
46
client/.gitignore
vendored
Normal file
46
client/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
dist
|
||||
tmp
|
||||
out-tsc
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.cursor/rules/nx-rules.mdc
|
||||
.github/instructions/nx.instructions.md
|
||||
|
||||
.angular
|
||||
6
client/.prettierignore
Normal file
6
client/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
||||
# Add files here to ignore them from prettier formatting
|
||||
/dist
|
||||
/coverage
|
||||
/.nx/cache
|
||||
/.nx/workspace-data
|
||||
.angular
|
||||
3
client/.prettierrc
Normal file
3
client/.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
8
client/.vscode/extensions.json
vendored
Normal file
8
client/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"nrwl.angular-console",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
||||
82
client/README.md
Normal file
82
client/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Org
|
||||
|
||||
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
||||
|
||||
✨ Your new, shiny [Nx workspace](https://nx.dev) is almost ready ✨.
|
||||
|
||||
[Learn more about this workspace setup and its capabilities](https://nx.dev/getting-started/tutorials/angular-standalone-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
|
||||
|
||||
## Finish your remote caching setup
|
||||
|
||||
[Click here to finish setting up your workspace!](https://cloud.nx.app/connect/mI3T4Te8D0)
|
||||
|
||||
|
||||
## Run tasks
|
||||
|
||||
To run the dev server for your app, use:
|
||||
|
||||
```sh
|
||||
npx nx serve org
|
||||
```
|
||||
|
||||
To create a production bundle:
|
||||
|
||||
```sh
|
||||
npx nx build org
|
||||
```
|
||||
|
||||
To see all available targets to run for a project, run:
|
||||
|
||||
```sh
|
||||
npx nx show project org
|
||||
```
|
||||
|
||||
These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
|
||||
|
||||
[More about running tasks in the docs »](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
## Add new projects
|
||||
|
||||
While you could add new projects to your workspace manually, you might want to leverage [Nx plugins](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) and their [code generation](https://nx.dev/features/generate-code?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) feature.
|
||||
|
||||
Use the plugin's generator to create new projects.
|
||||
|
||||
To generate a new application, use:
|
||||
|
||||
```sh
|
||||
npx nx g @nx/angular:app demo
|
||||
```
|
||||
|
||||
To generate a new library, use:
|
||||
|
||||
```sh
|
||||
npx nx g @nx/angular:lib mylib
|
||||
```
|
||||
|
||||
You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx list <plugin-name>` to learn about more specific capabilities of a particular plugin. Alternatively, [install Nx Console](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) to browse plugins and generators in your IDE.
|
||||
|
||||
[Learn more about Nx plugins »](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry »](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
|
||||
[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
## Install Nx Console
|
||||
|
||||
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
|
||||
|
||||
[Install Nx Console »](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
## Useful links
|
||||
|
||||
Learn more:
|
||||
|
||||
- [Learn more about this workspace setup](https://nx.dev/getting-started/tutorials/angular-standalone-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
|
||||
And join the Nx community:
|
||||
- [Discord](https://go.nx.dev/community)
|
||||
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
|
||||
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
|
||||
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||
42
client/eslint.base.config.mjs
Normal file
42
client/eslint.base.config.mjs
Normal file
@ -0,0 +1,42 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
|
||||
export default [
|
||||
...nx.configs['flat/base'],
|
||||
...nx.configs['flat/typescript'],
|
||||
...nx.configs['flat/javascript'],
|
||||
{
|
||||
ignores: ['**/dist'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: '*',
|
||||
onlyDependOnLibsWithTags: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
'**/*.cts',
|
||||
'**/*.mts',
|
||||
'**/*.js',
|
||||
'**/*.jsx',
|
||||
'**/*.cjs',
|
||||
'**/*.mjs',
|
||||
],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
51
client/eslint.config.mjs
Normal file
51
client/eslint.config.mjs
Normal file
@ -0,0 +1,51 @@
|
||||
import baseConfig from './eslint.base.config.mjs';
|
||||
import nx from '@nx/eslint-plugin';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
ignores: ['**/dist'],
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
'**/*.cts',
|
||||
'**/*.mts',
|
||||
'**/*.js',
|
||||
'**/*.jsx',
|
||||
'**/*.cjs',
|
||||
'**/*.mjs',
|
||||
],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'app',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'app',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
6
client/jest.config.ts
Normal file
6
client/jest.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { Config } from 'jest';
|
||||
import { getJestProjectsAsync } from '@nx/jest';
|
||||
|
||||
export default async (): Promise<Config> => ({
|
||||
projects: await getJestProjectsAsync(),
|
||||
});
|
||||
3
client/jest.preset.js
Normal file
3
client/jest.preset.js
Normal file
@ -0,0 +1,3 @@
|
||||
const nxPreset = require('@nx/jest/preset').default;
|
||||
|
||||
module.exports = { ...nxPreset };
|
||||
7
client/libs/shared/api/README.md
Normal file
7
client/libs/shared/api/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# api
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test api` to execute the unit tests.
|
||||
34
client/libs/shared/api/eslint.config.mjs
Normal file
34
client/libs/shared/api/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
client/libs/shared/api/jest.config.ts
Normal file
21
client/libs/shared/api/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'api',
|
||||
preset: '../../../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../../../coverage/src/app/libs/shared/api',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
client/libs/shared/api/project.json
Normal file
20
client/libs/shared/api/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "api",
|
||||
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "src/app/libs/shared/api/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "src/app/libs/shared/api/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
client/libs/shared/api/src/index.ts
Normal file
6
client/libs/shared/api/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './lib/token';
|
||||
export * from './lib/types';
|
||||
|
||||
export * from './lib/comments.service';
|
||||
export * from './lib/tasks.service';
|
||||
export * from './lib/users.service';
|
||||
24
client/libs/shared/api/src/lib/comments.service.ts
Normal file
24
client/libs/shared/api/src/lib/comments.service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { API } from './token';
|
||||
import { Comment, CommentResponse } from './types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CommentsService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly apiEndpoints = inject(API);
|
||||
|
||||
getComments(): Observable<CommentResponse[]> {
|
||||
return this.http.get<CommentResponse[]>(this.apiEndpoints.getComments);
|
||||
}
|
||||
|
||||
createComment(comment: Omit<Comment, 'id'>): Observable<CommentResponse> {
|
||||
return this.http.post<CommentResponse>(
|
||||
this.apiEndpoints.createComment,
|
||||
comment
|
||||
);
|
||||
}
|
||||
}
|
||||
21
client/libs/shared/api/src/lib/tasks.service.ts
Normal file
21
client/libs/shared/api/src/lib/tasks.service.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { API } from './token';
|
||||
import { Task, TaskResponse } from './types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TasksService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly apiEndpoints = inject(API);
|
||||
|
||||
getTasks(): Observable<TaskResponse[]> {
|
||||
return this.http.get<TaskResponse[]>(this.apiEndpoints.getTasks);
|
||||
}
|
||||
|
||||
createTask(task: Omit<Task, 'id'>): Observable<TaskResponse> {
|
||||
return this.http.post<TaskResponse>(this.apiEndpoints.createTask, task);
|
||||
}
|
||||
}
|
||||
17
client/libs/shared/api/src/lib/token.ts
Normal file
17
client/libs/shared/api/src/lib/token.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { APIS } from './types';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3000';
|
||||
|
||||
export const API = new InjectionToken<APIS>('API');
|
||||
|
||||
export const apiUrls: APIS = {
|
||||
getUsers: `${API_BASE_URL}/users`,
|
||||
getUserById: `${API_BASE_URL}/users`,
|
||||
createUser: `${API_BASE_URL}/users`,
|
||||
deleteUser: `${API_BASE_URL}/users`,
|
||||
getTasks: `${API_BASE_URL}/tasks`,
|
||||
createTask: `${API_BASE_URL}/tasks`,
|
||||
getComments: `${API_BASE_URL}/comments`,
|
||||
createComment: `${API_BASE_URL}/comments`,
|
||||
};
|
||||
48
client/libs/shared/api/src/lib/types.ts
Normal file
48
client/libs/shared/api/src/lib/types.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export type Task = {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
user_id: number;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export type TaskResponse = {
|
||||
tasks: Task;
|
||||
users: User;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
id: number;
|
||||
content: string;
|
||||
task_id: number;
|
||||
user_id: number;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export type CommentResponse = {
|
||||
comments: Comment;
|
||||
tasks: Task;
|
||||
users: User;
|
||||
};
|
||||
|
||||
export const API_KEYS = [
|
||||
'getUsers',
|
||||
'getUserById',
|
||||
'createUser',
|
||||
'deleteUser',
|
||||
'getTasks',
|
||||
'createTask',
|
||||
'getComments',
|
||||
'createComment',
|
||||
] as const;
|
||||
|
||||
export type ApiKey = (typeof API_KEYS)[number];
|
||||
|
||||
export type APIS = Record<ApiKey, string>;
|
||||
29
client/libs/shared/api/src/lib/users.service.ts
Normal file
29
client/libs/shared/api/src/lib/users.service.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { API } from './token';
|
||||
import { User } from './types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UsersService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly apiEndpoints = inject(API);
|
||||
|
||||
getUsers(): Observable<User[]> {
|
||||
return this.http.get<User[]>(this.apiEndpoints.getUsers);
|
||||
}
|
||||
|
||||
getUser(id: number): Observable<User> {
|
||||
return this.http.get<User>(`${this.apiEndpoints.getUserById}/${id}`);
|
||||
}
|
||||
|
||||
createUser(user: Omit<User, 'id'>): Observable<User> {
|
||||
return this.http.post<User>(this.apiEndpoints.createUser, user);
|
||||
}
|
||||
|
||||
deleteUser(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiEndpoints.deleteUser}/${id}`);
|
||||
}
|
||||
}
|
||||
6
client/libs/shared/api/src/test-setup.ts
Normal file
6
client/libs/shared/api/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
30
client/libs/shared/api/tsconfig.json
Normal file
30
client/libs/shared/api/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
client/libs/shared/api/tsconfig.lib.json
Normal file
16
client/libs/shared/api/tsconfig.lib.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
17
client/libs/shared/api/tsconfig.spec.json
Normal file
17
client/libs/shared/api/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
client/libs/shared/navbar/README.md
Normal file
7
client/libs/shared/navbar/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# navbar
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test navbar` to execute the unit tests.
|
||||
34
client/libs/shared/navbar/eslint.config.mjs
Normal file
34
client/libs/shared/navbar/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
client/libs/shared/navbar/jest.config.ts
Normal file
21
client/libs/shared/navbar/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'navbar',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/navbar',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
client/libs/shared/navbar/project.json
Normal file
20
client/libs/shared/navbar/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "navbar",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/navbar/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/navbar/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
client/libs/shared/navbar/src/index.ts
Normal file
1
client/libs/shared/navbar/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lib/navbar.component';
|
||||
53
client/libs/shared/navbar/src/lib/navbar.component.ts
Normal file
53
client/libs/shared/navbar/src/lib/navbar.component.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-navbar',
|
||||
imports: [MatToolbarModule, MatIcon],
|
||||
template: `
|
||||
<mat-toolbar color="primary" class="navbar">
|
||||
<span class="logo">
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
<span class="brand">TaskManager</span>
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
<a mat-button routerLink="/users" routerLinkActive="active-link">Users</a>
|
||||
<a mat-button routerLink="/tasks" routerLinkActive="active-link">Tasks</a>
|
||||
</mat-toolbar>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.navbar {
|
||||
padding: 0 2rem;
|
||||
min-height: 64px;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.brand {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
a[mat-button] {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a[mat-button]:hover, a[mat-button].active-link {
|
||||
background: rgba(255,255,255,0.15);
|
||||
color: #fff;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class NavbarComponent { }
|
||||
6
client/libs/shared/navbar/src/test-setup.ts
Normal file
6
client/libs/shared/navbar/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
30
client/libs/shared/navbar/tsconfig.json
Normal file
30
client/libs/shared/navbar/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
client/libs/shared/navbar/tsconfig.lib.json
Normal file
16
client/libs/shared/navbar/tsconfig.lib.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
17
client/libs/shared/navbar/tsconfig.spec.json
Normal file
17
client/libs/shared/navbar/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
client/libs/shared/stores/README.md
Normal file
7
client/libs/shared/stores/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# stores
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test stores` to execute the unit tests.
|
||||
34
client/libs/shared/stores/eslint.config.mjs
Normal file
34
client/libs/shared/stores/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
client/libs/shared/stores/jest.config.ts
Normal file
21
client/libs/shared/stores/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'stores',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/stores',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
client/libs/shared/stores/project.json
Normal file
20
client/libs/shared/stores/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "stores",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/stores/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/stores/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
client/libs/shared/stores/src/index.ts
Normal file
3
client/libs/shared/stores/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './lib/user.store.service';
|
||||
export * from './lib/tasks.store.service';
|
||||
export * from './lib/comments.store.service';
|
||||
24
client/libs/shared/stores/src/lib/comments.store.service.ts
Normal file
24
client/libs/shared/stores/src/lib/comments.store.service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { Comment, CommentResponse, CommentsService } from '@org/shared/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CommentStoreService {
|
||||
private readonly service = inject(CommentsService);
|
||||
comments = signal<CommentResponse[]>([]);
|
||||
|
||||
getComments() {
|
||||
this.service.getComments().subscribe({
|
||||
next: (comments) => this.comments.set(comments),
|
||||
error: (error) => console.error('Failed to load comments:', error),
|
||||
})
|
||||
}
|
||||
|
||||
createComment(comment: Omit<Comment, 'id'>) {
|
||||
this.service.createComment(comment).subscribe({
|
||||
next: (newComment) => this.comments.update((comments) => [...comments, newComment]),
|
||||
error: (error) => console.error('Failed to create comment:', error),
|
||||
});
|
||||
}
|
||||
};
|
||||
24
client/libs/shared/stores/src/lib/tasks.store.service.ts
Normal file
24
client/libs/shared/stores/src/lib/tasks.store.service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { Task, TasksService, TaskResponse } from '@org/shared/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TasksStoreService {
|
||||
private readonly service = inject(TasksService);
|
||||
tasks = signal<TaskResponse[]>([]);
|
||||
|
||||
getTasks() {
|
||||
this.service.getTasks().subscribe({
|
||||
next: (tasks) => this.tasks.set(tasks),
|
||||
error: (error) => console.error('Failed to load tasks:', error),
|
||||
})
|
||||
}
|
||||
|
||||
createTask(task: Omit<Task, 'id'>) {
|
||||
this.service.createTask(task).subscribe({
|
||||
next: (newTask) => this.tasks.update((tasks) => [...tasks, newTask]),
|
||||
error: (error) => console.error('Failed to create task:', error),
|
||||
});
|
||||
}
|
||||
};
|
||||
31
client/libs/shared/stores/src/lib/user.store.service.ts
Normal file
31
client/libs/shared/stores/src/lib/user.store.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { User, UsersService } from '@org/shared/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserStoreService {
|
||||
private readonly service = inject(UsersService);
|
||||
users = signal<User[]>([]);
|
||||
|
||||
getUsers() {
|
||||
this.service.getUsers().subscribe({
|
||||
next: (users) => this.users.set(users),
|
||||
error: (error) => console.error('Failed to load users:', error),
|
||||
});
|
||||
}
|
||||
|
||||
createUser(user: Omit<User, 'id'>) {
|
||||
this.service.createUser(user).subscribe({
|
||||
next: (newUser) => this.users.update((users) => [...users, newUser]),
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(id: number) {
|
||||
this.service.deleteUser(id).subscribe({
|
||||
next: () =>
|
||||
this.users.update((users) => users.filter((user) => user.id !== id)),
|
||||
error: (error) => console.error('Failed to delete user:', error),
|
||||
});
|
||||
}
|
||||
}
|
||||
6
client/libs/shared/stores/src/test-setup.ts
Normal file
6
client/libs/shared/stores/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
30
client/libs/shared/stores/tsconfig.json
Normal file
30
client/libs/shared/stores/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
client/libs/shared/stores/tsconfig.lib.json
Normal file
16
client/libs/shared/stores/tsconfig.lib.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
17
client/libs/shared/stores/tsconfig.spec.json
Normal file
17
client/libs/shared/stores/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
client/libs/shared/toast/README.md
Normal file
7
client/libs/shared/toast/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# toast
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test toast` to execute the unit tests.
|
||||
34
client/libs/shared/toast/eslint.config.mjs
Normal file
34
client/libs/shared/toast/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
client/libs/shared/toast/jest.config.ts
Normal file
21
client/libs/shared/toast/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'toast',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/toast',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
client/libs/shared/toast/project.json
Normal file
20
client/libs/shared/toast/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "toast",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/toast/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/toast/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
client/libs/shared/toast/src/index.ts
Normal file
1
client/libs/shared/toast/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lib/toast.service';
|
||||
20
client/libs/shared/toast/src/lib/toast.service.ts
Normal file
20
client/libs/shared/toast/src/lib/toast.service.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ToastService {
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
show(message: string, type: 'success' | 'error' | 'info' = 'success', duration: number = 3000) {
|
||||
const panelClass = `toast-${type}`;
|
||||
|
||||
this.snackBar.open(message, 'Close', {
|
||||
duration,
|
||||
horizontalPosition: 'end',
|
||||
verticalPosition: 'top',
|
||||
panelClass: [panelClass]
|
||||
});
|
||||
}
|
||||
}
|
||||
6
client/libs/shared/toast/src/test-setup.ts
Normal file
6
client/libs/shared/toast/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
30
client/libs/shared/toast/tsconfig.json
Normal file
30
client/libs/shared/toast/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
client/libs/shared/toast/tsconfig.lib.json
Normal file
17
client/libs/shared/toast/tsconfig.lib.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
17
client/libs/shared/toast/tsconfig.spec.json
Normal file
17
client/libs/shared/toast/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
client/libs/web/tasks/README.md
Normal file
7
client/libs/web/tasks/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# tasks
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test tasks` to execute the unit tests.
|
||||
34
client/libs/web/tasks/eslint.config.mjs
Normal file
34
client/libs/web/tasks/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
client/libs/web/tasks/jest.config.ts
Normal file
21
client/libs/web/tasks/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'tasks',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/web/tasks',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
client/libs/web/tasks/project.json
Normal file
20
client/libs/web/tasks/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "tasks",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/web/tasks/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/web/tasks/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/libs/web/tasks/src/index.ts
Normal file
2
client/libs/web/tasks/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './lib/task-list.component';
|
||||
export * from './lib/task-form.component';
|
||||
39
client/libs/web/tasks/src/lib/add-task-dialog.component.ts
Normal file
39
client/libs/web/tasks/src/lib/add-task-dialog.component.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogActions, MatDialogClose, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
|
||||
import { TaskFormComponent } from './task-form.component';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-add-task-dialog',
|
||||
standalone: true,
|
||||
template: `
|
||||
<h2 mat-dialog-title>Add New Task</h2>
|
||||
<mat-dialog-content>
|
||||
<lib-task-form (taskAdded)="onTaskAdded()"></lib-task-form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close type="button">Cancel</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
styles: [`
|
||||
mat-dialog-content {
|
||||
min-width: 320px;
|
||||
}
|
||||
`],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
TaskFormComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddTaskDialogComponent {
|
||||
readonly dialogRef = inject(MatDialogRef<AddTaskDialogComponent>);
|
||||
|
||||
onTaskAdded() {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
||||
111
client/libs/web/tasks/src/lib/task-form.component.ts
Normal file
111
client/libs/web/tasks/src/lib/task-form.component.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, Output, inject, input } from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TasksService } from '@org/shared/api';
|
||||
import { ToastService } from '@org/shared/toast';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-task-form',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
],
|
||||
template: `
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Add New Task</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>
|
||||
<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-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="description"
|
||||
placeholder="Task description"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="taskForm.invalid"
|
||||
>
|
||||
Add Task
|
||||
</button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class TaskFormComponent {
|
||||
userId = input<number>(0);
|
||||
@Output() taskAdded = new EventEmitter<void>();
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private tasksService = inject(TasksService);
|
||||
private toastService = inject(ToastService);
|
||||
|
||||
taskForm: FormGroup = this.fb.group({
|
||||
title: ['', Validators.required],
|
||||
description: [''],
|
||||
});
|
||||
|
||||
onSubmit() {
|
||||
if (this.taskForm.invalid) return;
|
||||
|
||||
const task = {
|
||||
title: this.taskForm.value.title,
|
||||
description: this.taskForm.value.description,
|
||||
user_id: this.userId(),
|
||||
};
|
||||
|
||||
this.tasksService.createTask(task).subscribe({
|
||||
next: () => {
|
||||
this.taskForm.reset();
|
||||
this.toastService.show('Task created successfully!');
|
||||
this.taskAdded.emit();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating task:', error);
|
||||
this.toastService.show('Failed to create task!', 'error');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
548
client/libs/web/tasks/src/lib/task-list.component.ts
Normal file
548
client/libs/web/tasks/src/lib/task-list.component.ts
Normal file
@ -0,0 +1,548 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AddTaskDialogComponent } from './add-task-dialog.component';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import {
|
||||
CommentResponse,
|
||||
CommentsService,
|
||||
TaskResponse,
|
||||
TasksService,
|
||||
User,
|
||||
UsersService,
|
||||
} from '@org/shared/api';
|
||||
import { ToastService } from '@org/shared/toast';
|
||||
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-task-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatCardModule,
|
||||
MatListModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatToolbarModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatExpansionModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDividerModule,
|
||||
MatBadgeModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
template: `
|
||||
<div class="page-container">
|
||||
<mat-toolbar color="primary" class="page-header">
|
||||
<span>Task Management</span>
|
||||
<span class="spacer"></span>
|
||||
<button mat-icon-button matTooltip="Refresh data" (click)="loadData()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
|
||||
<button class="fab" mat-fab color="accent" (click)="openAddTaskDialog()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="loading-container">
|
||||
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
|
||||
<p>Loading tasks...</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>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="task-container">
|
||||
@for (task of tasks(); track task.tasks.id) {
|
||||
<mat-expansion-panel class="task-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<div class="task-title-container">
|
||||
<mat-icon>assignment</mat-icon>
|
||||
<span>{{ task.tasks.title }}</span>
|
||||
@if (getTaskComments(task.tasks.id).length) {
|
||||
<mat-icon
|
||||
class="comment-badge"
|
||||
matBadge="{{ getTaskComments(task.tasks.id).length }}"
|
||||
matBadgeColor="accent"
|
||||
>
|
||||
comment
|
||||
</mat-icon>
|
||||
}
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<div class="task-info">
|
||||
<span class="task-author"
|
||||
>By {{ getUserName(task.tasks.user_id) }}</span
|
||||
>
|
||||
<span class="task-timestamp">{{
|
||||
formatDate(task.tasks.timestamp)
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="task-details">
|
||||
<p class="task-description">
|
||||
{{ task.tasks.description }}
|
||||
</p>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="comments-section">
|
||||
<h3>
|
||||
<mat-icon>forum</mat-icon>
|
||||
Comments
|
||||
</h3>
|
||||
|
||||
@if (getTaskComments(task.tasks.id).length) {
|
||||
<div class="comments-list">
|
||||
@for (comment of getTaskComments(task.tasks.id); track
|
||||
comment.comments.id) {
|
||||
<div class="comment-item">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author">{{ comment.users.name }}</span>
|
||||
<span class="comment-time">{{
|
||||
formatDate(comment.comments.timestamp)
|
||||
}}</span>
|
||||
</div>
|
||||
<p class="comment-content">{{ comment.comments.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="no-comments">No comments yet.</p>
|
||||
}
|
||||
|
||||
<form
|
||||
[formGroup]="commentForm"
|
||||
(ngSubmit)="addComment(task.tasks.id)"
|
||||
class="comment-form"
|
||||
>
|
||||
<h4>Add a comment</h4>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Your comment</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="content"
|
||||
placeholder="Share your thoughts..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
@if (commentForm.get('content')?.hasError('required') &&
|
||||
commentForm.get('content')?.touched) {
|
||||
<mat-error>Comment text is required</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Post as</mat-label>
|
||||
<select matNativeControl formControlName="userId" required>
|
||||
<option value="" disabled selected>Select user</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-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="commentForm.invalid || addingComment()"
|
||||
>
|
||||
@if (addingComment()) {
|
||||
<mat-spinner
|
||||
diameter="20"
|
||||
class="button-spinner"
|
||||
></mat-spinner>
|
||||
} @else {
|
||||
<mat-icon>send</mat-icon>
|
||||
}
|
||||
<span>{{
|
||||
addingComment() ? 'Posting...' : 'Post Comment'
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.fab {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1100;
|
||||
}
|
||||
.dialog-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1200;
|
||||
}
|
||||
.dialog-card {
|
||||
min-width: 320px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
.page-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 4rem 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 3rem;
|
||||
margin: 2rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
color: #bdbdbd;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.task-container {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.task-panel {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-badge {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.task-author {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-timestamp {
|
||||
font-size: 0.8rem;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.task-details {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.5rem;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.comments-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 1rem;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: 0.8rem;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
font-style: italic;
|
||||
color: #757575;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comment-form h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-spinner {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class TaskListComponent implements OnInit {
|
||||
private tasksService = inject(TasksService);
|
||||
private usersService = inject(UsersService);
|
||||
private commentsService = inject(CommentsService);
|
||||
private fb = inject(FormBuilder);
|
||||
private toastService = inject(ToastService);
|
||||
|
||||
tasks = signal<TaskResponse[]>([]);
|
||||
users = signal<User[]>([]);
|
||||
comments = signal<CommentResponse[]>([]);
|
||||
loading = signal(true);
|
||||
addingComment = signal(false);
|
||||
|
||||
private readonly dialog = inject(MatDialog);
|
||||
|
||||
openAddTaskDialog() {
|
||||
const dialogRef = this.dialog.open(AddTaskDialogComponent, {
|
||||
width: '400px',
|
||||
enterAnimationDuration: '220ms',
|
||||
exitAnimationDuration: '180ms',
|
||||
autoFocus: true,
|
||||
restoreFocus: true,
|
||||
disableClose: false,
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
commentForm: FormGroup = this.fb.group({
|
||||
content: ['', Validators.required],
|
||||
userId: ['', Validators.required],
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.loading.set(true);
|
||||
|
||||
forkJoin({
|
||||
users: this.usersService.getUsers(),
|
||||
tasks: this.tasksService.getTasks(),
|
||||
comments: this.commentsService.getComments(),
|
||||
})
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
console.error('Error loading data:', error);
|
||||
this.toastService.show(
|
||||
'Failed to load data. Please try again.',
|
||||
'error'
|
||||
);
|
||||
return of({ users: [], tasks: [], comments: [] });
|
||||
}),
|
||||
finalize(() => this.loading.set(false))
|
||||
)
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
this.users.set(data.users);
|
||||
this.tasks.set(data.tasks);
|
||||
this.comments.set(data.comments);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getUserName(userId: number): string {
|
||||
const user = this.users().find((u) => u.id === userId);
|
||||
return user ? user.name : 'Unknown User';
|
||||
}
|
||||
|
||||
getTaskComments(taskId: number): CommentResponse[] {
|
||||
return this.comments().filter(
|
||||
(comment) => comment.comments.task_id === taskId
|
||||
);
|
||||
}
|
||||
|
||||
formatDate(timestamp?: string): string {
|
||||
if (!timestamp) return 'Unknown date';
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return (
|
||||
'Today at ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
);
|
||||
} else if (diffDays === 1) {
|
||||
return (
|
||||
'Yesterday at ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
);
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
} else {
|
||||
return date.toLocaleDateString([], {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addComment(taskId: number) {
|
||||
if (this.commentForm.invalid) return;
|
||||
|
||||
this.addingComment.set(true);
|
||||
|
||||
const comment = {
|
||||
content: this.commentForm.value.content,
|
||||
task_id: taskId,
|
||||
user_id: parseInt(this.commentForm.value.userId),
|
||||
};
|
||||
|
||||
this.commentsService
|
||||
.createComment(comment)
|
||||
.pipe(finalize(() => this.addingComment.set(false)))
|
||||
.subscribe({
|
||||
next: (newComment) => {
|
||||
this.comments.update((comments) => [...comments, newComment]);
|
||||
this.commentForm.reset({ content: '', userId: '' });
|
||||
this.toastService.show('Comment added successfully!');
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error adding comment:', error);
|
||||
this.toastService.show(
|
||||
'Failed to add comment! Please try again.',
|
||||
'error'
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
6
client/libs/web/tasks/src/test-setup.ts
Normal file
6
client/libs/web/tasks/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
30
client/libs/web/tasks/tsconfig.json
Normal file
30
client/libs/web/tasks/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
client/libs/web/tasks/tsconfig.lib.json
Normal file
16
client/libs/web/tasks/tsconfig.lib.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
17
client/libs/web/tasks/tsconfig.spec.json
Normal file
17
client/libs/web/tasks/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
client/libs/web/users/README.md
Normal file
7
client/libs/web/users/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# users
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test users` to execute the unit tests.
|
||||
34
client/libs/web/users/eslint.config.mjs
Normal file
34
client/libs/web/users/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
client/libs/web/users/jest.config.ts
Normal file
21
client/libs/web/users/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'users',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/web/users',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
client/libs/web/users/project.json
Normal file
20
client/libs/web/users/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "users",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/web/users/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/web/users/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/libs/web/users/src/index.ts
Normal file
2
client/libs/web/users/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './lib/user-details.component';
|
||||
export * from './lib/user-list.component';
|
||||
107
client/libs/web/users/src/lib/add-user-dialog.component.ts
Normal file
107
client/libs/web/users/src/lib/add-user-dialog.component.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogActions, MatDialogClose, MatDialogContent, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-add-user-dialog',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
template: `
|
||||
<h2 mat-dialog-title class="dialog-title">Add New User</h2>
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Full name" />
|
||||
@if (userForm.get('name')?.hasError('required') && userForm.get('name')?.touched) {
|
||||
<mat-error>
|
||||
Name is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput formControlName="email" placeholder="Email address" />
|
||||
@if (userForm.get('email')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Email is required
|
||||
</mat-error>
|
||||
}
|
||||
@if (userForm.get('email')?.hasError('email') && !userForm.get('email')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions align="end" class="dialog-actions">
|
||||
<button mat-stroked-button mat-dialog-close type="button">Cancel</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="userForm.invalid"
|
||||
>
|
||||
<mat-icon>person_add</mat-icon> Add User
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.dialog-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
color: #1976d2;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.dialog-content {
|
||||
min-width: 340px;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin: 1.2rem 0;
|
||||
}
|
||||
.dialog-actions {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
button[mat-raised-button] mat-icon {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dialog-content {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
`
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddUserDialogComponent {
|
||||
readonly dialogRef = inject(MatDialogRef<AddUserDialogComponent>);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
|
||||
userForm: FormGroup = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
|
||||
onSubmit() {
|
||||
if (this.userForm.invalid) return;
|
||||
this.dialogRef.close(this.userForm.value);
|
||||
}
|
||||
}
|
||||
144
client/libs/web/users/src/lib/user-details.component.ts
Normal file
144
client/libs/web/users/src/lib/user-details.component.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import {
|
||||
TaskResponse,
|
||||
TasksService,
|
||||
User,
|
||||
UsersService,
|
||||
} from '@org/shared/api';
|
||||
import { TaskFormComponent } from '@org/web/tasks';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-user-details',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatListModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatToolbarModule,
|
||||
TaskFormComponent,
|
||||
],
|
||||
template: `
|
||||
<mat-toolbar color="primary">User Details</mat-toolbar>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="loading-container">
|
||||
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
} @else if (user()) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ user()?.name }}</mat-card-title>
|
||||
<mat-card-subtitle>{{ user()?.email }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-actions>
|
||||
<button mat-button routerLink="/users">Back to Users</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<div class="tasks-section">
|
||||
<h2>User Tasks</h2>
|
||||
|
||||
@if (userTasks().length) {
|
||||
<mat-list>
|
||||
@for (task of userTasks(); track task.tasks.id) {
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon>assignment</mat-icon>
|
||||
<h3 matListItemTitle>{{ task.tasks.title }}</h3>
|
||||
<p matListItemLine>
|
||||
{{ task.tasks.description || 'No description' }}
|
||||
</p>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
} @else {
|
||||
<p>No tasks found for this user.</p>
|
||||
}
|
||||
|
||||
<lib-task-form
|
||||
[userId]="user()?.id || 0"
|
||||
(taskAdded)="loadUserTasks()"
|
||||
></lib-task-form>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="error-container">
|
||||
<p>User not found</p>
|
||||
<button mat-button routerLink="/users">Back to Users</button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.loading-container,
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.tasks-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class UserDetailsComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private usersService = inject(UsersService);
|
||||
private tasksService = inject(TasksService);
|
||||
|
||||
user = signal<User | null>(null);
|
||||
userTasks = signal<TaskResponse[]>([]);
|
||||
loading = signal(true);
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe((params) => {
|
||||
const userId = +params['id'];
|
||||
this.loadUserDetails(userId);
|
||||
this.loadUserTasks(userId);
|
||||
});
|
||||
}
|
||||
|
||||
loadUserDetails(userId: number) {
|
||||
this.loading.set(true);
|
||||
this.usersService.getUser(userId).subscribe({
|
||||
next: (userData) => {
|
||||
this.user.set(userData);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error fetching user:', error);
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
loadUserTasks(userId?: number) {
|
||||
const id = userId || this.user()?.id;
|
||||
if (!id) return;
|
||||
|
||||
this.tasksService.getTasks().subscribe({
|
||||
next: (tasks) => {
|
||||
this.userTasks.set(tasks.filter((task) => task.tasks.user_id === id));
|
||||
},
|
||||
error: (error) => console.error('Error fetching tasks:', error),
|
||||
});
|
||||
}
|
||||
}
|
||||
344
client/libs/web/users/src/lib/user-list.component.ts
Normal file
344
client/libs/web/users/src/lib/user-list.component.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, effect, inject, signal } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AddUserDialogComponent } from './add-user-dialog.component';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CommentStoreService, TasksStoreService, UserStoreService } from '@org/shared/stores';
|
||||
import { ToastService } from '@org/shared/toast';
|
||||
import { CommentResponse, TaskResponse, User } from '@org/shared/api';
|
||||
|
||||
type UserWithCounts = User & { taskCount: number; commentCount: number };
|
||||
|
||||
@Component({
|
||||
selector: 'lib-user-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatIconModule,
|
||||
MatToolbarModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatDividerModule,
|
||||
],
|
||||
template: `
|
||||
<mat-toolbar color="primary">User Management</mat-toolbar>
|
||||
<div class="container">
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Search Users</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[formControl]="searchControl"
|
||||
placeholder="type name or email"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="loading-container">
|
||||
<mat-progress-spinner mode="indeterminate" />
|
||||
</div>
|
||||
} @else if (filteredUsers.length === 0) {
|
||||
<div class="empty-state">
|
||||
<p>No users found. Try adjusting your search or add a new user.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="user-grid">
|
||||
@for (user of filteredUsers; track user.id) {
|
||||
<mat-card class="user-card">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
class="delete-btn"
|
||||
(click)="deleteUser(user.id)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<a
|
||||
[routerLink]="['/users', user.id]"
|
||||
class="user-link"
|
||||
>{{ user.name }}</a
|
||||
>
|
||||
</mat-card-title>
|
||||
<mat-card-subtitle>{{ user.email }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="user-meta">
|
||||
<span>
|
||||
<mat-icon class="meta-icon">assignment</mat-icon>
|
||||
{{ user.taskCount }} Tasks
|
||||
</span>
|
||||
<span>
|
||||
<mat-icon class="meta-icon">comment</mat-icon>
|
||||
{{ user.commentCount }} Comments
|
||||
</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
.search-container {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background-color: var(--mat-grey-100, #f5f5f5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.user-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.user-card {
|
||||
position: relative;
|
||||
background: var(--mat-card-background, #fff);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
will-change: box-shadow, transform;
|
||||
animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
padding-bottom: 0.5rem;
|
||||
overflow: visible;
|
||||
}
|
||||
.user-card:hover {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.14);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
}
|
||||
.user-link {
|
||||
text-decoration: none;
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.user-link:hover {
|
||||
color: #125ea2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 0.7rem;
|
||||
right: 0.7rem;
|
||||
z-index: 2;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #ffeaea;
|
||||
}
|
||||
.user-meta {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
margin-top: 0.7rem;
|
||||
font-size: 0.98rem;
|
||||
color: #555;
|
||||
align-items: center;
|
||||
}
|
||||
.meta-icon {
|
||||
font-size: 1.1em;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.2em;
|
||||
color: #1976d2;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.user-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.search-container {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.dialog-card {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
.dialog-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.25s;
|
||||
}
|
||||
.dialog-card {
|
||||
min-width: 320px;
|
||||
max-width: 90vw;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
animation: scaleIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class UserListComponent {
|
||||
private readonly userStore = inject(UserStoreService);
|
||||
private readonly taskStore = inject(TasksStoreService);
|
||||
private readonly commentsStore = inject(CommentStoreService);
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly toastService = inject(ToastService);
|
||||
|
||||
protected searchControl = new FormControl('');
|
||||
protected isLoading = signal(true);
|
||||
|
||||
filteredUsers: UserWithCounts[] = [];
|
||||
private allUsersWithCounts: (User & { taskCount: number; commentCount: number })[] = [];
|
||||
|
||||
constructor() {
|
||||
this.userStore.getUsers();
|
||||
effect(() => {
|
||||
const users = this.userStore.users();
|
||||
const tasks = this.taskStore.tasks?.() ?? [];
|
||||
const comments = this.commentsStore.comments?.() ?? [];
|
||||
if (Array.isArray(users)) {
|
||||
this.allUsersWithCounts = addCountsToUsers(users, tasks, comments);
|
||||
this.isLoading.set(false);
|
||||
this.applyFilter();
|
||||
}
|
||||
});
|
||||
this.searchControl.valueChanges.subscribe(() => this.applyFilter());
|
||||
}
|
||||
private readonly dialog = inject(MatDialog);
|
||||
|
||||
userForm: FormGroup = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
|
||||
applyFilter() {
|
||||
const term = this.searchControl.value?.toLocaleLowerCase() ?? '';
|
||||
this.filteredUsers = this.allUsersWithCounts
|
||||
.filter(
|
||||
(u) =>
|
||||
u.name?.toLocaleLowerCase().includes(term) ||
|
||||
u.email?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
deleteUser(id: number) {
|
||||
if (confirm('Are you sure you want to delete this user?')) {
|
||||
this.userStore.deleteUser(id);
|
||||
this.toastService.show('User deleted successfully');
|
||||
this.applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
openAddUserDialog() {
|
||||
const dialogRef = this.dialog.open(AddUserDialogComponent, {
|
||||
width: '350px',
|
||||
enterAnimationDuration: '220ms',
|
||||
exitAnimationDuration: '180ms',
|
||||
autoFocus: true,
|
||||
restoreFocus: true,
|
||||
disableClose: false,
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.userStore.createUser(result);
|
||||
this.toastService.show('User created successfully');
|
||||
this.applyFilter();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addCountsToUsers(users: User[], tasks: TaskResponse[], comments: CommentResponse[]) {
|
||||
return users.map((user) => {
|
||||
const userTasks = tasks.filter((t) => t.tasks.user_id === user.id);
|
||||
const userComments = comments.filter((c) => c.comments.user_id === user.id);
|
||||
return {
|
||||
...user,
|
||||
taskCount: userTasks.length,
|
||||
commentCount: userComments.length,
|
||||
};
|
||||
})
|
||||
}
|
||||
6
client/libs/web/users/src/test-setup.ts
Normal file
6
client/libs/web/users/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
30
client/libs/web/users/tsconfig.json
Normal file
30
client/libs/web/users/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
client/libs/web/users/tsconfig.lib.json
Normal file
16
client/libs/web/users/tsconfig.lib.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
17
client/libs/web/users/tsconfig.spec.json
Normal file
17
client/libs/web/users/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
64
client/nx.json
Normal file
64
client/nx.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||
"defaultBase": "master",
|
||||
"namedInputs": {
|
||||
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||
"production": [
|
||||
"default",
|
||||
"!{projectRoot}/.eslintrc.json",
|
||||
"!{projectRoot}/eslint.config.mjs",
|
||||
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||
"!{projectRoot}/tsconfig.spec.json",
|
||||
"!{projectRoot}/jest.config.[jt]s",
|
||||
"!{projectRoot}/src/test-setup.[jt]s",
|
||||
"!{projectRoot}/test-setup.[jt]s"
|
||||
],
|
||||
"sharedGlobals": []
|
||||
},
|
||||
"nxCloudId": "6870dfc26f399d24e54b83c5",
|
||||
"targetDefaults": {
|
||||
"@angular/build:application": {
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["production", "^production"]
|
||||
},
|
||||
"@nx/eslint:lint": {
|
||||
"cache": true,
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/.eslintignore",
|
||||
"{workspaceRoot}/eslint.config.mjs"
|
||||
]
|
||||
},
|
||||
"@nx/jest:jest": {
|
||||
"cache": true,
|
||||
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"codeCoverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"@nx/angular:application": {
|
||||
"e2eTestRunner": "none",
|
||||
"linter": "eslint",
|
||||
"style": "scss",
|
||||
"unitTestRunner": "jest"
|
||||
},
|
||||
"@nx/angular:library": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest"
|
||||
},
|
||||
"@nx/angular:component": {
|
||||
"style": "css"
|
||||
}
|
||||
},
|
||||
"defaultProject": "org"
|
||||
}
|
||||
18624
client/package-lock.json
generated
Normal file
18624
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
client/package.json
Normal file
64
client/package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@org/source",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "nx serve",
|
||||
"build": "nx build",
|
||||
"test": "nx test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^20.0.7",
|
||||
"@angular/cdk": "^20.1.0",
|
||||
"@angular/common": "~20.0.0",
|
||||
"@angular/compiler": "~20.0.0",
|
||||
"@angular/core": "~20.0.0",
|
||||
"@angular/forms": "~20.0.0",
|
||||
"@angular/material": "^20.1.0",
|
||||
"@angular/platform-browser": "~20.0.0",
|
||||
"@angular/platform-browser-dynamic": "~20.0.0",
|
||||
"@angular/router": "~20.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/core": "~20.0.0",
|
||||
"@angular-devkit/schematics": "~20.0.0",
|
||||
"@angular/build": "~20.0.0",
|
||||
"@angular/cli": "~20.0.0",
|
||||
"@angular/compiler-cli": "~20.0.0",
|
||||
"@angular/language-service": "~20.0.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@nx/angular": "21.2.2",
|
||||
"@nx/eslint": "21.2.2",
|
||||
"@nx/eslint-plugin": "21.2.2",
|
||||
"@nx/jest": "21.2.2",
|
||||
"@nx/js": "21.2.2",
|
||||
"@nx/web": "21.2.2",
|
||||
"@nx/workspace": "21.2.2",
|
||||
"@schematics/angular": "~20.0.0",
|
||||
"@swc-node/register": "~1.9.1",
|
||||
"@swc/core": "~1.5.7",
|
||||
"@swc/helpers": "~0.5.11",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "18.16.9",
|
||||
"@typescript-eslint/utils": "^8.29.0",
|
||||
"angular-eslint": "^20.0.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "~14.6.0",
|
||||
"nx": "21.2.2",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "~5.8.2",
|
||||
"typescript-eslint": "^8.29.0"
|
||||
},
|
||||
"nx": {
|
||||
"includedScripts": []
|
||||
}
|
||||
}
|
||||
96
client/project.json
Normal file
96
client/project.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "org",
|
||||
"$schema": "node_modules/nx/schemas/project-schema.json",
|
||||
"includedScripts": [],
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "./src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular/build:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/org",
|
||||
"browser": "./src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"./src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"continuous": true,
|
||||
"executor": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "org:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "org:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular/build:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "org:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["./src"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectName}"],
|
||||
"options": {
|
||||
"jestConfig": "jest.config.app.ts"
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"continuous": true,
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "org:build",
|
||||
"staticFilePath": "dist/org/browser",
|
||||
"spa": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
25
client/src/app/app.config.ts
Normal file
25
client/src/app/app.config.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
importProvidersFrom,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { API, apiUrls } from '@org/shared/api';
|
||||
import { appRoutes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(appRoutes),
|
||||
provideHttpClient(),
|
||||
importProvidersFrom(MatSnackBarModule),
|
||||
{
|
||||
provide: API,
|
||||
useValue: apiUrls,
|
||||
},
|
||||
],
|
||||
};
|
||||
23
client/src/app/app.routes.ts
Normal file
23
client/src/app/app.routes.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { TaskListComponent } from '@org/web/tasks';
|
||||
import { UserDetailsComponent, UserListComponent } from '@org/web/users';
|
||||
|
||||
export const appRoutes: Route[] = [
|
||||
{
|
||||
path: 'users',
|
||||
component: UserListComponent,
|
||||
},
|
||||
{
|
||||
path: 'users/:id',
|
||||
component: UserDetailsComponent,
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
component: TaskListComponent,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'users',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
];
|
||||
0
client/src/app/app.scss
Normal file
0
client/src/app/app.scss
Normal file
18
client/src/app/app.spec.ts
Normal file
18
client/src/app/app.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
import { NxWelcome } from './nx-welcome';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App, NxWelcome],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Welcome org');
|
||||
});
|
||||
});
|
||||
14
client/src/app/app.ts
Normal file
14
client/src/app/app.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NavbarComponent } from '@org/shared/navbar';
|
||||
|
||||
@Component({
|
||||
imports: [RouterModule, NavbarComponent],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<lib-navbar />
|
||||
<router-outlet />
|
||||
`,
|
||||
styles: [``],
|
||||
})
|
||||
export class App { }
|
||||
15
client/src/index.html
Normal file
15
client/src/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>org</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
5
client/src/main.ts
Normal file
5
client/src/main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||
43
client/src/styles.scss
Normal file
43
client/src/styles.scss
Normal file
@ -0,0 +1,43 @@
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
max-width: 100vw;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #fafafa;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
body.dark-theme {
|
||||
background-color: #303030;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-bottom: 16px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.dark-theme .mat-mdc-card {
|
||||
--mdc-elevated-card-container-color: #424242;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
6
client/src/test-setup.ts
Normal file
6
client/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
16
client/src/theme.scss
Normal file
16
client/src/theme.scss
Normal file
@ -0,0 +1,16 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
@include mat.theme(
|
||||
(
|
||||
color: (
|
||||
primary: mat.$azure-palette,
|
||||
tertiary: mat.$blue-palette,
|
||||
theme-type: color-scheme,
|
||||
),
|
||||
typography: Roboto,
|
||||
density: -3,
|
||||
)
|
||||
);
|
||||
}
|
||||
14
client/tsconfig.app.json
Normal file
14
client/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"src/test-setup.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
20
client/tsconfig.base.json
Normal file
20
client/tsconfig.base.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"emitDecoratorMetadata": false,
|
||||
"target": "es2022",
|
||||
"module": "preserve",
|
||||
"lib": ["es2020", "dom"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@org/shared/api": ["libs/shared/api/src/index.ts"],
|
||||
"@org/shared/navbar": ["libs/shared/navbar/src/index.ts"],
|
||||
"@org/shared/stores": ["libs/shared/stores/src/index.ts"],
|
||||
"@org/shared/toast": ["libs/shared/toast/src/index.ts"],
|
||||
"@org/web/tasks": ["libs/web/tasks/src/index.ts"],
|
||||
"@org/web/users": ["libs/web/users/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
}
|
||||
23
client/tsconfig.json
Normal file
23
client/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "./tsconfig.base.json"
|
||||
}
|
||||
17
client/tsconfig.spec.json
Normal file
17
client/tsconfig.spec.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user