mirror of
https://github.com/a-mayb3/KanbanCloneAngular.git
synced 2026-03-21 09:55:37 +01:00
Created project managment page
This commit is contained in:
parent
dd043b3385
commit
c5b21f166d
3 changed files with 433 additions and 0 deletions
239
src/app/pages/project-details/project-details.component.css
Normal file
239
src/app/pages/project-details/project-details.component.css
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
.details-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background-color: #e2e8f0;
|
||||
color: #1f2933;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.title h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 26px;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.title p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.project-card,
|
||||
.collaborators-card,
|
||||
.tasks-card,
|
||||
.danger-zone,
|
||||
.loading,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
background: #ffffff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.project-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.collaborators-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #fee2e2;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.danger-header h3 {
|
||||
margin: 0 0 6px 0;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.danger-header p {
|
||||
margin: 0;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
.danger-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.danger-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.danger-item h4 {
|
||||
margin: 0 0 4px 0;
|
||||
color: #7f1d1d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.danger-item p {
|
||||
margin: 0;
|
||||
color: #991b1b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-danger,
|
||||
.btn-danger-outline {
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border: none;
|
||||
background-color: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-danger-outline {
|
||||
border: 1px solid #fca5a5;
|
||||
background-color: #ffffff;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-danger-outline:hover {
|
||||
border-color: #f87171;
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
.project-card h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 22px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.tasks-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.collaborators-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.collaborators-count {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.collaborators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.btn-add-task {
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background-color: #10b981;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn-add-collaborator {
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background-color: #10b981;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn-add-task:hover {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.btn-add-collaborator:hover {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.tasks-count {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.tasks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading p,
|
||||
.error-state p,
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.error-state p {
|
||||
color: #b91c1c;
|
||||
}
|
||||
86
src/app/pages/project-details/project-details.component.html
Normal file
86
src/app/pages/project-details/project-details.component.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<div class="details-container">
|
||||
<main class="content">
|
||||
@if (isLoading()) {
|
||||
<div class="loading">
|
||||
<p>Loading project...</p>
|
||||
</div>
|
||||
} @else if (errorMessage()) {
|
||||
<div class="error-state">
|
||||
<p>{{ errorMessage() }}</p>
|
||||
</div>
|
||||
} @else {
|
||||
<section class="project-card">
|
||||
<h2>{{ project()?.name }}</h2>
|
||||
<h4>Project description:</h4>
|
||||
<p class="description">
|
||||
{{ project()?.description || 'No description available.' }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="collaborators-card">
|
||||
<div class="collaborators-header">
|
||||
<h4>Collaborators</h4>
|
||||
<span class="collaborators-count"> {{ project()?.users?.length ?? 0 }} total </span>
|
||||
</div>
|
||||
|
||||
@if ((project()?.users?.length ?? 0) > 0) {
|
||||
<div class="collaborators-grid">
|
||||
@for (user of project()?.users ?? []; track user.id) {
|
||||
<app-collaborator-item [user]="user" (remove)="onRemoveCollaborator($event)" />
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="empty-state">
|
||||
<p>No collaborators yet.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn-add-collaborator" type="button">Add collaborator</button>
|
||||
</section>
|
||||
|
||||
<section class="tasks-card">
|
||||
<div class="tasks-header">
|
||||
<h4>Tasks</h4>
|
||||
<span class="tasks-count">{{ project()?.tasks?.length ?? 0 }} total</span>
|
||||
</div>
|
||||
|
||||
@if ((project()?.tasks?.length ?? 0) > 0) {
|
||||
<div class="tasks-grid">
|
||||
@for (task of project()?.tasks ?? []; track task.id) {
|
||||
<app-task-item [task]="task" />
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="empty-state">
|
||||
<p>No tasks yet.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn-add-task" type="button" (click)="onAddTask()">Add task</button>
|
||||
</section>
|
||||
|
||||
<section class="danger-zone">
|
||||
<div class="danger-header">
|
||||
<h3>Danger zone</h3>
|
||||
<p>Actions that affect this project permanently.</p>
|
||||
</div>
|
||||
<div class="danger-actions">
|
||||
<div class="danger-item">
|
||||
<div>
|
||||
<h4>Edit project details</h4>
|
||||
<p>Update the project name or description.</p>
|
||||
</div>
|
||||
<button class="btn-danger-outline" type="button">Edit project</button>
|
||||
</div>
|
||||
<div class="danger-item">
|
||||
<div>
|
||||
<h4>Delete project</h4>
|
||||
<p>This will remove the project and all associated data.</p>
|
||||
</div>
|
||||
<button class="btn-danger" type="button">Delete project</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
108
src/app/pages/project-details/project-details.component.ts
Normal file
108
src/app/pages/project-details/project-details.component.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Component, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { ProjectFull } from '../../models/projects.models';
|
||||
import { CollaboratorItemComponent } from '../../components/collaborator-item/collaborator-item.component';
|
||||
import { TaskItemComponent } from '../../components/task-item/task-item.component';
|
||||
import { User } from '../../models/auth.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-details',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CollaboratorItemComponent, TaskItemComponent],
|
||||
templateUrl: './project-details.component.html',
|
||||
styleUrl: './project-details.component.css',
|
||||
})
|
||||
export class ProjectDetailsComponent {
|
||||
private apiService = inject(ApiService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private projectId: number | null = null;
|
||||
|
||||
project = signal<ProjectFull | null>(null);
|
||||
isLoading = signal(true);
|
||||
errorMessage = signal('');
|
||||
|
||||
constructor() {
|
||||
const navState = this.router.getCurrentNavigation()?.extras.state ?? (history.state as any);
|
||||
const initialProject = navState?.project as ProjectFull | undefined;
|
||||
|
||||
if (initialProject) {
|
||||
this.project.set(initialProject);
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
|
||||
const idParam = this.route.snapshot.paramMap.get('id');
|
||||
const projectId = idParam ? Number(idParam) : Number.NaN;
|
||||
|
||||
if (!Number.isFinite(projectId)) {
|
||||
this.isLoading.set(false);
|
||||
this.errorMessage.set('Invalid project id.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.projectId = projectId;
|
||||
|
||||
this.apiService.get<ProjectFull>(`/projects/${projectId}`).subscribe({
|
||||
next: (project) => {
|
||||
this.project.set(project);
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading.set(false);
|
||||
if (!initialProject) {
|
||||
this.errorMessage.set(error?.error?.message || 'Failed to load project details.');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onBack() {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
onRemoveCollaborator(user: User) {
|
||||
const current = this.project();
|
||||
if (!current || this.projectId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = this.getUserId(user);
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousUsers = current.users ?? [];
|
||||
const updatedUsers = (current.users ?? []).filter(
|
||||
(collaborator) => this.getUserId(collaborator) !== targetId,
|
||||
);
|
||||
|
||||
this.project.set({
|
||||
...current,
|
||||
users: updatedUsers,
|
||||
});
|
||||
|
||||
this.apiService.delete<void>(`/projects/${this.projectId}/users/${targetId}`).subscribe({
|
||||
error: (error) => {
|
||||
this.project.set({
|
||||
...current,
|
||||
users: previousUsers,
|
||||
});
|
||||
this.errorMessage.set(error?.error?.message || 'Failed to remove collaborator.');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getUserId(user: User): string {
|
||||
return String(user.id ?? '');
|
||||
}
|
||||
|
||||
onAddTask() {
|
||||
if (this.projectId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate(['/projects', this.projectId, 'tasks', 'new']);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue