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