mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
modal refresh (#636)
* modal refresh * fix up yields and custom injections
This commit is contained in:
@@ -227,10 +227,6 @@ select {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
#scroll-arrow {
|
||||
transition: transform 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
34
app/javascript/controllers/modal_controller.js
Normal file
34
app/javascript/controllers/modal_controller.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
63
app/views/shared/_modal.html.erb
Normal file
63
app/views/shared/_modal.html.erb
Normal 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>
|
||||
@@ -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;"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 18.0711L19.0711 11L57.4189 49.3478L50.3478 56.4189L12 18.0711Z" fill="#17171D"/>
|
||||
<path d="M88.8633 18.0711L81.7922 11L43.4444 49.3478L50.5155 56.4189L88.8633 18.0711Z" fill="#17171D"/>
|
||||
<path d="M12 51.0711L19.0711 44L57.4189 82.3478L50.3478 89.4189L12 51.0711Z" fill="#17171D"/>
|
||||
<path d="M88.8633 51.0711L81.7922 44L43.4444 82.3478L50.5155 89.4189L88.8633 51.0711Z" fill="#17171D"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 503 B |
Reference in New Issue
Block a user