اذهب إلى المحتوى

السؤال

نشر

تواجهني مشكلة غريبة لم افهم سببها والتي هي لما ازور صفحة freelancer.html الاحظ ان صفحة تبقى loading profile.... الى ما لا نهاية حتى اعدل سطر كود ويعيد Angular بناء سيرفر ثم يظهر profile ويوجد مشكلة اخرى والتي هي لما احاول اعدل بيانات واضغط ع زر save يبقى saving... ايضا ال ما لا نهاية 

import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';

import {
  FreelancerService,
  IFreelancer,
  IUpdateFreelancer,
} from '../../../../core/services/freelancer.service';

import { AuthState } from '../../../../core/states/auth.state';
import { JobTitle, Skills } from '../../../../core/enums/freelancer.enum';

type ProfileTab = 'overview' | 'projects' | 'reviews';

interface IEditFormErrors {
  jobTitle?: string;
  bio?: string;
  hourlyRate?: string;
  skills?: string;
}

const MAX_SKILLS = 10;
const MAX_BIO_LENGTH = 600;
const MAX_HOURLY_RATE = 1000;

@Component({
  selector: 'app-freelancer-profile',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './freelancer.html',
  styleUrl: './freelancer.scss',
})
export class FreelancerProfile implements OnInit {
  private readonly freelancerService = inject(FreelancerService);
  private readonly authState = inject(AuthState);

  readonly user = this.authState.user;

  freelancer: IFreelancer | null = null;

  activeTab: ProfileTab = 'overview';

  loading = true;
  errorMessage = '';

  isEditMode = false;
  isUpdating = false;
  updateMessage = '';
  updateErrorMessage = '';

  readonly maxSkills = MAX_SKILLS;
  readonly maxBioLength = MAX_BIO_LENGTH;
  readonly maxHourlyRate = MAX_HOURLY_RATE;

  editForm: IUpdateFreelancer = {
    jobTitle: undefined,
    bio: '',
    skills: [],
    hourlyRate: 0,
  };

  formErrors: IEditFormErrors = {};

  readonly jobTitles = Object.values(JobTitle);
  readonly availableSkills = Object.values(Skills);

  ngOnInit() {
    this.loadProfile();
  }

  loadProfile() {
    this.loading = true;
    this.errorMessage = '';

    this.freelancerService.profile().subscribe({
      next: (response: any) => {
        this.freelancer = response.data;
        this.loading = false;
      },

      error: (error) => {
        console.error('Failed to load freelancer profile:', error);

        this.errorMessage = error?.error?.message || 'Failed to load profile. Please try again.';

        this.loading = false;
      },
    });
  }

  setTab(tab: ProfileTab) {
    this.activeTab = tab;
  }

  openEditMode() {
    if (!this.freelancer) {
      return;
    }

    this.editForm = {
      jobTitle: this.freelancer.jobTitle,
      bio: this.freelancer.bio ?? '',
      skills: [...this.freelancer.skills],
      hourlyRate: this.freelancer.hourlyRate,
    };

    this.formErrors = {};
    this.updateMessage = '';
    this.updateErrorMessage = '';
    this.isEditMode = true;
  }

  cancelEdit() {
    if (this.isUpdating) {
      return;
    }

    this.isEditMode = false;
    this.formErrors = {};
    this.updateMessage = '';
    this.updateErrorMessage = '';
  }

  toggleSkill(skill: Skills) {
    const currentSkills = this.editForm.skills ?? [];

    if (skill === Skills.NO_SKILLS) {
      this.editForm.skills = currentSkills.includes(skill) ? [] : [skill];

      this.formErrors.skills = undefined;
      return;
    }

    if (currentSkills.includes(Skills.NO_SKILLS)) {
      this.editForm.skills = currentSkills.filter(
        (currentSkill) => currentSkill !== Skills.NO_SKILLS,
      );
    }

    const skills = this.editForm.skills ?? [];

    if (skills.includes(skill)) {
      this.editForm.skills = skills.filter((currentSkill) => currentSkill !== skill);

      this.formErrors.skills = undefined;
      return;
    }

    if (skills.length >= this.maxSkills) {
      this.formErrors.skills = `You can select up to ${this.maxSkills} skills.`;
      return;
    }

    this.editForm.skills = [...skills, skill];

    this.formErrors.skills = undefined;
  }

