diff --git a/apps/devmx/public/icons/drag/handle.svg b/apps/devmx/public/icons/drag/handle.svg
new file mode 100644
index 00000000..7646b71e
--- /dev/null
+++ b/apps/devmx/public/icons/drag/handle.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/devmx/public/icons/drag/indicator.svg b/apps/devmx/public/icons/drag/indicator.svg
new file mode 100644
index 00000000..870d64f8
--- /dev/null
+++ b/apps/devmx/public/icons/drag/indicator.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/account/data-source/package.json b/packages/account/data-source/package.json
index ebd15d07..b2928c13 100644
--- a/packages/account/data-source/package.json
+++ b/packages/account/data-source/package.json
@@ -15,7 +15,8 @@
"passport-jwt": "^4.0.1",
"@nestjs/passport": "^10.0.3",
"@devmx/location-data-source": "0.0.1",
- "@devmx/shared-util-data": "0.0.1"
+ "@devmx/shared-util-data": "0.0.1",
+ "@devmx/learn-data-source": "0.0.1"
},
"type": "commonjs",
"main": "./src/index.js",
diff --git a/packages/account/data-source/src/lib/dtos/index.ts b/packages/account/data-source/src/lib/dtos/index.ts
index a55d07f7..6af05e3e 100644
--- a/packages/account/data-source/src/lib/dtos/index.ts
+++ b/packages/account/data-source/src/lib/dtos/index.ts
@@ -12,6 +12,7 @@ export * from './update-user';
export * from './user-contact';
export * from './user-profile';
export * from './user-roles';
+export * from './user-skill';
export * from './user-social';
export * from './user';
export * from './validate-user-code';
diff --git a/packages/account/data-source/src/lib/dtos/update-user.ts b/packages/account/data-source/src/lib/dtos/update-user.ts
index 04187cb4..f32f5d62 100644
--- a/packages/account/data-source/src/lib/dtos/update-user.ts
+++ b/packages/account/data-source/src/lib/dtos/update-user.ts
@@ -1,8 +1,14 @@
-import { IsNotEmpty, IsString, ValidateNested } from 'class-validator';
+import {
+ IsArray,
+ IsNotEmpty,
+ IsOptional,
+ IsString,
+ ValidateNested,
+} from 'class-validator';
import { UpdateProfileDto } from './update-profile';
import { UpdateContactDto } from './update-contact';
import { Exclude, Type } from 'class-transformer';
-import { ApiProperty } from '@nestjs/swagger';
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { UserSocialDto } from './user-social';
import { RolesDto } from './roles';
import {
@@ -10,6 +16,7 @@ import {
EditableUser,
UserPassword,
} from '@devmx/shared-api-interfaces';
+import { UserSkillDto } from './user-skill';
export class UpdateUserDto implements EditableUser {
@ApiProperty()
@@ -30,6 +37,13 @@ export class UpdateUserDto implements EditableUser {
@Exclude()
roles: RolesDto;
+ @IsArray()
+ @IsOptional()
+ @ValidateNested()
+ @Type(() => UserSkillDto)
+ @ApiPropertyOptional({ type: () => [UserSkillDto] })
+ skills: UserSkillDto[];
+
@ApiProperty()
@ValidateNested()
@Type(() => UpdateProfileDto)
diff --git a/packages/account/data-source/src/lib/dtos/user-skill.ts b/packages/account/data-source/src/lib/dtos/user-skill.ts
new file mode 100644
index 00000000..d568fbf6
--- /dev/null
+++ b/packages/account/data-source/src/lib/dtos/user-skill.ts
@@ -0,0 +1,16 @@
+import { UserSkill } from '@devmx/shared-api-interfaces';
+import { IsNotEmpty, IsNumber } from 'class-validator';
+import { SkillDto } from '@devmx/learn-data-source';
+import { ApiProperty } from '@nestjs/swagger';
+import { Type } from 'class-transformer';
+
+export class UserSkillDto implements UserSkill {
+ @IsNumber()
+ @IsNotEmpty()
+ @ApiProperty()
+ weight: number;
+
+ @Type(() => SkillDto)
+ @ApiProperty({ type: () => SkillDto })
+ skill: SkillDto;
+}
diff --git a/packages/account/data-source/src/lib/dtos/user.ts b/packages/account/data-source/src/lib/dtos/user.ts
index 201d28cc..9707c898 100644
--- a/packages/account/data-source/src/lib/dtos/user.ts
+++ b/packages/account/data-source/src/lib/dtos/user.ts
@@ -5,6 +5,7 @@ import { UserProfileDto } from './user-profile';
import { ApiProperty } from '@nestjs/swagger';
import { UserSocialDto } from './user-social';
import { RolesDto } from './roles';
+import { UserSkillDto } from './user-skill';
export class UserDto implements User {
@ApiProperty()
@@ -23,6 +24,10 @@ export class UserDto implements User {
@ApiProperty({ type: () => RolesDto })
roles: RolesDto;
+ @Type(() => UserSkillDto)
+ @ApiProperty({ type: () => [UserSkillDto] })
+ skills: UserSkillDto[];
+
@ApiProperty()
@Type(() => UserContactDto)
contact: UserContactDto;
diff --git a/packages/account/data-source/src/lib/schemas/user.ts b/packages/account/data-source/src/lib/schemas/user.ts
index 679eb8e7..2ad8e295 100644
--- a/packages/account/data-source/src/lib/schemas/user.ts
+++ b/packages/account/data-source/src/lib/schemas/user.ts
@@ -3,9 +3,10 @@ import { UserContactCollection, UserContactSchema } from './user-contact';
import { UserProfileCollection, UserProfileSchema } from './user-profile';
import { UserSocialCollection, UserSocialSchema } from './user-social';
import { UserCodeCollection, UserCodeSchema } from './user-code';
-import { Roles, User } from '@devmx/shared-api-interfaces';
+import { Roles, User, UserSkill } from '@devmx/shared-api-interfaces';
import { createSchema } from '@devmx/shared-data-source';
import { DEFAULT_ROLES } from '@devmx/shared-util-data';
+import { SkillSchema } from '@devmx/learn-data-source';
import { Prop, raw, Schema } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@@ -25,6 +26,16 @@ export class UserCollection extends Document implements User {
@Prop({ required: true, type: Object, default: DEFAULT_ROLES })
roles: Roles;
+ @Prop([
+ {
+ type: raw({
+ skill: { type: SkillSchema, required: true },
+ weight: { type: Number, required: true },
+ }),
+ },
+ ])
+ skills: UserSkill[];
+
@Prop({ required: true, type: raw(UserContactSchema) })
contact: UserContactCollection;
diff --git a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html
index 0899fdd5..7e44f01f 100644
--- a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html
+++ b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html
@@ -11,6 +11,23 @@
{{'@' + user.name}}
}
+
+
+@if (user.skills?.length) {
+
+
+
+ @for (item of user.skills; track item.skill.id) {
+ -
+
{{item.skill.name}}
+
+
+ }
+
+
+
+}
+
}
diff --git a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss
index ade9d6c9..5fc59fd0 100644
--- a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss
+++ b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss
@@ -11,4 +11,19 @@
display: flex;
flex-direction: column;
}
+
+ ol {
+ padding-left: 1.4em;
+
+ li {
+ margin-bottom: 1em;
+ p {
+ margin: 0;
+ }
+ }
+ }
+}
+
+::ng-deep ol li .mdc-linear-progress .mdc-linear-progress__bar-inner {
+ border-color: #3BCE53;
}
diff --git a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts
index 215827bd..43e0c167 100644
--- a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts
+++ b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts
@@ -1,5 +1,6 @@
import { PresentationCardListComponent } from '@devmx/presentation-ui-shared';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
import { PresentationFacade } from '@devmx/presentation-data-access';
import { MarkdownComponent } from '@devmx/shared-ui-global/editor';
import { AuthenticationFacade } from '@devmx/account-data-access';
@@ -23,6 +24,7 @@ import { filter, map, take } from 'rxjs';
MarkdownComponent,
PresentationCardListComponent,
EventCardListComponent,
+ MatProgressBarModule,
AsyncPipe,
],
standalone: true,
diff --git a/packages/account/feature-shell/src/lib/containers/account/account.container.html b/packages/account/feature-shell/src/lib/containers/account/account.container.html
index 6ebc8433..23b55344 100644
--- a/packages/account/feature-shell/src/lib/containers/account/account.container.html
+++ b/packages/account/feature-shell/src/lib/containers/account/account.container.html
@@ -6,6 +6,8 @@
+
+
diff --git a/packages/account/feature-shell/src/lib/containers/account/account.container.ts b/packages/account/feature-shell/src/lib/containers/account/account.container.ts
index 33d26571..c4063aa8 100644
--- a/packages/account/feature-shell/src/lib/containers/account/account.container.ts
+++ b/packages/account/feature-shell/src/lib/containers/account/account.container.ts
@@ -2,6 +2,7 @@ import { SelectFileComponent } from '@devmx/shared-ui-global/image';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ProfileComponent } from './profile/profile.component';
import { ContactComponent } from './contact/contact.component';
+import { SkillsComponent } from './skills/skills.component';
import { SocialComponent } from './social/social.component';
import { UserPhoto, provideUserPhoto } from '../../dialogs';
import { MatButtonModule } from '@angular/material/button';
@@ -23,6 +24,7 @@ import {
AuthenticationFacade,
} from '@devmx/account-data-access';
+
@Component({
selector: 'devmx-account',
templateUrl: './account.container.html',
@@ -35,6 +37,7 @@ import {
ProfileComponent,
ContactComponent,
SocialComponent,
+ SkillsComponent,
SelectFileComponent,
MatButtonModule,
AvatarComponent,
diff --git a/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html
new file mode 100644
index 00000000..60cd0f4b
--- /dev/null
+++ b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html
@@ -0,0 +1,45 @@
+
+
+ @for (item of form.skills.controls; track item.value.skill?.id; let index =
+ $index) {
+ -
+
+
+
+
+ {{ item.value.skill?.name }}
+
+
+
+
+
+
+
+ {{ item.value.weight }}%
+
+ }
+
+
+
+
+ Adicionar habilidade
+
+
+ @if (skillFacade.response$ | async; as response) { @for (option of
+ response.data; track option.id) {
+ {{ option.name }}
+ } }
+
+
diff --git a/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss
new file mode 100644
index 00000000..09bb0274
--- /dev/null
+++ b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss
@@ -0,0 +1,68 @@
+:host {
+ display: flex;
+ flex-direction: column;
+
+ .skill-list {
+ padding: 0;
+
+ gap: 1em;
+ display: flex;
+ flex-direction: column;
+
+ devmx-icon {
+ cursor: grab;
+
+ &:active {
+ cursor: grabbing;
+ }
+ }
+
+ li {
+ gap: 1.2em;
+ display: flex;
+ align-items: center;
+
+ & > div {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+
+ div {
+ padding-left: 0.5em;
+ }
+ }
+ }
+ }
+
+ .skills-list {
+ width: 500px;
+ max-width: 100%;
+ overflow: hidden;
+
+
+ li {
+ min-height: 3em;
+ box-sizing: border-box;
+ cursor: move;
+ }
+ }
+
+ .cdk-drag-preview {
+ border: none;
+ box-sizing: border-box;
+ box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
+ 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
+ }
+
+ .cdk-drag-placeholder {
+ opacity: 0;
+ }
+
+ .cdk-drag-animating {
+ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+ }
+
+ .skills-list.cdk-drop-list-dragging .skills-list li:not(.cdk-drag-placeholder) {
+ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+ }
+}
diff --git a/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts
new file mode 100644
index 00000000..7f9e88a3
--- /dev/null
+++ b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts
@@ -0,0 +1,98 @@
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
+import { inject, Component, ChangeDetectorRef } from '@angular/core';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { MatButtonModule } from '@angular/material/button';
+import { MatSliderModule } from '@angular/material/slider';
+import { UserForm, UserSkillForm } from '../../../forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatListModule } from '@angular/material/list';
+import { SkillFacade } from '@devmx/learn-data-access';
+import { Skill } from '@devmx/shared-api-interfaces';
+import { debounceTime, filter } from 'rxjs';
+import { AsyncPipe } from '@angular/common';
+import {
+ FormControl,
+ ControlContainer,
+ ReactiveFormsModule,
+} from '@angular/forms';
+import { IconComponent } from '@devmx/shared-ui-global/icon';
+import {
+ CdkDragDrop,
+ DragDropModule,
+ moveItemInArray,
+} from '@angular/cdk/drag-drop';
+
+@Component({
+ selector: 'devmx-account-skills',
+ templateUrl: './skills.component.html',
+ styleUrl: './skills.component.scss',
+ viewProviders: [
+ {
+ provide: ControlContainer,
+ useFactory: () => inject(ControlContainer, { skipSelf: true }),
+ },
+ ],
+ imports: [
+ MatAutocompleteModule,
+ MatProgressBarModule,
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatButtonModule,
+ MatSliderModule,
+ DragDropModule,
+ MatInputModule,
+ MatListModule,
+ IconComponent,
+ AsyncPipe,
+ ],
+})
+export class SkillsComponent {
+ container = inject(ControlContainer);
+
+ skillFacade = inject(SkillFacade);
+
+ cdr = inject(ChangeDetectorRef);
+
+ get form() {
+ return this.container.control as UserForm;
+ }
+
+ searchControl = new FormControl('');
+
+ constructor() {
+ this.searchControl.valueChanges
+ .pipe(
+ filter((value) => typeof value === 'string'),
+ filter((value) => value.length > 1),
+ takeUntilDestroyed(),
+ debounceTime(600)
+ )
+ .subscribe((name) => {
+ this.skillFacade.setFilter({ name });
+ this.skillFacade.load();
+ });
+ }
+
+ displayFn(skill: Skill) {
+ return skill && skill.name ? skill.name : '';
+ }
+
+ onOptionSelected(skill: Skill) {
+ this.form.skills.add({ skill, weight: 0 });
+ this.searchControl.setValue('');
+ }
+
+ formatLabel(value: number) {
+ return `${value}%`;
+ }
+
+ drop(event: CdkDragDrop) {
+ moveItemInArray(
+ this.form.skills.controls,
+ event.previousIndex,
+ event.currentIndex
+ );
+ }
+}
diff --git a/packages/account/feature-shell/src/lib/forms/index.ts b/packages/account/feature-shell/src/lib/forms/index.ts
index e285e204..dbd5a854 100644
--- a/packages/account/feature-shell/src/lib/forms/index.ts
+++ b/packages/account/feature-shell/src/lib/forms/index.ts
@@ -1,4 +1,6 @@
export * from './user-contact';
export * from './user-profile';
+export * from './user-roles';
+export * from './user-skill';
export * from './user-social';
export * from './user';
diff --git a/packages/account/feature-shell/src/lib/forms/user-skill.ts b/packages/account/feature-shell/src/lib/forms/user-skill.ts
new file mode 100644
index 00000000..0b65aaaa
--- /dev/null
+++ b/packages/account/feature-shell/src/lib/forms/user-skill.ts
@@ -0,0 +1,45 @@
+import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
+import { Skill, UserSkill } from '@devmx/shared-api-interfaces';
+import { TypedForm } from '@devmx/shared-ui-global/forms';
+import { SkillForm } from '@devmx/learn-ui-shared';
+
+export class UserSkillForm extends FormGroup> {
+ constructor(value?: Partial) {
+ super({
+ skill: new SkillForm(),
+ weight: new FormControl(0, {
+ nonNullable: true,
+ validators: [Validators.min(0), Validators.max(100)],
+ }),
+ });
+
+ if (value) {
+ this.patchValue(value);
+ }
+ }
+}
+
+export class UserSkillsForm extends FormArray {
+ constructor() {
+ super([]);
+ }
+
+ add(value?: UserSkill) {
+ this.push(new UserSkillForm(value));
+ }
+
+ has(skill?: Skill) {
+ return this.value.some((item) => item.skill === skill);
+ }
+
+ override patchValue(
+ value: Partial[],
+ options?: { onlySelf?: boolean; emitEvent?: boolean }
+ ): void {
+ for (const item of value) {
+ if (!this.has(item.skill)) {
+ this.push(new UserSkillForm(item), options);
+ }
+ }
+ }
+}
diff --git a/packages/account/feature-shell/src/lib/forms/user.ts b/packages/account/feature-shell/src/lib/forms/user.ts
index 35fa3858..c1a9b8e6 100644
--- a/packages/account/feature-shell/src/lib/forms/user.ts
+++ b/packages/account/feature-shell/src/lib/forms/user.ts
@@ -5,6 +5,7 @@ import { UserProfileForm } from './user-profile';
import { UserContactForm } from './user-contact';
import { UserSocialForm } from './user-social';
import { UserRolesForm } from './user-roles';
+import { UserSkillsForm } from './user-skill';
export class UserForm extends FormGroup> {
constructor() {
@@ -26,6 +27,7 @@ export class UserForm extends FormGroup> {
social: new UserSocialForm(),
active: new FormControl(),
roles: new UserRolesForm(),
+ skills: new UserSkillsForm(),
});
}
@@ -45,6 +47,10 @@ export class UserForm extends FormGroup> {
return this.controls.roles as UserRolesForm;
}
+ get skills() {
+ return this.controls.skills as UserSkillsForm;
+ }
+
patch(value: Partial) {
this.patchValue(value);
diff --git a/packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts b/packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts
new file mode 100644
index 00000000..3931e437
--- /dev/null
+++ b/packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts
@@ -0,0 +1,4 @@
+import { EditableEntity } from '../types';
+import { UserSkill } from '../entities';
+
+export type EditableUserSkill = EditableEntity;
diff --git a/packages/shared/api-interfaces/src/lib/dtos/index.ts b/packages/shared/api-interfaces/src/lib/dtos/index.ts
index 2e1247e3..28c18be9 100644
--- a/packages/shared/api-interfaces/src/lib/dtos/index.ts
+++ b/packages/shared/api-interfaces/src/lib/dtos/index.ts
@@ -18,6 +18,7 @@ export * from './editable-photo';
export * from './editable-presentation';
export * from './editable-skill';
export * from './editable-subject';
+export * from './editable-user-skill';
export * from './editable-user';
export * from './event-out';
export * from './event-page-schema';
diff --git a/packages/shared/api-interfaces/src/lib/entities/user.ts b/packages/shared/api-interfaces/src/lib/entities/user.ts
index cb6a3225..711c45b8 100644
--- a/packages/shared/api-interfaces/src/lib/entities/user.ts
+++ b/packages/shared/api-interfaces/src/lib/entities/user.ts
@@ -3,6 +3,7 @@ import { UserCode } from './user-code';
import { UserContact } from './user-contact';
import { UserPassword } from './user-password';
import { UserProfile } from './user-profile';
+import { UserSkill } from './user-skill';
import { UserSocial } from './user-social';
export interface User {
@@ -16,6 +17,8 @@ export interface User {
roles: Roles;
+ skills?: UserSkill[];
+
contact: UserContact;
code?: UserCode;
diff --git a/packages/shared/ui-global/icon/src/lib/types/drag.ts b/packages/shared/ui-global/icon/src/lib/types/drag.ts
new file mode 100644
index 00000000..8d781536
--- /dev/null
+++ b/packages/shared/ui-global/icon/src/lib/types/drag.ts
@@ -0,0 +1,3 @@
+export type Drag = 'indicator' | 'handle';
+
+export type DragIcon = `drag/${Drag}`;
diff --git a/packages/shared/ui-global/icon/src/lib/types/icon.ts b/packages/shared/ui-global/icon/src/lib/types/icon.ts
index f7378f51..f56a302e 100644
--- a/packages/shared/ui-global/icon/src/lib/types/icon.ts
+++ b/packages/shared/ui-global/icon/src/lib/types/icon.ts
@@ -12,6 +12,7 @@ import { FinanceIcon } from './finance';
import { TextIcon } from './text';
import { MessageIcon } from './message';
import { SocialIcon } from './social';
+import { DragIcon } from './drag';
type Root =
| 'airplay'
@@ -145,4 +146,5 @@ export type Icon =
| FinanceIcon
| TextIcon
| MessageIcon
- | SocialIcon;
+ | SocialIcon
+ | DragIcon;