Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
49 changes: 49 additions & 0 deletions app/assets/stylesheets/components/_feed.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 <details> 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);
Expand Down
18 changes: 18 additions & 0 deletions app/assets/stylesheets/components/_inspector.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 13 additions & 0 deletions app/assets/stylesheets/components/_sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions app/assets/stylesheets/components/_username_link.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.username-link__admin {
font-size: 0.85em;
text-decoration: none;
opacity: 0.85;

&:hover {
opacity: 1;
}
}
25 changes: 25 additions & 0 deletions app/assets/stylesheets/pages/projects/_show.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions app/assets/stylesheets/pages/shop/_suggestions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions app/assets/stylesheets/pages/user/_show.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down
13 changes: 11 additions & 2 deletions app/assets/stylesheets/utils.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
:root {
--color-admin: #ff9100;
--color-admin: var(--color-brand-orange);
--color-dev: #52d831;
}

Expand All @@ -20,7 +20,7 @@
border: 3px dashed;
border-radius: 5px;
display: inline-block;
padding: 4px;
padding: 6px;
}

.admin.tools-do {
Expand All @@ -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;
}
36 changes: 31 additions & 5 deletions app/components/posts/card_component.html.erb
Original file line number Diff line number Diff line change
@@ -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 %>
<article id="<%= dom_id(post) %>" class="<%= card_classes %>">
Expand All @@ -17,9 +18,8 @@
<div class="feed-post-card__reposted-by">
<%= inline_svg_tag "icons/repost.svg", class: "feed-post-card__reposted-by-icon" %>
<span>Reposted by</span>
<%= 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"}" %>
</div>
<% end %>

Expand Down Expand Up @@ -49,8 +49,7 @@
<div class="feed-post-card__meta">
<p class="feed-post-card__byline">
<% if author.present? %>
<%= link_to "@#{author_name}", profile_path(author.display_name), class: "feed-post-card__author" %>
<%= render StreakBadgeComponent.new(user: author) %><%= " ⚡" if author.admin? %>
<%= render "shared/username_link", user: author, link_class: "feed-post-card__author", text: "@#{author_name}" %>
<% else %>
<span class="feed-post-card__author">@stardancer</span>
<% end %>
Expand Down Expand Up @@ -85,6 +84,24 @@
</div>
</header>

<% if deleted_devlog? %>
<p class="feed-post-card__deleted-notice">This devlog was deleted<% if display_postable.deleted_at.present? %> on <%= l(display_postable.deleted_at.to_date, format: :long) %><% end %>.</p>
<% end %>

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

<% if fire_event? %>
<div class="feed-post-card__super-star-body markdown-content">
<!-- Hardcoded body since the old ship events had the header inside the body -->
Expand Down Expand Up @@ -278,3 +295,12 @@
</footer>
<% end %>
</article>
<% end %>

<% if deleted_devlog? %>
<%= helpers.admin_tool(roles: [ :fraud_dept ]) do %>
<%= card_content %>
<% end %>
<% else %>
<%= card_content %>
<% end %>
13 changes: 13 additions & 0 deletions app/components/posts/card_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -246,6 +253,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",
Expand Down
10 changes: 7 additions & 3 deletions app/components/sidebar_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@
<%= helpers.stardust_icon(extra_class: "sidebar__user-balance-icon") %>
<span class="sidebar__user-balance-amount"><%= number_with_delimiter(user.cached_balance) %></span>
</div>
<%= link_to profile_path(user.display_name), class: "sidebar__user-meta-handle", data: { turbo: true } do %>
@<%= user.display_name %>
<% end %>
<div class="sidebar__user-handle-row">
<%= link_to profile_path(user.display_name), class: "sidebar__user-meta-handle", data: { turbo: true } do %>
@<%= user.display_name %>
<% end %>
<%= helpers.admin_tool(compact: true) do %><%= link_to "🔨", helpers.admin_user_path(user), class: "username-link__admin", "aria-label": "View #{user.display_name} in admin" %><% end %>
<%= helpers.admin_tool(compact: true) do %>#<%= user.id %><% end %>
</div>
</div>

<div class="sidebar__user-actions">
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ def index
users = users.where("email ILIKE ? OR display_name ILIKE ? OR slack_id ILIKE ?", q, q, q)
end

@pagy, @users = pagy(:offset, users.order(:id))
# Pin the viewing admin's own row to the top of the (first page of the) list.
users = users.order(Arel.sql(User.sanitize_sql_array([ "(id = ?) DESC", current_user.id ]))).order(:id)

@pagy, @users = pagy(:offset, users)
end

def show
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/projects/devlogs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Projects::DevlogsController < ApplicationController

before_action :set_project
before_action :set_devlog, only: %i[show edit update destroy versions]
before_action :set_devlog_including_deleted, only: %i[hackatime_breakdown]
before_action :require_hackatime_project, only: %i[create]
before_action :sync_hackatime_projects, only: %i[create]

Expand Down Expand Up @@ -148,6 +149,12 @@ def versions
@versions = @devlog.versions.order(version_number: :desc)
end

def hackatime_breakdown
authorize @devlog
@breakdown = @devlog.hackatime_project_breakdown
render layout: false
end

private

def set_project
Expand All @@ -161,6 +168,24 @@ def set_devlog
.postable
end

# Post::Devlog has its own default_scope (SoftDeletable) — .postable on a
# deleted devlog's Post would otherwise silently return nil (default_scope
# applies to the preloader/association fetch regardless of the Post row
# itself being found), which then blows up `authorize` with
# Pundit::NotDefinedError for nil. Only used by hackatime_breakdown, which
# is already admin-only (Post::DevlogPolicy#hackatime_breakdown?) — unlike
# set_devlog's other callers (show, edit, ...), whose policies don't check
# deleted status themselves and rely on set_devlog silently failing to find
# deleted records as their only protection.
def set_devlog_including_deleted
@devlog = Post::Devlog.unscoped do
@project.posts
.where(postable_type: "Post::Devlog")
.find_by!(postable_id: params[:id])
.postable
end
end

def require_hackatime_project
return if test_time_granted?

Expand Down
Loading