modal refresh (#636)

* modal refresh

* fix up yields and custom injections
This commit is contained in:
Echo
2025-11-17 17:37:38 -05:00
committed by GitHub
parent 25ea2c00db
commit ed7fcc8680
11 changed files with 234 additions and 158 deletions

View File

@@ -227,10 +227,6 @@ select {
animation: bounce 1s infinite;
}
#scroll-arrow {
transition: transform 1s ease-in-out;
}
@keyframes bounce {
0%,
100% {

View File

@@ -181,4 +181,12 @@ module ApplicationHelper
else language.capitalize
end
end
def modal_open_button(modal_id, text, **options)
button_tag text, {
type: "button",
data: { action: "click->modal#open" },
onclick: "document.getElementById('#{modal_id}').querySelector('[data-controller=\"modal\"]').dispatchEvent(new CustomEvent('modal:open', { bubbles: true }))"
}.merge(options)
end
end

View File

@@ -17,42 +17,11 @@ function setupCurrentlyHacking() {
function outta() {
// we should figure out a better way of doing this rather than this shit ass way, but it works for now
const modal = document.getElementById('logout-modal');
const can = document.getElementById('cancel-logout');
if (!modal) return;
if (!modal || !can) return;
modal.classList.remove('hidden');
function logshow() {
modal.classList.remove('pointer-events-none');
modal.classList.remove('opacity-0');
modal.querySelector('.bg-dark').classList.remove('scale-95');
modal.querySelector('.bg-dark').classList.add('scale-100');
}
function logquit() {
modal.classList.add('opacity-0');
modal.querySelector('.bg-dark').classList.remove('scale-100');
modal.querySelector('.bg-dark').classList.add('scale-95');
setTimeout(() => {
modal.classList.add('pointer-events-none');
}, 300);
}
window.showLogout = logshow;
can.addEventListener('click', logquit);
modal.addEventListener('click', function(e) {
if (e.target === modal) {
logquit();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && !modal.classList.contains('pointer-events-none')) {
logquit();
}
});
window.showLogout = function() {
modal.dispatchEvent(new CustomEvent('modal:open', { bubbles: true }));
};
}
function weirdclockthing() {
@@ -108,16 +77,6 @@ function weirdclockthing() {
}
}
function scrollarrow() {
window.addEventListener('scroll', function () {
const arrow = document.getElementById('scroll-arrow');
arrow.classList.remove('bounce');
this.setInterval(() => {
arrow.style.transform = 'translateY(200px)';
}, 100);
});
}
// Handle both initial page load and subsequent Turbo navigations
document.addEventListener('turbo:load', function() {
setupCurrentlyHacking();
@@ -133,5 +92,4 @@ document.addEventListener('DOMContentLoaded', function() {
setupCurrentlyHacking();
outta();
weirdclockthing();
scrollarrow();
});

View File

@@ -1,98 +1,87 @@
import { Controller } from "@hotwired/stimulus"
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
async rotateKey(event) {
event.preventDefault()
if (!confirm("Are you sure you want to rotate your API key? Your old key will be immediately invalidated and you'll need to update it in all your applications.")) {
return
}
confirm(event) {
event.preventDefault();
document
.getElementById("api-key-confirm-modal")
.dispatchEvent(new CustomEvent("modal:open", { bubbles: true }));
}
const button = event.currentTarget
const og = button.textContent
button.textContent = "Rotating..."
button.disabled = true
async rotate(event) {
event.preventDefault();
const b = event.currentTarget;
const og = b.textContent;
b.textContent = "Rotating...";
b.disabled = true;
try {
const r = await fetch("/my/settings/rotate_api_key", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector("[name='csrf-token']").content
}
})
"X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
},
});
const d = await r.json()
const data = await r.json();
if (r.ok && d.token) {
this.m(d.token)
if (r.ok && data.token) {
this.done(data.token);
} else {
alert("Failed to rotate API key: " + (d.error || "Unknown error"))
alert("Failed to rotate API key: " + (data.error || "Unknown error"));
}
} catch (error) {
console.error("Error rotating API key:", error)
alert("Failed to rotate API key. Please try again.")
console.error("Error rotating API key:", error);
alert("Failed to rotate API key. Please try again.");
} finally {
button.textContent = og
button.disabled = false
b.textContent = og;
b.disabled = false;
}
}
m(token) {
const c = `
<div id="api-key-modal" class="fixed inset-0 flex items-center justify-center z-[9999]" style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);">
<div class="bg-darker rounded-lg p-6 max-w-md w-full mx-4 border border-gray-600">
<h3 class="text-xl font-bold text-white mb-4">🔑 New API Key Generated</h3>
<div class="space-y-4">
<div>
<p class="text-sm text-gray-300 mb-3">
We have gone ahead and invalidated your old API key, here is your new API key. Update your editor configuration with this new key.
</p>
<div class="bg-gray-800 border border-gray-600 rounded p-3">
<code id="new-api-key" class="text-sm text-white break-all">${token}</code>
</div>
</div>
<div class="flex gap-3 pt-2">
<button type="button" id="copy-api-key"
class="flex-1 bg-primary hover:bg-red text-white px-4 py-2 rounded-lg transition-colors">
Copy Key
</button>
<button type="button" id="close-modal"
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition-colors">
Close
</button>
</div>
</div>
</div>
</div>
`
done(token) {
const d = document.getElementById("new-api-key-display");
document.body.insertAdjacentHTML("beforeend", c)
if (!d) {
console.error("Could not find new-api-key-display element");
alert("Error displaying new API key. Please refresh and try again.");
return;
}
document.getElementById("copy-api-key").addEventListener("click", () => {
navigator.clipboard.writeText(token).then(() => {
const button = document.getElementById("copy-api-key")
const og = button.textContent
button.textContent = "Copied!"
setTimeout(() => {
button.textContent = og
}, 2000)
})
})
d.textContent = token;
d.dataset.token = token;
document
.getElementById("api-key-confirm-modal")
.dispatchEvent(new CustomEvent("modal:close", { bubbles: true }));
document.getElementById("close-modal").addEventListener("click", this.closeModal)
document.getElementById("api-key-modal").addEventListener("click", (event) => {
if (event.target.id === "api-key-modal") {
this.closeModal()
}
})
setTimeout(() => {
document
.getElementById("api-key-success-modal")
.dispatchEvent(new CustomEvent("modal:open", { bubbles: true }));
}, 150);
}
closeModal() {
const modal = document.getElementById("api-key-modal")
if (modal) {
modal.remove()
copyKey(event) {
event.preventDefault();
const d = document.getElementById("new-api-key-display");
if (!d) {
console.error("modal issues???");
return;
}
const t = d.dataset.token;
const b = event.currentTarget;
navigator.clipboard.writeText(t).then(() => {
const og = b.textContent;
b.textContent = "Copied!";
setTimeout(() => {
b.textContent = og;
}, 2000);
});
}
}

View File

@@ -0,0 +1,34 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content"]
connect() {
this.element.addEventListener("click", (e) => {
if (e.target === this.element) {
this.close()
}
})
this.element.addEventListener("modal:open", () => this.open())
this.element.addEventListener("modal:close", () => this.close())
}
open() {
this.element.classList.remove("hidden")
setTimeout(() => {
this.element.classList.remove("opacity-0", "pointer-events-none")
this.contentTarget.classList.remove("scale-95")
this.contentTarget.classList.add("scale-100")
}, 10)
}
close() {
this.element.classList.add("opacity-0", "pointer-events-none")
this.contentTarget.classList.remove("scale-100")
this.contentTarget.classList.add("scale-95")
setTimeout(() => {
this.element.classList.add("hidden")
}, 300)
}
}

View File

@@ -231,31 +231,25 @@
<div data-currently-hacking-target="content" style="display: none;">
</div>
</div>
<div id="logout-modal" class="fixed inset-0 flex items-center justify-center z-[9999] opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out hidden" style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);">
<div class="bg-dark border border-primary rounded-lg p-6 max-w-md w-full mx-4 flex flex-col items-center justify-center transform scale-95 transition-transform duration-300 ease-in-out">
<div class="flex flex-col items-center w-full">
<div class="mb-4 flex justify-center w-full">
<svg class="w-12 h-12 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path fill="currentColor" d="M5 5h6c.55 0 1-.45 1-1s-.45-1-1-1H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h6c.55 0 1-.45 1-1s-.45-1-1-1H5z" /><path fill="currentColor" d="m20.65 11.65l-2.79-2.79a.501.501 0 0 0-.86.35V11h-7c-.55 0-1 .45-1 1s.45 1 1 1h7v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.19.2-.51.01-.7" />
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-2 text-center w-full">Woah hold on a sec</h3>
<p class="text-gray-300 mb-6 text-center w-full">You sure you want to sign out? You can sign back in later but that is a bit of a hassle...</p>
<div class="flex w-full h-[40px] gap-3">
<button id="cancel-logout" class="flex-1 px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-darkless transition-colors duration-200 cursor-pointer m-0 h-[40px]">
nevermind
</button>
<form method="post" action="<%= signout_path %>" class="flex-1">
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<input type="hidden" name="_method" value="delete">
<button type="submit" class="w-full px-4 py-2 bg-primary text-white rounded-lg hover:bg-red-600 transition-colors duration-200 font-medium cursor-pointer m-0 h-[40px]">
get me outta here
</button>
</form>
</div>
</div>
</div>
</div>
<%= render "shared/modal",
modal_id: "logout-modal",
title: "Woah hold on a sec",
description: "You sure you want to sign out? You can sign back in later but that is a bit of a hassle...",
icon_svg: '<path fill="currentColor" d="M5 5h6c.55 0 1-.45 1-1s-.45-1-1-1H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h6c.55 0 1-.45 1-1s-.45-1-1-1H5z" /><path fill="currentColor" d="m20.65 11.65l-2.79-2.79a.501.501 0 0 0-.86.35V11h-7c-.55 0-1 .45-1 1s.45 1 1 1h7v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.19.2-.51.01-.7" />',
icon_color: "text-primary",
buttons: [
{
text: "Go back",
class: "border border-gray-600 text-gray-300 hover:bg-darkless",
action: "click->modal#close"
},
{
text: "Log out now",
class: "bg-primary text-white hover:bg-red-600 font-medium",
form: true,
url: signout_path,
method: "delete"
}
] %>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<%
modal_id ||= "modal-#{SecureRandom.hex(4)}"
icon_svg ||= nil
icon_color ||= "text-primary"
title ||= "Confirm"
description ||= nil
buttons ||= []
max_width ||= "max-w-md"
custom ||= nil
%>
<div id="<%= modal_id %>"
class="fixed inset-0 flex items-center justify-center z-9999 opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out hidden"
style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);"
data-controller="modal">
<div class="bg-dark border border-primary rounded-lg p-6 <%= max_width %> w-full mx-4 flex flex-col items-center justify-center transform scale-95 transition-transform duration-300 ease-in-out"
data-modal-target="content">
<div class="flex flex-col items-center w-full">
<% if icon_svg %>
<div class="mb-4 flex justify-center w-full">
<svg class="w-12 h-12 <%= icon_color %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<%= icon_svg.html_safe %>
</svg>
</div>
<% end %>
<h3 class="text-2xl font-bold text-white mb-2 text-center w-full"><%= title %></h3>
<% if description %>
<p class="text-gray-300 mb-6 text-center w-full"><%= description %></p>
<% end %>
<% if custom.present? %>
<%= custom.html_safe %>
<% end %>
<% if buttons.any? %>
<div class="flex w-full gap-2 items-center justify-center">
<% buttons.each do |button| %>
<% if button[:form] %>
<form method="post" action="<%= button[:url] %>" class="flex-1 basis-1/2 items-center justify-center w-full h-10 m-0">
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<% if button[:method] && button[:method] != 'post' %>
<input type="hidden" name="_method" value="<%= button[:method] %>">
<% end %>
<button type="submit"
class="w-full h-10 px-4 rounded-lg transition-colors duration-200 font-medium cursor-pointer m-0 <%= button[:class] %>">
<%= button[:text] %>
</button>
</form>
<% else %>
<button type="button"
data-action="<%= button[:action] || 'click->modal#close' %>"
class="flex-1 basis-1/2 items-center justify-center w-full h-10 px-4 rounded-lg transition-colors duration-200 cursor-pointer m-0 <%= button[:class] %>">
<%= button[:text] %>
</button>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -76,6 +76,7 @@
</div>
<div>
<button
type="button"
onclick="showLogout()"
class="w-full text-left cursor-pointer block px-2 py-1 rounded-lg transition hover:text-primary"
style="display: block; padding: 10px 15px; border-radius: 6px; transition: background-color 0.15s, color 0.15s; text-decoration: none;"

View File

@@ -1,6 +1,3 @@
<div class="fixed bottom-6 right-4 bg-primary w-[30px] h-[30px] rounded-b-full flex justify-center items-center z-50 bounce" id="scroll-arrow">
<img src="/images/down-arrow.svg" class="w-8 h-8" />
</div>
<div class="container">
<% if current_user&.trust_level == "red" %>
<div class="text-primary bg-red-500/10 border-2 border-red-500/20 p-4 text-center rounded-lg mb-4">

View File

@@ -183,7 +183,7 @@
<button type="button"
data-controller="api-key-rotation"
data-action="click->api-key-rotation#rotateKey"
data-action="click->api-key-rotation#confirm"
class="w-full px-4 py-2 bg-primary hover:bg-red text-white font-medium rounded transition-colors duration-200 cursor-pointer">
Rotate API Key
</button>
@@ -550,3 +550,45 @@
<% end %>
</div>
</div>
<div data-controller="api-key-rotation">
<%= render "shared/modal",
modal_id: "api-key-confirm-modal",
title: "Rotate API Key?",
description: "Your old key will be immediately invalidated and you'll need to update it in all your applications.",
icon_svg: '<path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>',
icon_color: "text-primary",
buttons: [
{
text: "Cancel",
class: "border border-gray-600 text-gray-300 hover:bg-darkless",
action: "click->modal#close"
},
{
text: "Rotate Now",
class: "bg-primary text-white hover:bg-red-600 font-medium",
action: "click->api-key-rotation#rotate"
}
] %>
<%= render "shared/modal",
modal_id: "api-key-success-modal",
title: "New API Key Generated",
description: "Your old API key has been invalidated. Update your editor configuration with this new key:",
icon_svg: '<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>',
icon_color: "text-green-500",
max_width: "max-w-lg",
buttons: [
{
text: "Close",
class: "border border-gray-600 text-gray-300 hover:bg-darkless",
action: "click->modal#close"
},
{
text: "Copy Key",
class: "bg-primary text-white hover:bg-red-600 font-medium",
action: "click->api-key-rotation#copyKey"
}
],
custom: '<div class="w-full mb-4"><div class="bg-darker rounded-lg p-3"><code id="new-api-key-display" class="text-sm text-white break-all" data-token=""></code></div></div>' %>
</div>