diff --git a/src/app/issues/issue-comments/issue-comments.css b/src/app/issues/issue-comments/issue-comments.css
new file mode 100644
index 0000000..fdef573
--- /dev/null
+++ b/src/app/issues/issue-comments/issue-comments.css
@@ -0,0 +1,92 @@
+.section-header {
+ font-size: 0.7rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: #6b7280;
+ background-color: #f9fafb;
+}
+
+.comment-item {
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #f3f4f6;
+}
+
+.comment-item:last-of-type {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.comment-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.comment-date {
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: #374151;
+}
+
+.comment-edited {
+ font-size: 0.75rem;
+ color: #9ca3af;
+ font-style: italic;
+}
+
+.comment-actions {
+ margin-left: auto;
+ display: flex;
+ gap: 0.5rem;
+}
+
+.comment-action-btn {
+ border: none;
+ background: none;
+ font-size: 0.75rem;
+ color: #6b7280;
+ cursor: pointer;
+ padding: 0;
+}
+
+.comment-action-btn:hover {
+ color: #111827;
+ text-decoration: underline;
+}
+
+.comment-action-delete:hover {
+ color: #b91c1c;
+}
+
+.comment-text {
+ font-size: 0.875rem;
+ line-height: 1.6;
+ color: #374151;
+}
+
+.comment-text :is(h1, h2, h3, h4) {
+ margin-top: 0.5rem;
+ margin-bottom: 0.25rem;
+ font-weight: 700;
+}
+
+.comment-text p {
+ margin-bottom: 0.4rem;
+}
+
+.comment-text p:last-child {
+ margin-bottom: 0;
+}
+
+.comment-text code {
+ background: #f3f4f6;
+ border-radius: 0.25rem;
+ padding: 0.1em 0.35em;
+ font-size: 0.85em;
+}
+
+.comment-new {
+ padding-top: 0.5rem;
+}
diff --git a/src/app/issues/issue-comments/issue-comments.html b/src/app/issues/issue-comments/issue-comments.html
new file mode 100644
index 0000000..0b56166
--- /dev/null
+++ b/src/app/issues/issue-comments/issue-comments.html
@@ -0,0 +1,51 @@
+
+
();
+
+ protected readonly comments = computed(
+ () => this.issuesStore.issues().find((i) => i.id === this.issueId())?.comments ?? [],
+ );
+
+ protected newCommentText = '';
+ protected editingCommentId: number | null = null;
+ protected editingCommentText = '';
+
+ protected parseMarkdown(text: string): SafeHtml {
+ const html = marked.parse(text) as string;
+ return this.sanitizer.bypassSecurityTrustHtml(html);
+ }
+
+ protected formatDate(iso: string): string {
+ return new Date(iso).toLocaleString('fr-FR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ }
+
+ protected addComment(): void {
+ const text = this.newCommentText.trim();
+ if (!text) return;
+ const issue = this.issuesStore.getById(this.issueId());
+ if (!issue) return;
+ const nextId = Math.max(0, ...issue.comments.map((c) => c.id)) + 1;
+ const comment: IssueComment = { id: nextId, text, createdAt: new Date().toISOString(), updatedAt: null };
+ this.issuesStore.upsert({ ...issue, comments: [...issue.comments, comment] });
+ this.newCommentText = '';
+ }
+
+ protected startEditComment(comment: IssueComment): void {
+ this.editingCommentId = comment.id;
+ this.editingCommentText = comment.text;
+ }
+
+ protected saveEditComment(): void {
+ const text = this.editingCommentText.trim();
+ if (!text || this.editingCommentId === null) return;
+ const issue = this.issuesStore.getById(this.issueId());
+ if (!issue) return;
+ const updatedComments = issue.comments.map((c) =>
+ c.id === this.editingCommentId ? { ...c, text, updatedAt: new Date().toISOString() } : c,
+ );
+ this.issuesStore.upsert({ ...issue, comments: updatedComments });
+ this.editingCommentId = null;
+ this.editingCommentText = '';
+ }
+
+ protected cancelEditComment(): void {
+ this.editingCommentId = null;
+ this.editingCommentText = '';
+ }
+
+ protected deleteComment(id: number): void {
+ const issue = this.issuesStore.getById(this.issueId());
+ if (!issue) return;
+ this.issuesStore.upsert({ ...issue, comments: issue.comments.filter((c) => c.id !== id) });
+ }
+}
diff --git a/src/app/issues/issue-detail/issue-detail.html b/src/app/issues/issue-detail/issue-detail.html
index b10ecc7..674ea4f 100644
--- a/src/app/issues/issue-detail/issue-detail.html
+++ b/src/app/issues/issue-detail/issue-detail.html
@@ -247,6 +247,11 @@
}
+
+@if (!isNewIssueRoute) {
+
+}
+
@if (isNewIssueRoute) {
diff --git a/src/app/issues/issue-detail/issue-detail.ts b/src/app/issues/issue-detail/issue-detail.ts
index 2810d30..0fac2d0 100644
--- a/src/app/issues/issue-detail/issue-detail.ts
+++ b/src/app/issues/issue-detail/issue-detail.ts
@@ -5,10 +5,11 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { marked } from 'marked';
import { IssueEntity, IssuesStore } from '../issues.store';
+import { IssueComments } from '../issue-comments/issue-comments';
@Component({
selector: 'app-issue-detail',
- imports: [FormsModule],
+ imports: [FormsModule, IssueComments],
templateUrl: './issue-detail.html',
styleUrl: './issue-detail.css',
})
@@ -158,6 +159,7 @@ export class IssueDetail {
description: '',
estimatedTime: null,
dependsOnIds: [],
+ comments: [],
priority: 'Moyenne',
status: 'draft',
progress: 0,
@@ -277,6 +279,7 @@ export class IssueDetail {
description: '',
estimatedTime: null,
dependsOnIds: [],
+ comments: [],
priority: 'Moyenne',
status: 'draft',
progress: 0,
@@ -296,6 +299,7 @@ export class IssueDetail {
description: '',
estimatedTime: null,
dependsOnIds: [],
+ comments: [],
priority: 'Moyenne',
status: 'draft',
progress: 0,
diff --git a/src/app/issues/issues.store.ts b/src/app/issues/issues.store.ts
index 05d4ead..908c3df 100644
--- a/src/app/issues/issues.store.ts
+++ b/src/app/issues/issues.store.ts
@@ -6,6 +6,13 @@ export type IssueStatus = 'draft' | 'todo' | 'done' | 'in-progress';
export type IssuePriority = 'Basse' | 'Moyenne' | 'Haute';
export type IssueType = 'Epic' | 'Bug' | 'Study' | 'Story' | 'Task' | 'Technical Story';
+export type IssueComment = {
+ id: number;
+ text: string;
+ createdAt: string;
+ updatedAt: string | null;
+};
+
export type IssueEntity = {
id: number;
type: IssueType;
@@ -16,6 +23,7 @@ export type IssueEntity = {
description: string;
estimatedTime: number | null;
dependsOnIds: number[];
+ comments: IssueComment[];
priority: IssuePriority;
status: IssueStatus;
progress: number;
@@ -32,6 +40,7 @@ const DEFAULT_ISSUES: IssueEntity[] = [
description: 'Corriger le comportement du menu sur petits ecrans.',
estimatedTime: 8,
dependsOnIds: [],
+ comments: [],
priority: 'Haute',
status: 'in-progress',
progress: 35,
@@ -46,6 +55,7 @@ const DEFAULT_ISSUES: IssueEntity[] = [
description: 'Fiabiliser les regles de validation du formulaire projet.',
estimatedTime: 16,
dependsOnIds: [],
+ comments: [],
priority: 'Moyenne',
status: 'todo',
progress: 20,
@@ -60,6 +70,7 @@ const DEFAULT_ISSUES: IssueEntity[] = [
description: 'Mettre a jour le wording d accueil selon la charte produit.',
estimatedTime: 4,
dependsOnIds: [],
+ comments: [],
priority: 'Basse',
status: 'done',
progress: 100,
@@ -135,6 +146,7 @@ export class IssuesStore {
type: issue.type ?? 'Story',
estimatedTime: issue.estimatedTime ?? null,
dependsOnIds: normalizedDependencies,
+ comments: Array.isArray(issue.comments) ? issue.comments : [],
} as IssueEntity;
}