  isSkillSelected(skill: Skills) {
    return this.editForm.skills?.includes(skill) ?? false;
  }

  isSkillDisabled(skill: Skills) {
    const currentSkills = this.editForm.skills ?? [];

    if (currentSkills.includes(Skills.NO_SKILLS)) {
      return skill !== Skills.NO_SKILLS;
    }

    return currentSkills.length >= this.maxSkills && !currentSkills.includes(skill);
  }

  private validateForm(): boolean {
    const errors: IEditFormErrors = {};

    if (!this.editForm.jobTitle) {
      errors.jobTitle = 'Please select a job title.';
    }

    const bioLength = this.editForm.bio?.trim().length ?? 0;
    if (bioLength > this.maxBioLength) {
      errors.bio = `Bio must not exceed ${this.maxBioLength} characters.`;
    }

    const rate = Number(this.editForm.hourlyRate);
    if (
      this.editForm.hourlyRate === null ||
      this.editForm.hourlyRate === undefined ||
      Number.isNaN(rate)
    ) {
      errors.hourlyRate = 'Hourly rate is required.';
    } else if (rate <= 0) {
      errors.hourlyRate = 'Hourly rate must be greater than 0.';
    } else if (rate > this.maxHourlyRate) {
      errors.hourlyRate = `Hourly rate must not exceed $${this.maxHourlyRate}.`;
    }

    const skillsCount = this.editForm.skills?.length ?? 0;
    if (skillsCount === 0) {
      errors.skills = 'Select at least one skill.';
    } else if (skillsCount > this.maxSkills) {
      errors.skills = `You can select up to ${this.maxSkills} skills.`;
    }

    this.formErrors = errors;

    return Object.keys(errors).length === 0;
  }

  updateProfile() {
    if (!this.freelancer || this.isUpdating) {
      return;
    }

    this.updateMessage = '';
    this.updateErrorMessage = '';

    if (!this.validateForm()) {
      this.updateErrorMessage = 'Please fix the errors below before saving.';
      return;
    }

    this.isUpdating = true;

    const payload: IUpdateFreelancer = {
      jobTitle: this.editForm.jobTitle,
      bio: this.editForm.bio?.trim(),
      skills: this.editForm.skills ?? [],
      hourlyRate: Number(this.editForm.hourlyRate),
    };

    this.freelancerService.update(payload).subscribe({
      next: (response: any) => {
        this.freelancer = response.data ?? {
          ...this.freelancer!,
          ...payload,
        };

        this.updateMessage = 'Profile updated successfully.';
        this.isUpdating = false;
        this.isEditMode = false;
      },

      error: (error) => {
        console.error('Failed to update freelancer profile:', error);

        this.updateErrorMessage =
          error?.error?.message || 'Failed to save changes. Please try again.';

        this.isUpdating = false;
      },
    }); 
  }
}
<div class="profile-page">
  @if (loading) {
    <div class="loading-state">Loading profile...</div>
  } @else if (errorMessage) {
    <div class="error-state">{{ errorMessage }}</div>
  } @else if (freelancer) {
    @if (!isEditMode) {
      <!-- Profile Header -->
      <section class="profile-header">
        <div class="header-accent"></div>

        <div class="profile-main">
          <div class="avatar-wrapper">  
            @if (user()?.avatar) {
              <img
                class="avatar" 
                [src]="user()?.avatar"
                [alt]="user()?.fullname || 'Profile avatar'"
              />
            } @else {
              <div class="avatar avatar-placeholder">
                {{ user()?.fullname?.charAt(0)?.toUpperCase() || '?' }}
              </div>
            }

            <span class="online-dot"></span>
          </div>

          <div class="profile-info">
            <div class="name-row">
              <h1>{{ user()?.fullname || 'Freelancer' }}</h1>
              <span class="verified">✓</span>
            </div>

            <p class="job-title">{{ freelancer.jobTitle }}</p>

            <div class="profile-meta">
              <span>★ {{ freelancer.rating }}</span>
              <span>{{ freelancer.completedProjects }} projects</span>
              <span>${{ freelancer.hourlyRate }}/hr</span>
            </div>
          </div>
        </div>

        <button type="button" class="edit-button" (click)="openEditMode()">Edit Profile</button>
      </section>

      <!-- Profile Tabs -->
      <nav class="profile-tabs">
        <button
          type="button"
          [class.active]="activeTab === 'overview'"
          (click)="setTab('overview')"
        >
          Overview
        </button>
        <button
          type="button"
          [class.active]="activeTab === 'projects'"
          (click)="setTab('projects')"
        >
          Projects
        </button>
        <button type="button" [class.active]="activeTab === 'reviews'" (click)="setTab('reviews')">
          Reviews
        </button>
      </nav>

      <!-- Overview Tab -->
      @if (activeTab === 'overview') {
        <section class="tab-content">
          <div class="content-card">
            <div class="card-heading"><h2>About me</h2></div>
            <p class="bio">{{ freelancer.bio || 'No biography added yet.' }}</p>
          </div>

          <div class="content-card">
            <div class="card-heading"><h2>Skills</h2></div>

            @if (freelancer.skills?.length) {
              <div class="skills">
                @for (skill of freelancer.skills; track skill) {
                  <span class="skill">{{ skill }}</span>
                }
              </div>
            } @else {
              <div class="empty-state">No skills added yet.</div>
            }
          </div>

          <div class="stats-grid">
            <div class="stat-card">
              <span>Total earnings</span>
              <strong>${{ freelancer.totalEarnings }}</strong>
            </div>
            <div class="stat-card">
              <span>Completed projects</span>
              <strong>{{ freelancer.completedProjects }}</strong>
            </div>
            <div class="stat-card">
              <span>Pending proposals</span>
              <strong>{{ freelancer.pendingProposals }}</strong>
            </div>
            <div class="stat-card">
              <span>Active projects</span>
              <strong>{{ freelancer.underImplementationProjects }}</strong>
            </div>
          </div>
        </section>
      }

      <!-- Projects Tab -->
      @if (activeTab === 'projects') {
        <section class="tab-content">
          <div class="content-card">
            <div class="card-heading">
              <h2>Projects</h2>
              <span>{{ freelancer.completedProjects }} completed</span>
            </div>
            <div class="empty-state">Projects will appear here.</div>
          </div>
        </section>
      }

      <!-- Reviews Tab -->
      @if (activeTab === 'reviews') {
        <section class="tab-content">
          <div class="content-card">
            <div class="card-heading">
              <h2>Reviews</h2>
              <div class="rating">★ {{ freelancer.rating }}</div>
            </div>
            <div class="empty-state">Reviews will appear here.</div>
          </div>
        </section>
      }
    } @else {
      <!-- Edit Mode (fully replaces the view mode) -->
      <section class="edit-card">
        <div class="edit-card-header">
          <div>
            <h2>Edit Profile</h2>
            <p>Update your professional information.</p>
          </div>
          <button type="button" class="close-button" (click)="cancelEdit()" [disabled]="isUpdating">
            ×
          </button>
        </div>

        @if (updateErrorMessage) {
          <div class="form-error">{{ updateErrorMessage }}</div>
        }
        @if (updateMessage) {
          <div class="form-success">{{ updateMessage }}</div>
        }

        <form class="edit-form" (ngSubmit)="updateProfile()" novalidate>
          <!-- Job Title -->
          <div class="form-group">
            <label for="jobTitle">Job title</label>
            <select
              id="jobTitle"
              name="jobTitle"
              [(ngModel)]="editForm.jobTitle"
              [class.invalid]="formErrors.jobTitle"
            >
              <option [ngValue]="undefined" disabled>Select a job title</option>
              @for (jobTitle of jobTitles; track jobTitle) {
                <option [ngValue]="jobTitle">{{ jobTitle }}</option>
              }
            </select>
            @if (formErrors.jobTitle) {
              <span class="field-error">{{ formErrors.jobTitle }}</span>
            }
          </div>

          <!-- Bio -->
          <div class="form-group">
            <div class="label-row">
              <label for="bio">Biography</label>
              <span
                class="char-counter"
                [class.limit-reached]="(editForm.bio?.length ?? 0) > maxBioLength"
              >
                {{ editForm.bio?.length ?? 0 }}/{{ maxBioLength }}
              </span>
            </div>
            <textarea
              id="bio"
              name="bio"
              rows="5"
              placeholder="Tell clients about yourself..."
              [(ngModel)]="editForm.bio"
              [class.invalid]="formErrors.bio"
            ></textarea>
            @if (formErrors.bio) {
              <span class="field-error">{{ formErrors.bio }}</span>
            }
          </div>

          <!-- Hourly Rate -->
          <div class="form-group">
            <label for="hourlyRate">Hourly rate</label>
            <div class="input-with-prefix">
              <span>$</span>
              <input
                id="hourlyRate"
                name="hourlyRate"
                type="number"
                min="1"
                [max]="maxHourlyRate"
                step="1"
                [(ngModel)]="editForm.hourlyRate"
                [class.invalid]="formErrors.hourlyRate"
              />
            </div>
            @if (formErrors.hourlyRate) {
              <span class="field-error">{{ formErrors.hourlyRate }}</span>
            }
          </div>

          <!-- Skills -->
          <div class="form-group">
            <div class="label-row">
              <label>Skills</label>
              <span
                class="char-counter"
                [class.limit-reached]="(editForm.skills?.length ?? 0) >= maxSkills"
              >
                {{ editForm.skills?.length ?? 0 }}/{{ maxSkills }}
              </span>
            </div>

            <div class="skill-options">
              @for (skill of availableSkills; track skill) {
                <button
                  type="button"
                  class="skill-option"
                  [class.selected]="isSkillSelected(skill)"
                  [class.disabled]="isSkillDisabled(skill)"
                  [disabled]="isSkillDisabled(skill)"
                  (click)="toggleSkill(skill)"
                >
                  {{ skill }}
                </button>
              }
            </div>

            @if (formErrors.skills) {
              <span class="field-error">{{ formErrors.skills }}</span>
            }
          </div>

          <!-- Actions -->
          <div class="form-actions">
            <button
              type="button"
              class="cancel-button"
              (click)="cancelEdit()"
              [disabled]="isUpdating"
            >
              Cancel
            </button>
            <button type="submit" class="save-button" [disabled]="isUpdating">
              @if (isUpdating) {
                Saving...
              } @else {
                Save Changes
              }
            </button>
          </div>
        </form>
      </section>
    }
  } @else {
    <div class="error-state">No freelancer profile found.</div>
  }
</div>

وهاد كود NestJS Backend 

import {
  BadRequestException,
  Inject,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { UpdateFreelancerDto } from 'src/modules/freelancers/dto/update-freelancer.dto';
import { Skills } from 'src/common/enums/freelancer.enum';
import { AuthUser } from 'src/types/auth-user.interface';
import type { IFreelancerRepository } from 'src/modules/freelancers/repository/interface/freelancer.interface';
import { assertOwnerOrAdmin, response } from 'src/common/libs/helpers/helpers';
import { messages } from 'src/common/libs/messages';

@Injectable()
export class FreelancerService {
  constructor(
    @Inject('IFreelancerRepository')
    private readonly freelancerRepository: IFreelancerRepository,
  ) {}

  async profile(freelancerId: string) {
    const profile = await this.freelancerRepository.findOne({ freelancerId });
    if (!profile) {
      throw new NotFoundException(messages.freelancer.notFound);
    }

    return response(profile, null);
  }
  async update(authUser: AuthUser, data: UpdateFreelancerDto) {
    const profile = await this.freelancerRepository.findOne({
      freelancerId: authUser._id,
    });

    if (!profile) {
      throw new NotFoundException(messages.freelancer.notFound);
    }

    assertOwnerOrAdmin({
      ownerId: profile.freelancerId,
      authUser,
      message: messages.freelancer.forbidden,
    });

    const hasData = Object.values(data).some((value) => value !== undefined);

    if (!hasData) {
      throw new BadRequestException(messages.freelancer.update.invalidSkills);
    }

    if (data.skills) {
      if (data.skills.includes(Skills.NO_SKILLS) && data.skills.length > 1) {
        throw new BadRequestException(messages.freelancer.update.invalidSkills);
      }

      if (data.skills.length === 0) {
        data.skills = [Skills.NO_SKILLS];
      }
    }

    const updatedProfile = await this.freelancerRepository.update(
      { freelancerId: authUser._id },
      data,
    );

    return response(updatedProfile, messages.freelancer.success);
  }
}
import {
  Body,
  Controller,
  Get,
  Put,
  Req,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { UpdateFreelancerDto } from 'src/modules/freelancers/dto/update-freelancer.dto';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { ResponseInterceptor } from 'src/common/interceptors/response.interceptor';
import { FreelancerService } from 'src/modules/freelancers/freelancer.service';
import type { RequestWithUser } from 'src/types/express';
import { Roles } from 'src/common/decorators/role.decorator';
import { UserRole } from 'src/common/enums/user.enum';
import { UserActiveGuard } from 'src/common/guards/user-active.guard';
import { RolesGuard } from 'src/common/guards/roles.guard';

@Controller('/api/freelancers')
@UseGuards(AuthGuard, UserActiveGuard, RolesGuard)
@UseInterceptors(ResponseInterceptor)
export class FreelancerController {
  constructor(private readonly freelancerService: FreelancerService) {}

  @Get()
  profile(@Req() req: RequestWithUser) {
    return this.freelancerService.profile(req.user._id);
  }

  @Put()
  @Roles(UserRole.FREELANCER)
  update(
    @Req() req: RequestWithUser,
    @Body() data: UpdateFreelancerDto,
  ) {
    return this.freelancerService.update(req.user, data);
  }
}
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, UpdateQuery } from 'mongoose';
import { IFreelancerRepository } from 'src/modules/freelancers/repository/interface/freelancer.interface';
import { UpdateFreelancerDto } from 'src/modules/freelancers/dto/update-freelancer.dto';
import { Freelancer, FreelancerDocument } from '../schema/freelancer.schema';

@Injectable()
export class FreelancerRepository implements IFreelancerRepository {
  constructor(
    @InjectModel(Freelancer.name)
    private readonly freelancerModel: Model<FreelancerDocument>,
  ) {}

  async findOne(filter: Partial<Freelancer>): Promise<Freelancer | null> {
    return this.freelancerModel.findOne(filter).exec();
  }

  async update(
    filter: Partial<Freelancer>,
    data: UpdateQuery<UpdateFreelancerDto>,
  ): Promise<Freelancer | null> {
    return this.freelancerModel.findOneAndUpdate(
      filter,
      { $set: data },
      { new: true },
    );
  }

  async deleteAll(where: any = {}): Promise<void> {
    await this.freelancerModel.deleteMany(where).exec();
  }
}

وهاد كود API 

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { apiUrl } from './api.url';
import { JobTitle, Skills } from '../enums/freelancer.enum';

export interface IFreelancer {
  _id: string;
  freelancerId: string;
  jobTitle: JobTitle;
  bio: string;
  skills: Skills[];
  hourlyRate: number;
  totalEarnings: number;
  rating: number;
  completedProjects: number;
  pendingProposals: number;
  rejectedProposals: number;
  underImplementationProjects: number;

  createdAt?: Date;
  updatedAt?: Date;
}

export interface IUpdateFreelancer {
  jobTitle?: JobTitle;
  bio?: string;
  skills?: Skills[];
  hourlyRate?: number;
}

@Injectable({
  providedIn: 'root',
})
export class FreelancerService {
  private readonly http = inject(HttpClient);

  profile() {
    return this.http.get(`${apiUrl}/api/freelancers`, {
      withCredentials: true,
    });
  }

  update(data: IUpdateFreelancer) {
    return this.http.put(
      `${apiUrl}/api/freelancers`,
      { data },
      {
        withCredentials: true,
      },
    );
  }
}

 

Recommended Posts

لا توجد أي إجابات على هذا السؤال بعد

انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أجب على هذا السؤال...

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.

  • إعلانات

  • تابعنا على



×
×
  • أضف...