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 @@ + +
+
Commentaires
+
+ + @for (comment of comments(); track comment.id) { +
+
+ {{ formatDate(comment.createdAt) }} + @if (comment.updatedAt) { + (modifié le {{ formatDate(comment.updatedAt) }}) + } +
+ + +
+
+ + @if (editingCommentId === comment.id) { + +
+ + +
+ } @else { +
+ } +
+ } + +
+ +
+ +
+
+ +
+
diff --git a/src/app/issues/issue-comments/issue-comments.ts b/src/app/issues/issue-comments/issue-comments.ts new file mode 100644 index 0000000..a916e52 --- /dev/null +++ b/src/app/issues/issue-comments/issue-comments.ts @@ -0,0 +1,81 @@ +import { Component, computed, inject, input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { marked } from 'marked'; +import { IssueComment, IssuesStore } from '../issues.store'; + +@Component({ + selector: 'app-issue-comments', + imports: [FormsModule], + templateUrl: './issue-comments.html', + styleUrl: './issue-comments.css', +}) +export class IssueComments { + private readonly issuesStore = inject(IssuesStore); + private readonly sanitizer = inject(DomSanitizer); + + readonly issueId = input.required(); + + 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; }