diff --git a/internal/service/beads.go b/internal/service/beads.go index 1a9bc66..7c389a4 100644 --- a/internal/service/beads.go +++ b/internal/service/beads.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "time" "github.com/LazyBachelor/LazyPM/internal/models" @@ -51,3 +52,51 @@ func (s *BeadsService) DeleteIssues() error { } return nil } + +func (s *BeadsService) GetComments(ctx context.Context, issueID string) ([]models.Comment, error) { + query := `SELECT id, issue_id, author, text, created_at FROM comments WHERE issue_id = ? ORDER BY created_at ASC` + + rows, err := s.UnderlyingDB().QueryContext(ctx, query, issueID) + if err != nil { + return nil, err + } + defer rows.Close() + + var comments []models.Comment + for rows.Next() { + var c models.Comment + if err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil { + return nil, err + } + comments = append(comments, c) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return comments, nil +} + +func (s *BeadsService) AddComment(ctx context.Context, issueID, author, text string) (*models.Comment, error) { + query := `INSERT INTO comments (issue_id, author, text, created_at) VALUES (?, ?, ?, ?)` + + createdAt := time.Now() + result, err := s.UnderlyingDB().ExecContext(ctx, query, issueID, author, text, createdAt) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return &models.Comment{ + ID: id, + IssueID: issueID, + Author: author, + Text: text, + CreatedAt: createdAt, + }, nil +} diff --git a/pkg/web/components/comment.templ b/pkg/web/components/comment.templ new file mode 100644 index 0000000..60b6214 --- /dev/null +++ b/pkg/web/components/comment.templ @@ -0,0 +1,103 @@ +package components + +import ( + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components/base" +) + +type CommentSectionProps struct { + IssueID string + Comments []models.Comment +} + +templ CommentSection(props CommentSectionProps) { +
+

Comments

+ +
+ if len(props.Comments) == 0 { +

No comments yet. Be the first to comment!

+ } else { + for _, comment := range props.Comments { + @CommentItem(CommentItemProps{Comment: comment}) + } + } +
+ + @CommentForm(CommentFormProps{ + IssueID: props.IssueID, + }) +
+} + +type CommentListProps struct { + Comments []models.Comment +} + +templ CommentList(props CommentListProps) { + if len(props.Comments) == 0 { +

No comments yet. Be the first to comment!

+ } else { + for _, comment := range props.Comments { + @CommentItem(CommentItemProps{Comment: comment}) + } + } +} + +type CommentItemProps struct { + Comment models.Comment +} + +templ CommentItem(props CommentItemProps) { +
+
+
+ { props.Comment.Author } + { props.Comment.CreatedAt.Format("Jan 2, 2006 3:04 PM") } +
+

{ props.Comment.Text }

+
+
+} + +type CommentFormProps struct { + IssueID string + Author string + Text string + Class string +} + +templ CommentForm(props CommentFormProps) { +
+

Add a Comment

+
+ @base.Input(base.InputProps{ + Name: "author", + Label: "Your Name", + Value: props.Author, + Placeholder: "Enter your name", + Class: "w-full", + Required: true, + }) + @base.Textarea(base.TextareaProps{ + Name: "text", + Label: "Comment", + Value: props.Text, + Placeholder: "Write your comment here...", + Class: "w-full", + Required: true, + Rows: 4, + }) + +
+
+
+} diff --git a/pkg/web/components/comment_templ.go b/pkg/web/components/comment_templ.go new file mode 100644 index 0000000..41f1437 --- /dev/null +++ b/pkg/web/components/comment_templ.go @@ -0,0 +1,285 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.977 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components/base" +) + +type CommentSectionProps struct { + IssueID string + Comments []models.Comment +} + +func CommentSection(props CommentSectionProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Comments

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(props.Comments) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

No comments yet. Be the first to comment!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + for _, comment := range props.Comments { + templ_7745c5c3_Err = CommentItem(CommentItemProps{Comment: comment}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CommentForm(CommentFormProps{ + IssueID: props.IssueID, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +type CommentListProps struct { + Comments []models.Comment +} + +func CommentList(props CommentListProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if len(props.Comments) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

No comments yet. Be the first to comment!

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + for _, comment := range props.Comments { + templ_7745c5c3_Err = CommentItem(CommentItemProps{Comment: comment}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + return nil + }) +} + +type CommentItemProps struct { + Comment models.Comment +} + +func CommentItem(props CommentItemProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Comment.Author) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/comment.templ`, Line: 55, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Comment.CreatedAt.Format("Jan 2, 2006 3:04 PM")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/comment.templ`, Line: 56, Col: 102} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Comment.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/comment.templ`, Line: 58, Col: 54} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +type CommentFormProps struct { + IssueID string + Author string + Text string + Class string +} + +func CommentForm(props CommentFormProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var8 = []any{"w-full", props.Class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

Add a Comment

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = base.Input(base.InputProps{ + Name: "author", + Label: "Your Name", + Value: props.Author, + Placeholder: "Enter your name", + Class: "w-full", + Required: true, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = base.Textarea(base.TextareaProps{ + Name: "text", + Label: "Comment", + Value: props.Text, + Placeholder: "Write your comment here...", + Class: "w-full", + Required: true, + Rows: 4, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/web/components/issue.templ b/pkg/web/components/issue.templ index 9b57d9b..bf05365 100644 --- a/pkg/web/components/issue.templ +++ b/pkg/web/components/issue.templ @@ -1,6 +1,7 @@ package components import ( + "fmt" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components/base" ) @@ -101,12 +102,16 @@ templ IssueTable(props IssueTableProps) { templ IssueRows(issues []models.Issue) { for _, issue := range issues { - - { issue.ID } - { issue.Title } - { issue.Status } - { issue.IssueType } - { issue.Priority } + + + { issue.ID } + + + { issue.Title } + + { string(issue.Status) } + { string(issue.IssueType) } + { fmt.Sprintf("P%d", issue.Priority) } } } diff --git a/pkg/web/components/issue_templ.go b/pkg/web/components/issue_templ.go index a07dd0b..7e7ba72 100644 --- a/pkg/web/components/issue_templ.go +++ b/pkg/web/components/issue_templ.go @@ -9,6 +9,7 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( + "fmt" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components/base" ) @@ -70,7 +71,7 @@ func IssueForm(props IssueFormProps) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Action) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 23, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 24, Col: 25} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -224,72 +225,98 @@ func IssueRows(issues []models.Issue) templ.Component { } ctx = templ.ClearChildren(ctx) for _, issue := range issues { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"link link-primary font-mono\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 106, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 107, Col: 99} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"hover:text-primary\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType) + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 108, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 110, Col: 93} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Priority) + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.Status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 109, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 112, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.IssueType)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 113, Col: 57} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 114, Col: 68} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/handler/comments.go b/pkg/web/handler/comments.go new file mode 100644 index 0000000..b639677 --- /dev/null +++ b/pkg/web/handler/comments.go @@ -0,0 +1,72 @@ +package handler + +import ( + "net/http" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components" +) + +type CommentForm struct { + Author string `form:"author" validate:"required,max=100"` + Text string `form:"text" validate:"required,max=2000"` +} + +func ListComments(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value(issueKey).(*models.Issue) + svc := Services(r) + hx := HTMX(r) + + comments, err := svc.Beads.GetComments(r.Context(), issue.ID) + if err != nil { + http.Error(w, "Failed to retrieve comments: "+err.Error(), http.StatusInternalServerError) + return + } + + if hx.IsHxRequest() { + components.CommentList(components.CommentListProps{ + Comments: comments, + }).Render(r.Context(), w) + return + } + + w.Header().Set("Content-Type", "application/json") + hx.WriteJSON(comments) +} + +func CreateComment(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value(issueKey).(*models.Issue) + svc := Services(r) + hx := HTMX(r) + + form, err := ParseForm[CommentForm](r) + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + if err := ValidateForm(form); err != nil { + if hx.IsHxRequest() { + hx.WriteString("
Please fix the form errors
") + } else { + http.Error(w, "Validation error: "+err.Error(), http.StatusUnprocessableEntity) + } + return + } + + comment, err := svc.Beads.AddComment(r.Context(), issue.ID, form.Author, form.Text) + if err != nil { + http.Error(w, "Failed to create comment: "+err.Error(), http.StatusInternalServerError) + return + } + + if hx.IsHxRequest() { + components.CommentItem(components.CommentItemProps{ + Comment: *comment, + }).Render(r.Context(), w) + return + } + + w.Header().Set("Content-Type", "application/json") + hx.WriteJSON(comment) +} diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 9fb69a5..a11f14d 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/routes" "github.com/go-chi/chi/v5" ) @@ -97,8 +98,27 @@ func IssueCtx(next http.Handler) http.Handler { func GetIssue(w http.ResponseWriter, r *http.Request) { issue := r.Context().Value(issueKey).(*models.Issue) + svc := Services(r) hx := HTMX(r) + acceptHeader := r.Header.Get("Accept") + wantsHTML := acceptHeader == "" || (len(acceptHeader) >= 9 && acceptHeader[:9] == "text/html") + + if wantsHTML && !hx.IsHxRequest() { + comments, err := svc.Beads.GetComments(r.Context(), issue.ID) + if err != nil { + http.Error(w, "Failed to retrieve comments: "+err.Error(), http.StatusInternalServerError) + return + } + + routes.IssueDetail(routes.IssueDetailProps{ + Issue: *issue, + Comments: comments, + }).Render(r.Context(), w) + return + } + + w.Header().Set("Content-Type", "application/json") hx.WriteJSON(issue) } diff --git a/pkg/web/routes/issue_detail.templ b/pkg/web/routes/issue_detail.templ new file mode 100644 index 0000000..5a0b944 --- /dev/null +++ b/pkg/web/routes/issue_detail.templ @@ -0,0 +1,131 @@ +package routes + +import ( + "fmt" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components" +) + +type IssueDetailProps struct { + Issue models.Issue + Comments []models.Comment +} + +templ IssueDetail(props IssueDetailProps) { + @BaseLayout() { +
+
+ + + + + Back to Issues + +
+ +
+
+
+
+ { props.Issue.ID } +

{ props.Issue.Title }

+
+
+ @StatusBadge(props.Issue.Status) + @TypeBadge(props.Issue.IssueType) + @PriorityBadge(props.Issue.Priority) +
+
+ + if props.Issue.Description != "" { +
+

Description

+

{ props.Issue.Description }

+
+ } + +
+ +
+
+ Created +

{ props.Issue.CreatedAt.Format("Jan 2, 2006") }

+
+
+ Updated +

{ props.Issue.UpdatedAt.Format("Jan 2, 2006") }

+
+ if props.Issue.Assignee != "" { +
+ Assignee +

{ props.Issue.Assignee }

+
+ } + if props.Issue.Owner != "" { +
+ Owner +

{ props.Issue.Owner }

+
+ } +
+
+
+ + @components.CommentSection(components.CommentSectionProps{ + IssueID: props.Issue.ID, + Comments: props.Comments, + }) +
+ } +} + +templ StatusBadge(status models.Status) { + switch status { + case "open": + Open + case "in_progress": + In Progress + case "closed": + Closed + case "blocked": + Blocked + case "deferred": + Deferred + default: + { string(status) } + } +} + +templ TypeBadge(issueType models.IssueType) { + switch issueType { + case "bug": + Bug + case "feature": + Feature + case "task": + Task + case "chore": + Chore + case "epic": + Epic + default: + { string(issueType) } + } +} + +templ PriorityBadge(priority int) { + switch priority { + case 0: + P0 - Critical + case 1: + P1 - High + case 2: + P2 - Medium + case 3: + P3 - Low + case 4: + P4 - Minimal + default: + { fmt.Sprintf("P%d", priority) } + } +} diff --git a/pkg/web/routes/issue_detail_templ.go b/pkg/web/routes/issue_detail_templ.go new file mode 100644 index 0000000..b1e948f --- /dev/null +++ b/pkg/web/routes/issue_detail_templ.go @@ -0,0 +1,423 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.977 +package routes + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components" +) + +type IssueDetailProps struct { + Issue models.Issue + Comments []models.Comment +} + +func IssueDetail(props IssueDetailProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Back to Issues
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.ID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 30, Col: 67} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 31, Col: 62} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = StatusBadge(props.Issue.Status).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = TypeBadge(props.Issue.IssueType).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = PriorityBadge(props.Issue.Priority).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Issue.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Description

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 43, Col: 63} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Created

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.CreatedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 52, Col: 75} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Updated

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.UpdatedAt.Format("Jan 2, 2006")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 56, Col: 75} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Issue.Assignee != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
Assignee

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Assignee) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 61, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if props.Issue.Owner != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
Owner

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Owner) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 67, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.CommentSection(components.CommentSectionProps{ + IssueID: props.Issue.ID, + Comments: props.Comments, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func StatusBadge(status models.Status) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + switch status { + case "open": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Open") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "in_progress": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "In Progress") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "closed": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Closed") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "blocked": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Blocked") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "deferred": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Deferred") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(string(status)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 95, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func TypeBadge(issueType models.IssueType) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + switch issueType { + case "bug": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Bug") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "feature": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Feature") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "task": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Task") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "chore": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Chore") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case "epic": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Epic") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(string(issueType)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 112, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func PriorityBadge(priority int) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + switch priority { + case 0: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "P0 - Critical") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 1: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "P1 - High") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 2: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "P2 - Medium") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 3: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "P3 - Low") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + case 4: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "P4 - Minimal") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + default: + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", priority)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 129, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 3f16d8e..1f69bc7 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -38,6 +38,11 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { r.Get("/", handler.GetIssue) r.Patch("/", handler.UpdateIssue) r.Delete("/", handler.DeleteIssue) + + r.Route("/comments", func(r chi.Router) { + r.Get("/", handler.ListComments) + r.Post("/", handler.CreateComment) + }) }) }) return gziphandler.GzipHandler(r)