diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 97b83118c..54663720e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -29,6 +29,8 @@ @use "components/tooltip"; @use "components/like_button"; @use "components/welcome_tour"; +@use "components/inspector"; +@use "components/username_link"; @use "components/markdown_content"; @use "components/guide_content"; @use "components/guide_code"; diff --git a/app/assets/stylesheets/components/_feed.scss b/app/assets/stylesheets/components/_feed.scss index 7fbd8a5ba..01def342f 100644 --- a/app/assets/stylesheets/components/_feed.scss +++ b/app/assets/stylesheets/components/_feed.scss @@ -856,6 +856,55 @@ font-size: calc(var(--font-size-s) * 1.2); } + // Shown only inside the admin_tool box when a deleted devlog is surfaced + // to an admin/fraud_dept viewer (see Posts::CardComponent#deleted_devlog?). + &__deleted-notice { + margin: var(--space-xxs) 0 0; + padding-left: var(--feed-content-offset); + font-size: calc(var(--font-size-s) * 1.2); + color: var(--color-brand-orange); + } + + // Admin-only, lazy-loaded Hackatime project breakdown (see + // Posts::CardComponent#hackatime_breakdown_url). Collapsed by default; + // the turbo-frame inside only fetches once the
is opened. + &__hackatime-summary { + cursor: pointer; + font-size: calc(var(--font-size-s) * 1.2); + color: var(--color-space-text-muted); + + &:hover { + color: var(--color-space-text); + } + } + + &__hackatime-list { + margin: var(--space-xxs) 0 0; + padding: 0; + list-style: none; + } + + &__hackatime-item { + display: flex; + align-items: baseline; + gap: var(--space-xxs); + font-size: calc(var(--font-size-s) * 1.1); + } + + &__hackatime-name { + font-weight: 600; + } + + &__hackatime-percent { + color: var(--color-brand-orange); + } + + &__hackatime-seconds, + &__hackatime-loading, + &__hackatime-empty { + color: var(--color-space-text-muted); + } + &__body { padding-left: var(--feed-content-offset); font-size: calc(var(--font-size-s) * 1.2); diff --git a/app/assets/stylesheets/components/_inspector.scss b/app/assets/stylesheets/components/_inspector.scss new file mode 100644 index 000000000..42ab4281e --- /dev/null +++ b/app/assets/stylesheets/components/_inspector.scss @@ -0,0 +1,18 @@ +.inspector { + display: inline-block; + overflow-x: auto; + max-width: 1000px; + + &__summary { + cursor: pointer; + font-family: var(--font-family-text); + font-weight: 600; + } + + &__body { + margin: 0; + padding: 8px; + font-size: 0.75rem; + color: var(--color-space-text); + } +} diff --git a/app/assets/stylesheets/components/_sidebar.scss b/app/assets/stylesheets/components/_sidebar.scss index 52ff0ad30..a03bdaeb8 100644 --- a/app/assets/stylesheets/components/_sidebar.scss +++ b/app/assets/stylesheets/components/_sidebar.scss @@ -325,10 +325,23 @@ button.sidebar__nav-link { overflow: hidden; } +.sidebar__user-handle-row { + align-items: center; + display: flex; + gap: var(--space-xxs); + min-width: 0; + + .tools-do--inline { + flex-shrink: 0; + } +} + .sidebar__user-meta-handle { + flex-shrink: 1; font-weight: 700; font-size: var(--font-size-m); color: inherit; + min-width: 0; text-decoration: none; overflow: hidden; text-overflow: ellipsis; diff --git a/app/assets/stylesheets/components/_username_link.scss b/app/assets/stylesheets/components/_username_link.scss new file mode 100644 index 000000000..2e0725cb5 --- /dev/null +++ b/app/assets/stylesheets/components/_username_link.scss @@ -0,0 +1,9 @@ +.username-link__admin { + font-size: 0.85em; + text-decoration: none; + opacity: 0.85; + + &:hover { + opacity: 1; + } +} diff --git a/app/assets/stylesheets/pages/projects/_show.scss b/app/assets/stylesheets/pages/projects/_show.scss index e5db5ade1..548b1b44b 100644 --- a/app/assets/stylesheets/pages/projects/_show.scss +++ b/app/assets/stylesheets/pages/projects/_show.scss @@ -208,6 +208,31 @@ turbo-frame#project_hero { } } +.project-show__admin-link { + font-size: 0.85em; + text-decoration: none; + opacity: 0.85; + + &:hover { + opacity: 1; + } +} + +// Admin-editing-someone-else's-project notice. Sits inside an admin_tool +// dashed box, so it only needs its own text color, not the full pill/border +// treatment. Unlike the profile page's equivalent, this doesn't need a +// .profile--editing-style CSS reveal — the whole edit form is only rendered +// server-side when actually in edit mode, so there's no "empty box" case. +.project-show__admin-edit-notice { + margin: 0 0 var(--space-xs); + font-size: 0.85rem; + color: var(--color-brand-orange); + + strong { + font-weight: 700; + } +} + .project-show__title-input { padding: 0; border: 0; diff --git a/app/assets/stylesheets/pages/shop/_suggestions.scss b/app/assets/stylesheets/pages/shop/_suggestions.scss index 63556038d..eb2a05fe3 100644 --- a/app/assets/stylesheets/pages/shop/_suggestions.scss +++ b/app/assets/stylesheets/pages/shop/_suggestions.scss @@ -342,9 +342,10 @@ } } - &__admin-strip { + // Border/color come from the shared .admin.tools-do box (admin_tool helper); + // this only adds the card-specific layout and corner radius. + &__admin-strip.admin.tools-do { background: rgba(255, 176, 122, 0.05); - border: 2px dashed rgba(255, 176, 122, 0.4); border-radius: 0 0 12px 12px; display: flex; flex-direction: column; diff --git a/app/assets/stylesheets/pages/user/_show.scss b/app/assets/stylesheets/pages/user/_show.scss index f3552444e..8318619b4 100644 --- a/app/assets/stylesheets/pages/user/_show.scss +++ b/app/assets/stylesheets/pages/user/_show.scss @@ -599,6 +599,28 @@ turbo-frame#profile_tabs { align-items: center; } +// Wraps the admin-editing-someone-else's-profile notice. The admin_tool box +// itself (border + padding) must stay hidden until edit mode is entered — +// hiding only the text inside it still leaves an empty dashed square visible +// (mirrors .profile__banner-placeholder and friends, which reveal on the +// .profile--editing class). +.profile__admin-edit-tool { + display: none; + + .profile--editing & { + display: inline-block; + } +} + +.profile__admin-edit-notice { + font-size: 0.85rem; + color: var(--color-brand-orange); + + strong { + font-weight: 700; + } +} + // Profile action buttons (Edit / Follow / Save / Cancel). Sits inside the // set-3 profile card, so this uses the light-bg small-action variant per // branding §4.0 / §4.1. Base is the secondary action (transparent + diff --git a/app/assets/stylesheets/utils.scss b/app/assets/stylesheets/utils.scss index 0026723ff..75517f063 100644 --- a/app/assets/stylesheets/utils.scss +++ b/app/assets/stylesheets/utils.scss @@ -1,5 +1,5 @@ :root { - --color-admin: #ff9100; + --color-admin: var(--color-brand-orange); --color-dev: #52d831; } @@ -20,7 +20,7 @@ border: 3px dashed; border-radius: 5px; display: inline-block; - padding: 4px; + padding: 6px; } .admin.tools-do { @@ -32,3 +32,12 @@ background-color: color-mix(in srgb, var(--color-dev) 20%, transparent); border-color: var(--color-dev); } + +.tools-do--inline { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-left: var(--space-xxxs); + padding: 1px 4px; + border-radius: 4px; +} diff --git a/app/components/posts/card_component.html.erb b/app/components/posts/card_component.html.erb index 5117c79b3..2d4fc62b4 100644 --- a/app/components/posts/card_component.html.erb +++ b/app/components/posts/card_component.html.erb @@ -1,3 +1,4 @@ +<% card_content = capture do %> <% on_card_link_target = card_link_url.present? && request.path == card_link_url %> <% if on_card_link_target %>
@@ -17,9 +18,8 @@
<%= inline_svg_tag "icons/repost.svg", class: "feed-post-card__reposted-by-icon" %> Reposted by - <%= link_to "@#{post.user.display_name.presence || "stardancer"}", - profile_path(post.user.display_name), - class: "feed-post-card__reposted-by-link" %><%= render StreakBadgeComponent.new(user: post.user) %><%= " ⚡" if post.user.admin? %> + <%= render "shared/username_link", user: post.user, link_class: "feed-post-card__reposted-by-link", + text: "@#{post.user.display_name.presence || "stardancer"}" %>
<% end %> @@ -49,8 +49,7 @@
+ <% if deleted_devlog? %> +

This devlog was deleted<% if display_postable.deleted_at.present? %> on <%= l(display_postable.deleted_at.to_date, format: :long) %><% end %>.

+ <% end %> + + <% if devlog? && hackatime_breakdown_url.present? %> + <%= helpers.admin_tool do %> +
+ Hackatime breakdown + <%= helpers.turbo_frame_tag dom_id(postable, :hackatime_breakdown), + src: hackatime_breakdown_url, + loading: :lazy, + class: "feed-post-card__hackatime-frame" do %> +

Loading…

+ <% end %> +
+ <% end %> + <% end %> + <% if fire_event? %>
@@ -293,3 +310,12 @@ <% end %>
+<% end %> + +<% if deleted_devlog? %> + <%= helpers.admin_tool(roles: [ :fraud_dept ]) do %> + <%= card_content %> + <% end %> +<% else %> + <%= card_content %> +<% end %> diff --git a/app/components/posts/card_component.rb b/app/components/posts/card_component.rb index 2ef9a764b..01fcddeb6 100644 --- a/app/components/posts/card_component.rb +++ b/app/components/posts/card_component.rb @@ -34,6 +34,13 @@ def devlog? display_post&.postable_type == "Post::Devlog" end + # True when this card is only visible because the viewer has permission + # to see deleted devlogs (admin/fraud_dept, see ApplicationPolicy#view_deleted_devlogs?) + # — the devlog itself has been soft-deleted. + def deleted_devlog? + devlog? && display_postable.respond_to?(:deleted?) && display_postable.deleted? + end + def repost? post.postable_type == "Post::Repost" end @@ -252,6 +259,12 @@ def delete_url end end + def hackatime_breakdown_url + if devlog? && project.present? + helpers.hackatime_breakdown_project_devlog_path(project, postable) + end + end + def post_menu_data { controller: "post-menu", diff --git a/app/components/sidebar_component.html.erb b/app/components/sidebar_component.html.erb index 059cd7aae..33e163311 100644 --- a/app/components/sidebar_component.html.erb +++ b/app/components/sidebar_component.html.erb @@ -68,9 +68,13 @@ <%= helpers.stardust_icon(extra_class: "sidebar__user-balance-icon") %> <%= number_with_delimiter(user.cached_balance) %> - <%= link_to profile_path(user.display_name), class: "sidebar__user-meta-handle", data: { turbo: true } do %> - @<%= user.display_name %> - <% end %> + diff --git a/app/views/daily_rolls/leaderboard.html.erb b/app/views/daily_rolls/leaderboard.html.erb index 1171c8889..f9badab66 100644 --- a/app/views/daily_rolls/leaderboard.html.erb +++ b/app/views/daily_rolls/leaderboard.html.erb @@ -54,7 +54,7 @@
"> <%= index.zero? ? "★" : "##{index + 1}" %> <%= image_tag roll.user.avatar, alt: "", class: "rng-board__pedestal-avatar" %> - <%= link_to roll.user.display_name, profile_path(roll.user.display_name), class: "rng-board__pedestal-name" %> + <%= render "shared/username_link", user: roll.user, link_class: "rng-board__pedestal-name", text: roll.user.display_name, badge: false %> <% if current_user&.id == roll.user_id %>you<% end %> <%= formatted_roll_value(roll.total) %>
@@ -99,7 +99,7 @@ <% if @record %>

All-time best: - <%= link_to @record.user.display_name, profile_path(@record.user.display_name), class: "rng-board__alltime-name" %> + <%= render "shared/username_link", user: @record.user, link_class: "rng-board__alltime-name", text: @record.user.display_name, badge: false %> <%= formatted_roll_value(@record.total) %> on <%= link_to rng_date_label(@record.rolled_on, today: @today), rng_date_path(@record.rolled_on, today: @today), class: "rng-board__alltime-date" %>

diff --git a/app/views/leaderboard/_row.html.erb b/app/views/leaderboard/_row.html.erb index cc888e93c..8af41b95c 100644 --- a/app/views/leaderboard/_row.html.erb +++ b/app/views/leaderboard/_row.html.erb @@ -10,8 +10,7 @@

- <%= link_to user.display_name, profile_path(user.display_name), - class: "leaderboard-row__name-link" %><%= render StreakBadgeComponent.new(user: user) %><%= " ⚡" if user.admin? %> + <%= render "shared/username_link", user: user, link_class: "leaderboard-row__name-link", text: user.display_name %> <% if is_current_user %> YOU <% end %> diff --git a/app/views/notifications/inbox/_actor.html.erb b/app/views/notifications/inbox/_actor.html.erb index 9e3edb516..004cb24c3 100644 --- a/app/views/notifications/inbox/_actor.html.erb +++ b/app/views/notifications/inbox/_actor.html.erb @@ -1,7 +1,7 @@ <%# The notification's actor as a profile link, or a fallback label when the actor is missing (e.g. a deleted account). Pass `fallback:` to override. %> <% if notification.actor %> - <%= link_to "@#{notification.actor.display_name}", user_path(notification.actor), class: "notifications-item__actor" %><%= render StreakBadgeComponent.new(user: notification.actor) %><%= " ⚡" if notification.actor.admin? %> + <%= render "shared/username_link", user: notification.actor, link_class: "notifications-item__actor" %> <% else %> <%= local_assigns.fetch(:fallback, "Someone") %> <% end %> diff --git a/app/views/projects/_ship_card.html.erb b/app/views/projects/_ship_card.html.erb index 4274fb2dc..8a203d3cc 100644 --- a/app/views/projects/_ship_card.html.erb +++ b/app/views/projects/_ship_card.html.erb @@ -89,10 +89,8 @@

No Hackatime project data available for this devlog.

+ <% else %> +
    + <% @breakdown.each do |entry| %> +
  • + <%= entry[:name] %> + <%= entry[:percent] %>% + (<%= format_seconds(entry[:seconds]) %>) +
  • + <% end %> +
+ <% end %> +<% end %> diff --git a/app/views/projects/devlogs/show.html.erb b/app/views/projects/devlogs/show.html.erb index 4bd2ab6a4..c2a9a6e7f 100644 --- a/app/views/projects/devlogs/show.html.erb +++ b/app/views/projects/devlogs/show.html.erb @@ -11,6 +11,7 @@
+ <%= render "shared/inspector", record: @devlog %>