From f37c8784f5fa9d9ec6a444802eb6ec14526843ef Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 16 May 2026 14:48:48 +0300 Subject: [PATCH] issues: card-style posts, sidebar tabs with mentioned/activity feeds --- issue.v | 70 ++++++++++- issue_routes.v | 79 ++++++------ static/css/gitly.scss | 78 ++++++++++++ static/css/issues.scss | 188 +++++++++++++++++++++++----- templates/_issue_list.html | 37 ++++++ templates/issue.html | 45 ++++--- templates/issues.html | 40 +----- templates/user/_issues_sidebar.html | 22 ++++ templates/user/issues.html | 71 ++++------- translations/cn.tr | 49 +++++++- translations/en.tr | 45 +++++++ translations/es.tr | 49 +++++++- translations/jp.tr | 49 +++++++- translations/pt.tr | 49 +++++++- translations/ru.tr | 45 +++++++ 15 files changed, 742 insertions(+), 174 deletions(-) create mode 100644 templates/_issue_list.html create mode 100644 templates/user/_issues_sidebar.html diff --git a/issue.v b/issue.v index 8d3312f..1fa229a 100644 --- a/issue.v +++ b/issue.v @@ -138,10 +138,78 @@ fn (mut app App) get_repo_issue_count(repo_id int) int { fn (mut app App) find_user_issues(user_id int) []Issue { return sql app.db { - select from Issue where author_id == user_id && is_pr == false + select from Issue where author_id == user_id && is_pr == false order by created_at desc } or { []Issue{} } } +fn (mut app App) find_user_mentioned_issues(username string) []Issue { + needle := '@' + username + mut seen := map[int]bool{} + mut result := []Issue{} + direct_rows := db_exec_values(app.db, + 'select id from ${sql_table('Issue')} where is_pr = 0 and text like ${sql_like_pattern(needle)} order by created_at desc') or { + [][]string{} + } + for row in direct_rows { + id := row[0].int() + if id in seen { + continue + } + issue := app.find_issue_by_id(id) or { continue } + seen[id] = true + result << issue + } + comment_rows := db_exec_values(app.db, + 'select distinct issue_id from ${sql_table('Comment')} where text like ${sql_like_pattern(needle)}') or { + [][]string{} + } + for row in comment_rows { + id := row[0].int() + if id in seen { + continue + } + issue := app.find_issue_by_id(id) or { continue } + if issue.is_pr { + continue + } + seen[id] = true + result << issue + } + result.sort(a.created_at > b.created_at) + return result +} + +fn (mut app App) find_user_recent_issues(user_id int) []Issue { + mut seen := map[int]bool{} + mut result := []Issue{} + authored := app.find_user_issues(user_id) + for issue in authored { + if issue.id in seen { + continue + } + seen[issue.id] = true + result << issue + } + comment_rows := db_exec_values(app.db, + 'select distinct issue_id from ${sql_table('Comment')} where author_id = ${user_id}') or { + [][]string{} + } + for row in comment_rows { + id := row[0].int() + if id in seen { + continue + } + issue := app.find_issue_by_id(id) or { continue } + if issue.is_pr { + continue + } + seen[id] = true + result << issue + } + result.sort(a.created_at > b.created_at) + return result +} + fn (mut app App) delete_repo_issues(repo_id int) ! { sql app.db { delete from Issue where repo_id == repo_id diff --git a/issue_routes.v b/issue_routes.v index 94826ed..ed19676 100644 --- a/issue_routes.v +++ b/issue_routes.v @@ -39,7 +39,7 @@ pub fn (mut app App) new_issue(username string, repo_name string) veb.Result { @['/:username/issues'] pub fn (mut app App) handle_get_user_issues(mut ctx Context, username string) veb.Result { - return app.user_issues(mut ctx, username, '0') + return app.user_issues(mut ctx, username, 'created') } @['/:username/:repo_name/issues'; post] @@ -90,11 +90,14 @@ pub fn (mut app App) issues(mut ctx Context, username string, repo_name string, issue = repo_issues[i] user = app.get_user_by_id(issue.author_id) or { continue } issue.labels = app.get_issue_labels(issue.id) + issue.repo_author = repo.user_name + issue.repo_name = repo.name issues_with_users << IssueWithUser{ item: issue user: user } } + show_repo_link := false mut first := false mut last := false if repo.nr_open_issues > commits_per_page { @@ -137,8 +140,8 @@ pub fn (mut app App) issue(mut ctx Context, username string, repo_name string, i return $veb.html() } -@['/:username/issues/:page'] -pub fn (mut app App) user_issues(mut ctx Context, username string, page string) veb.Result { +@['/:username/issues/:tab'] +pub fn (mut app App) user_issues(mut ctx Context, username string, tab string) veb.Result { if !ctx.logged_in { return ctx.not_found() } @@ -149,47 +152,53 @@ pub fn (mut app App) user_issues(mut ctx Context, username string, page string) if !exists { return ctx.not_found() } - page_i := page.int() - mut issues := app.find_user_issues(user.id) - mut first := false - mut last := false - mut issue := Issue{} + current_tab := if tab in ['assigned', 'created', 'mentioned', 'activity'] { + tab + } else { + 'created' + } + mut issues := match current_tab { + 'assigned' { []Issue{} } + 'mentioned' { app.find_user_mentioned_issues(user.username) } + 'activity' { app.find_user_recent_issues(user.id) } + else { app.find_user_issues(user.id) } + } + mut issue_repo := Repo{} - mut i := 0 - for i = 0; i < issues.len; i++ { - issue = issues[i] + for mut issue in issues { issue_repo = app.find_repo_by_id(issue.repo_id) or { continue } - issues[i].repo_author = issue_repo.user_name - issues[i].repo_name = issue_repo.name - } - if issues.len > commits_per_page { - offset := page_i * commits_per_page - delta := issues.len - offset - if delta > 0 { - if delta == issues.len && page_i == 0 { - first = true - } else { - last = true - } - } - } else { - last = true - first = true + issue.repo_author = issue_repo.user_name + issue.repo_name = issue_repo.name + issue.labels = app.get_issue_labels(issue.id) } mut issues_with_users := []IssueWithUser{} - mut issue_author := User{} - for i = 0; i < issues.len; i++ { - issue = issues[i] - issue_author = app.get_user_by_id(issue.author_id) or { continue } + for issue in issues { + issue_author := app.get_user_by_id(issue.author_id) or { continue } issues_with_users << IssueWithUser{ item: issue user: issue_author } } - mut last_site := 0 - if page_i > 0 { - last_site = page_i - 1 + tab_assigned_class := if current_tab == 'assigned' { + 'user-issues-sidebar__item user-issues-sidebar__item--active' + } else { + 'user-issues-sidebar__item' + } + tab_created_class := if current_tab == 'created' { + 'user-issues-sidebar__item user-issues-sidebar__item--active' + } else { + 'user-issues-sidebar__item' + } + tab_mentioned_class := if current_tab == 'mentioned' { + 'user-issues-sidebar__item user-issues-sidebar__item--active' + } else { + 'user-issues-sidebar__item' + } + tab_activity_class := if current_tab == 'activity' { + 'user-issues-sidebar__item user-issues-sidebar__item--active' + } else { + 'user-issues-sidebar__item' } - next_site := page_i + 1 + show_repo_link := true return $veb.html() } diff --git a/static/css/gitly.scss b/static/css/gitly.scss index 8ef9e63..274ec48 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -1152,6 +1152,84 @@ form { color: $gray-dark; } +.issue-row__repo { + color: $link-color !important; + font-size: 12px !important; + + &:hover { + text-decoration: underline; + } +} + +.user-issues-layout { + display: grid; + grid-template-columns: 240px 1fr; + gap: 24px; + margin-top: 16px; + + @include mobile { + grid-template-columns: 1fr; + gap: 12px; + } +} + +.user-issues-sidebar { + display: flex; + flex-direction: column; + gap: 2px; +} + +.user-issues-sidebar__heading { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px 12px; + font-size: 16px; + font-weight: 600; + color: $black; + border-bottom: 1px solid $gray; + margin-bottom: 8px; +} + +.user-issues-sidebar__heading-icon { + width: 20px; + height: 20px; + color: $black; +} + +.user-issues-sidebar__item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 10px; + border-radius: $small-radius; + color: $black !important; + font-size: 14px; + transition: background-color 0.07s; + + &:hover { + background-color: $gray-light; + text-decoration: none; + } +} + +.user-issues-sidebar__item--active { + background-color: $gray-light; + font-weight: 600; + box-shadow: inset 2px 0 0 $link-color; +} + +.user-issues-sidebar__icon { + width: 16px; + height: 16px; + flex-shrink: 0; + color: $gray-dark; +} + +.user-issues-main { + min-width: 0; +} + .lang_select { margin-right: 16px; display: inline-flex; diff --git a/static/css/issues.scss b/static/css/issues.scss index 1f97e23..cc0b09d 100644 --- a/static/css/issues.scss +++ b/static/css/issues.scss @@ -1,49 +1,179 @@ -.issue-main-post { - border: 1px solid #dfdfdf; - border-radius: 3px; - padding: 10px; - margin-top: 10px; +.issue-header { + margin: 20px 0 16px; } -.issue-comment-post { - border: 1px solid #dfdfdf; - border-radius: 3px; - padding: 10px; - margin-top: 10px; +.issue-header__title { + font-size: 24px; + font-weight: 600; + line-height: 1.3; + margin: 0; + padding: 0; + word-wrap: break-word; + overflow-wrap: break-word; } -.comment-post-form { - display: grid; +.issue-header__id { + color: #57606a; + font-weight: 400; + margin-left: 4px; } -.comment-post-submit { - margin-top: 5px; +.issue-post { + display: grid; + grid-template-columns: 40px 1fr; + gap: 14px; + padding: 14px 16px; + border: 1px solid #d0d7de; + border-radius: 6px; + margin-bottom: 14px; + background: #fff; } -.comment { - margin-left: 20px; - margin-top: 20px; +.issue-post__avatar { + display: block; + + img { + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid #d0d7de; + display: block; + } } -.form .input-comment { - margin-top: 10px; +.issue-post__body { + min-width: 0; } -.avatar-with-user-info { +.issue-post__meta { display: flex; - align-items: center; + flex-wrap: wrap; + gap: 6px; + align-items: baseline; + font-size: 13px; + color: #57606a; + padding-bottom: 8px; + margin-bottom: 10px; + border-bottom: 1px solid #eaeef2; +} - img { - height: 24px; - width: 24px; +.issue-post__author { + font-weight: 600; + color: #24292f; +} + +.issue-post__time { + color: #57606a; +} + +.issue-post__content { + font-size: 14px; + line-height: 1.55; + color: #24292f; + word-wrap: break-word; + overflow-wrap: break-word; + + p { + margin: 0 0 12px; + } + + p:last-child { + margin-bottom: 0; + } +} + +.markdown-body.issue-post__content, +.issue-post__content.markdown-body { + h1, h2, h3, h4 { + font-weight: 600; + margin: 18px 0 8px; + padding: 0; + line-height: 1.3; + } + + h1 { font-size: 18px; } + h2 { font-size: 16px; } + h3 { font-size: 15px; } + h4 { font-size: 14px; } + + > *:first-child { + margin-top: 0; + } - margin-top: 5px; + ul, ol { + margin: 0 0 12px 22px; + padding: 0; + } + + li { + margin: 4px 0; + } + + a { + color: #0969da; + } + + code { + background: #f6f8fa; + padding: 1px 5px; + border-radius: 3px; + font-size: 90%; + } + + pre { + background: #f6f8fa; + padding: 12px; + border-radius: 6px; + overflow-x: auto; + } - border-radius: 100%; - border: 1px solid #24292e; + blockquote { + border-left: 3px solid #d0d7de; + padding: 0 12px; + color: #57606a; + margin: 0 0 12px; } +} + +.comment-form { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 18px; +} + +.comment-form__input { + box-sizing: border-box; + width: 100%; + min-height: 100px; + padding: 10px 12px; + font-size: 14px; + font-family: inherit; + line-height: 1.5; + resize: vertical; +} + +.comment-form__actions { + display: flex; + justify-content: flex-end; +} - span { - margin-left: 10px; +.comment-form__submit { + width: auto !important; + padding: 6px 16px; + background-color: #1f883d; + color: #fff; + border: 1px solid rgba(31, 136, 61, 0.6); + font-weight: 500; + + &:hover { + background-color: #1a7f37; + border-color: rgba(27, 31, 36, 0.15); + color: #fff; } } + +.comment { + margin-left: 20px; + margin-top: 20px; +} diff --git a/templates/_issue_list.html b/templates/_issue_list.html new file mode 100644 index 0000000..2a06c60 --- /dev/null +++ b/templates/_issue_list.html @@ -0,0 +1,37 @@ + diff --git a/templates/issue.html b/templates/issue.html index 5f2bf7e..f8e7f74 100644 --- a/templates/issue.html +++ b/templates/issue.html @@ -13,32 +13,45 @@
@include 'layout/repo_menu.html' -
-

@{issue.formatted_title()} #@issue.id

-