Skip to content

Commit 33b5c52

Browse files
committed
Add public profile deletion flow
1 parent fcfeada commit 33b5c52

33 files changed

Lines changed: 747 additions & 53 deletions

app/public/css/application.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ a {
3030
color: inherit;
3131
}
3232

33+
.visually-hidden {
34+
clip: rect(0 0 0 0);
35+
clip-path: inset(50%);
36+
height: 1px;
37+
overflow: hidden;
38+
position: absolute;
39+
white-space: nowrap;
40+
width: 1px;
41+
}
42+
3343
@media (prefers-reduced-motion: reduce) {
3444
html {
3545
scroll-behavior: auto;

app/public/css/pages/profiles.css

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,100 @@
555555
background: #f4f7f4;
556556
}
557557

558+
.profile-deleted {
559+
color: var(--muted);
560+
max-width: 680px;
561+
}
562+
563+
.profile-deleted h2 {
564+
color: var(--ink);
565+
}
566+
567+
.profile-delete-section {
568+
padding-top: 10px;
569+
}
570+
571+
.profile-delete {
572+
color: var(--muted);
573+
font-size: 0.95rem;
574+
text-align: center;
575+
}
576+
577+
.profile-delete p {
578+
margin-bottom: 0;
579+
}
580+
581+
.profile-delete__link {
582+
background: transparent;
583+
border: 0;
584+
color: var(--accent);
585+
cursor: pointer;
586+
font: inherit;
587+
font-weight: 800;
588+
padding: 0;
589+
text-decoration: underline;
590+
}
591+
592+
.profile-delete__link:hover {
593+
color: var(--accent-strong);
594+
}
595+
596+
.profile-delete-modal {
597+
align-items: center;
598+
background: rgba(29, 37, 33, 0.48);
599+
display: flex;
600+
inset: 0;
601+
justify-content: center;
602+
padding: 20px;
603+
position: fixed;
604+
z-index: 50;
605+
}
606+
607+
.profile-delete-modal[hidden] {
608+
display: none;
609+
}
610+
611+
.profile-delete-modal__panel {
612+
background: #ffffff;
613+
border: 1px solid var(--line);
614+
border-radius: 8px;
615+
box-shadow: 0 20px 60px rgba(29, 37, 33, 0.22);
616+
color: var(--ink);
617+
max-width: 420px;
618+
padding: 24px;
619+
text-align: center;
620+
width: 100%;
621+
}
622+
623+
.profile-delete-modal__actions {
624+
display: flex;
625+
gap: 12px;
626+
justify-content: center;
627+
margin-top: 20px;
628+
}
629+
630+
.profile-delete-modal__confirm,
631+
.profile-delete-modal__cancel {
632+
border-radius: 8px;
633+
cursor: pointer;
634+
font: inherit;
635+
font-weight: 900;
636+
min-width: 96px;
637+
padding: 10px 18px;
638+
}
639+
640+
.profile-delete-modal__confirm {
641+
background: #991b1b;
642+
border: 1px solid #991b1b;
643+
color: #ffffff;
644+
}
645+
646+
.profile-delete-modal__cancel {
647+
background: #ffffff;
648+
border: 1px solid var(--line);
649+
color: var(--ink);
650+
}
651+
558652
@media (max-width: 780px) {
559653
.organization-repository-columns,
560654
.profile-access-grid,

app/public/js/navigation.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,40 @@ function handleBadgeMarkdownCopy(event) {
200200
copyText(button.dataset.copyText || "").then(() => markCopyButton(button));
201201
}
202202

203+
function profileDeleteModal(button) {
204+
const target = button.dataset.target;
205+
return target ? document.querySelector(target) : null;
206+
}
207+
208+
function closeProfileDeleteModal(modal) {
209+
modal.hidden = true;
210+
}
211+
212+
function handleProfileDeleteModal(event) {
213+
const openButton = event.target.closest(".js-profile-delete-open");
214+
if (openButton) {
215+
const modal = profileDeleteModal(openButton);
216+
if (modal) {
217+
event.preventDefault();
218+
modal.hidden = false;
219+
modal.querySelector(".js-profile-delete-cancel")?.focus();
220+
}
221+
return;
222+
}
223+
224+
const cancelButton = event.target.closest(".js-profile-delete-cancel");
225+
if (cancelButton) {
226+
closeProfileDeleteModal(cancelButton.closest(".js-profile-delete-modal"));
227+
return;
228+
}
229+
230+
if (event.target.classList.contains("js-profile-delete-modal")) {
231+
closeProfileDeleteModal(event.target);
232+
}
233+
}
234+
203235
document.addEventListener("click", handleBadgeMarkdownCopy);
236+
document.addEventListener("click", handleProfileDeleteModal);
204237
window.addEventListener("resize", scheduleNavLayout);
205238

206239
window.addEventListener("DOMContentLoaded", () => {

app/views/profiles/subject_hero.erb

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<div class="content profile-hero__inner">
33
<p class="eyebrow"><%= h platform_name(record.fetch(:platform)) %></p>
44
<div class="profile-identity">
5+
<% profile_deleted = record[:profile_deleted] == 1 %>
56
<% avatar_url = safe_external_url(record[:avatar_url]) %>
67
<% source_url = safe_external_url(record[:html_url]) %>
78
<% homepage_url = safe_external_url(record[:homepage]) %>
@@ -13,20 +14,26 @@
1314
<div>
1415
<div class="profile-title-row">
1516
<h1><%= h(record[:name].to_s.empty? ? record.fetch(:login) : record[:name]) %></h1>
16-
<%= erb :"shared/elite_medal", locals: { rank: record[:elite_rank], size: "large" } %>
17-
</div>
18-
<p class="profile-login">@<%= h record.fetch(:login) %></p>
19-
<div class="profile-actions">
20-
<% if source_url %>
21-
<a class="profile-action" href="<%= h source_url %>"><%= h t("#{translation_scope}.links.source_profile", platform: platform_name(record.fetch(:platform))) %></a>
22-
<% end %>
23-
<% if homepage_url %>
24-
<a class="profile-action" href="<%= h homepage_url %>"><%= h t('users.links.homepage') %></a>
17+
<% unless profile_deleted %>
18+
<%= erb :"shared/elite_medal", locals: { rank: record[:elite_rank], size: "large" } %>
2519
<% end %>
2620
</div>
21+
<p class="profile-login"><%= h(profile_deleted ? t("#{translation_scope}.deleted.label") : record.fetch(:login)) %></p>
22+
<% unless profile_deleted %>
23+
<div class="profile-actions">
24+
<% if source_url %>
25+
<a class="profile-action" href="<%= h source_url %>"><%= h t("#{translation_scope}.links.source_profile", platform: platform_name(record.fetch(:platform))) %></a>
26+
<% end %>
27+
<% if homepage_url %>
28+
<a class="profile-action" href="<%= h homepage_url %>"><%= h t('users.links.homepage') %></a>
29+
<% end %>
30+
</div>
31+
<% end %>
2732
</div>
2833
</div>
2934
</div>
3035
</section>
3136

32-
<%= erb :"shared/location_notice" %>
37+
<% unless profile_deleted %>
38+
<%= erb :"shared/location_notice" %>
39+
<% end %>

app/views/profiles/user.erb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<%= erb :"profiles/subject_hero", locals: { record: @profile, translation_scope: 'users', translation_value_key: :user } %>
22

3+
<% if @profile[:profile_deleted] == 1 %>
4+
<section class="profile-section profile-section--compact" aria-labelledby="profile-deleted-heading">
5+
<div class="content profile-deleted">
6+
<h2 id="profile-deleted-heading"><%= h t('users.deleted.title') %></h2>
7+
<p><%= h t('users.deleted.body') %></p>
8+
</div>
9+
</section>
10+
<% else %>
311
<% top_repository = @repositories.first %>
412
<% spotlight_items = [
513
{ label: t('users.spotlight.country_rank'), value: (@profile[:elite_rank] ? "##{@profile[:elite_rank]}" : '-') }
@@ -77,6 +85,28 @@
7785
</div>
7886
</section>
7987

88+
<% if @show_profile_delete_control %>
89+
<section class="profile-section profile-delete-section" aria-labelledby="profile-delete-heading">
90+
<div class="content profile-delete">
91+
<h2 id="profile-delete-heading" class="visually-hidden"><%= h t('users.delete.title') %></h2>
92+
<p>
93+
<%= h t('users.delete.prompt_prefix') %>
94+
<button class="profile-delete__link js-profile-delete-open" type="button" data-target="#profile-delete-modal"><%= h t('users.delete.link') %></button>.
95+
</p>
96+
<div id="profile-delete-modal" class="profile-delete-modal js-profile-delete-modal" role="dialog" aria-modal="true" aria-labelledby="profile-delete-modal-heading" hidden>
97+
<div class="profile-delete-modal__panel">
98+
<p id="profile-delete-modal-heading"><%= h t('users.delete.confirm') %></p>
99+
<form class="profile-delete-modal__actions" method="post" action="<%= h app_path("#{user_profile_path(@profile)}/delete") %>">
100+
<input type="hidden" name="csrf_token" value="<%= h csrf_token %>">
101+
<button class="profile-delete-modal__confirm" type="submit"><%= h t('users.delete.confirm_yes') %></button>
102+
<button class="profile-delete-modal__cancel js-profile-delete-cancel" type="button"><%= h t('users.delete.confirm_no') %></button>
103+
</form>
104+
</div>
105+
</div>
106+
</div>
107+
</section>
108+
<% end %>
109+
80110
<section class="profile-section profile-section--alt" aria-labelledby="profile-repositories-heading">
81111
<div class="content">
82112
<h2 id="profile-repositories-heading"><%= h t('users.repositories.title') %></h2>
@@ -88,3 +118,4 @@
88118
} %>
89119
</div>
90120
</section>
121+
<% end %>

app/views/rankings/table.erb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
<div class="ranking-list__title">
1111
<%= erb :"shared/platform_icon", locals: { platform: row.fetch(:platform, "github") } %>
1212
<% if kind == "users" %>
13-
<a class="primary-link" href="<%= h app_path(user_profile_path(row)) %>"><%= h row.fetch(:login) %></a>
13+
<% if row[:profile_deleted] == 1 %>
14+
<span class="primary-link primary-link--static"><%= h row.fetch(:login) %></span>
15+
<% else %>
16+
<a class="primary-link" href="<%= h app_path(user_profile_path(row)) %>"><%= h row.fetch(:login) %></a>
17+
<% end %>
1418
<% elsif kind == "repositories" %>
1519
<a class="primary-link" href="<%= h app_path(repository_profile_path(row)) %>"><%= h row.fetch(:full_name) %></a>
1620
<% elsif kind == "organizations" %>
@@ -23,7 +27,9 @@
2327
<% if kind == "users" || kind == "organizations" %>
2428
<% location = row[:location_raw] || row[:city] || row[:country] || t('scope.poland') %>
2529
<span class="ranking-list__description">
26-
<% if row[:name] && !row[:name].empty? %>
30+
<% if kind == "users" && row[:profile_deleted] == 1 %>
31+
<%= h t('users.deleted.label') %>
32+
<% elsif row[:name] && !row[:name].empty? %>
2733
<%= h row[:name] %>
2834
<% end %>
2935
</span>

config/locales/en.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,17 @@ en:
527527
poland_rank: "Poland rank"
528528
status: "Discord"
529529
title: "Your Discord access"
530+
delete:
531+
confirm: "Are you sure you want to remove your profile from the database?"
532+
confirm_no: "No"
533+
confirm_yes: "Yes"
534+
link: "here"
535+
prompt_prefix: "Do you want to remove this page from the database? Click"
536+
title: "Profile deletion"
537+
deleted:
538+
body: "This profile has been removed from the database."
539+
label: "Deleted"
540+
title: "Profile deleted"
530541
profile_access:
531542
title: "Access and badge"
532543
repository_badges:

config/locales/pl.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,17 @@ pl:
529529
poland_rank: "Ranking Polski"
530530
status: "Discord"
531531
title: "Twój dostęp Discord"
532+
delete:
533+
confirm: "Czy na pewno chcesz usunąć twój profil z bazy danych?"
534+
confirm_no: "Nie"
535+
confirm_yes: "Tak"
536+
link: "tutaj"
537+
prompt_prefix: "Chcesz usunąć tę stronę z bazy danych? Kliknij"
538+
title: "Usunięcie profilu"
539+
deleted:
540+
body: "Ten profil został usunięty z bazy danych."
541+
label: "Usunięte"
542+
title: "Profil usunięty"
532543
profile_access:
533544
title: "Dostęp i odznaka"
534545
repository_badges:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module PolishOpenSourceRank
4+
module Contexts
5+
module Publication
6+
module Application
7+
class DeletePublicProfile
8+
def initialize(profile_repository:)
9+
@profile_repository = profile_repository
10+
end
11+
12+
def call(platform:, source_id:)
13+
profile_repository.redact_profile(platform: platform, source_id: source_id)
14+
end
15+
16+
private
17+
18+
attr_reader :profile_repository
19+
end
20+
end
21+
end
22+
end
23+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
module PolishOpenSourceRank
4+
module Contexts
5+
module Publication
6+
module Infrastructure
7+
module SQLite
8+
class ReplicatedPublicProfileRepository
9+
def initialize(repositories)
10+
@repositories = repositories
11+
end
12+
13+
def upsert_github_profile(attributes)
14+
repositories.each { it.upsert_github_profile(attributes) }
15+
end
16+
17+
def redact_profile(platform:, source_id:)
18+
repositories.each { it.redact_profile(platform: platform, source_id: source_id) }
19+
end
20+
21+
private
22+
23+
attr_reader :repositories
24+
end
25+
end
26+
end
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)