diff --git a/standalone/package.json b/standalone/package.json index 43d0b6a..d094e92 100644 --- a/standalone/package.json +++ b/standalone/package.json @@ -1,6 +1,6 @@ { "name": "cap-standalone", - "version": "2.0.7", + "version": "2.0.8", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "bun run --watch src/index.js", diff --git a/standalone/public/index.css b/standalone/public/index.css index 7cb8826..d0a6c74 100644 --- a/standalone/public/index.css +++ b/standalone/public/index.css @@ -3,7 +3,7 @@ } body { - background-color: #181818; + background-color: #111113; color: white; display: flex; max-height: 100vh; @@ -31,11 +31,10 @@ nav .logo { border: none; font-family: inherit; font-size: inherit; - cursor: pointer; background-color: transparent; padding: 0px; margin-right: auto; - transition: opacity 0.2s; + transition: opacity 0.15s; } nav .logo img { @@ -49,7 +48,7 @@ nav .logo p { color: rgb(248, 248, 248); opacity: 0; margin-left: -5px; - transition: margin-left 0.2s, opacity 0.2s; + transition: margin-left 0.15s, opacity 0.15s; pointer-events: none; filter: blur(1px); } @@ -76,7 +75,7 @@ nav .settings { display: flex; align-items: center; justify-content: center; - transition: opacity 0.2s; + transition: opacity 0.15s; } nav .settings:hover { @@ -93,7 +92,7 @@ nav .logout { border: none; color: rgb(248, 248, 248); opacity: 0.6; - transition: opacity 0.2s; + transition: opacity 0.15s; } nav .logout:hover { @@ -161,13 +160,13 @@ nav .logout:hover { margin: 4px; border-radius: 4px; border: none; - background-color: #0076d8; + background-color: #007aff; color: white; font-family: inherit; cursor: pointer; white-space: nowrap; padding: 0px 6px; - transition: transform 0.2s, opacity 0.2s; + transition: transform 0.15s, opacity 0.15s, box-shadow 0.15s; } .state-homepage .keys-top .create-key svg { @@ -184,6 +183,10 @@ nav .logout:hover { transform: scale(0.98); } +.state-homepage .keys-top .create-key:focus { + box-shadow: 0px 0px 0px 2px rgba(0, 122, 255, 0.5); +} + .keys-list.no-keys p { text-align: center; opacity: 0.8; @@ -212,13 +215,13 @@ nav .logout:hover { } .modal-content { - background-color: #282828; + background-color: #212121; border-radius: 10px; max-width: 400px; width: 90%; color: white; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); - animation: scaleIn 0.2s ease-in-out; + animation: scaleIn 0.15s ease-in-out; } @keyframes scaleIn { @@ -256,6 +259,7 @@ nav .logout:hover { .modal .content { padding: 12px 16px; padding-top: 0px; + line-height: 1.4; } .modal-content input[type="text"] { @@ -264,21 +268,19 @@ nav .logout:hover { margin-top: 8px; border-radius: 8px; font-size: 15px; - color: white; - border: 1px solid #e3e4e810; - background-color: #e3e4e807; + box-shadow: inset 0 0 0 1px color(display-p3 0.9608 0.9608 1 / 0.242); + color: #eee; + border: none; + background-color: transparent; font-family: inherit; margin-bottom: 8px; text-overflow: ellipsis; transition: box-shadow 0.15s; } -.modal-content input[type="text"]::focus { - text-overflow: unset; -} - .modal-content input[type="text"]:focus { - box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.37); + text-overflow: unset; + box-shadow: 0px 0px 0px 2px rgba(255, 255, 255); outline: none; } @@ -290,11 +292,10 @@ nav .logout:hover { .modal-content button.primary { border: none; border-radius: 8px; - background-color: white; - color: black; + background-color: #007aff; + color: white; font-size: 15px; cursor: pointer; - transition: background-color 0.2s; font-family: inherit; height: 36px; font-weight: 500; @@ -304,7 +305,7 @@ nav .logout:hover { margin: 16px; width: calc(100% - 32px); margin-top: 0px; - transition: opacity 0.2s; + transition: opacity 0.15s, background-color 0.15s; } .modal-content button.primary.secondary { @@ -420,7 +421,7 @@ nav .logout:hover { .key:focus, .key:active, .key:has(button:focus) { - box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.37); + box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.5); } .state-key-page { @@ -462,7 +463,6 @@ nav .logout:hover { } .state-key-page .topbar .left button { - border: none; border-radius: 6px; color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.05); @@ -480,8 +480,8 @@ nav .logout:hover { background-color: rgba(255, 255, 255, 0.1); } -.state-key-page .topbar .left button:active { - opacity: 0.6; +.state-key-page .topbar .left button:focus { + box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5); } .state-key-page .topbar .right { @@ -492,7 +492,7 @@ nav .logout:hover { border: none; border-radius: 6px; color: white; - background-color: #181818; + background-color: #111113; padding: 0px; font-family: inherit; cursor: pointer; @@ -540,9 +540,10 @@ nav .logout:hover { margin-top: 8px; border-radius: 8px; font-size: 15px; - color: white; - border: 1px solid #e3e4e810; - background-color: #e3e4e807; + border: none; + box-shadow: inset 0 0 0 1px color(display-p3 0.9608 0.9608 1 / 0.242); + color: #eee; + background-color: transparent; font-family: inherit; margin-bottom: 8px; text-overflow: ellipsis; @@ -550,12 +551,9 @@ nav .logout:hover { display: block; } -.config input::focus { - text-overflow: unset; -} - .config input:focus { - box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.37); + text-overflow: unset; + box-shadow: 0px 0px 0px 2px white; outline: none; } @@ -575,7 +573,7 @@ nav .logout:hover { color: black; font-size: 15px; cursor: pointer; - transition: background-color 0.2s; + transition: background-color 0.15s; font-family: inherit; height: 36px; font-weight: 500; @@ -612,7 +610,7 @@ nav .logout:hover { margin-left: -6px; cursor: pointer; width: fit-content; - transition: opacity 0.2s; + transition: opacity 0.15s; } .keypage-back:hover { @@ -624,7 +622,7 @@ nav .logout:hover { } .keypage-back svg { - transition: margin-left 0.2s, margin-right 0.2s; + transition: margin-left 0.15s, margin-right 0.15s; } .keypage-back:hover svg { @@ -653,22 +651,27 @@ nav .logout:hover { .state-settings h1 { margin-top: 0px; font-size: 27px; - margin-bottom: 0px; + margin-bottom: 20px; + font-weight: 600; } .state-settings h2 { font-weight: 500; font-size: 22px; - margin-top: 14px; + margin: 0px; } -.sessions-list, .apikeys-list { +.sessions-list, +.apikeys-list { display: flex; flex-direction: column; gap: 12px; + margin-top: 13px; + margin-bottom: 22px; } -.sessions-list .session, .apikeys-list .apikey { +.sessions-list .session, +.apikeys-list .apikey { border: 1px solid #1f1f1f; border-radius: 8px; background-color: #161616; @@ -678,24 +681,90 @@ nav .logout:hover { align-items: center; } -.session .session-info, .apikey .apikey-info { +.apikeys-list p { + margin: 0px; +} + +.about-info { + line-height: 1.5; + color: #eee; +} + +.about-info b { + color: white; + font-weight: 500; +} + +.about-button { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 8px; + color: white; + text-decoration: none; + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.2); + width: fit-content; + padding: 0.4em 0.6em; + transition: opacity 0.15s; + border-radius: 6px; + animation: abtbtn 1s ease-in-out; + box-shadow: rgb(0 0 0 / 9%) 0px 8px 24px; +} + +.about-button:hover { + background-color: rgba(255, 255, 255, 0.02); +} + +.about-button:focus { + box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5); +} + +.nano-github-button { + display: inline-flex; + align-items: center; + gap: 6px; + opacity: 0.2; + text-decoration: none; + width: fit-content; + transition: opacity 0.15s; + font-size: 14px; + color: white; + margin-top: 2em; + border-radius: 6px; + animation: abtbtn 1s ease-in-out; +} + +.nano-github-button:hover { + opacity: .5; +} + +.nano-github-button:focus { + box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5); +} + +.session .session-info, +.apikey .apikey-info { display: flex; flex-direction: column; justify-content: center; padding: 12px 16px; } -.session-token, .apikey-token { +.session-token, +.apikey-token { margin-bottom: 5px; font-weight: 500; } -.session-expires, .apikey-created { +.session-expires, +.apikey-created { opacity: 0.93; font-size: 14px; } -.session .session-delete-button, .apikey .apikey-delete-button { +.session .session-delete-button, +.apikey .apikey-delete-button { border-radius: 6px; color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.05); @@ -707,12 +776,14 @@ nav .logout:hover { margin-right: 16px; } -.session .session-delete-button:hover, .apikey .apikey-delete-button:hover { +.session .session-delete-button:hover, +.apikey .apikey-delete-button:hover { background-color: rgba(255, 255, 255, 0.1); } -.session .session-delete-button:active, .apikey .apikey-delete-button:active { - opacity: 0.6; +.session .session-delete-button:focus, +.apikey .apikey-delete-button:focus { + box-shadow: 0px 0px 0px 2px white; } .apikeys-title { @@ -723,7 +794,7 @@ nav .logout:hover { .apikeys-title button { border: none; border-radius: 6px; - background-color: #0076d8; + background-color: #007aff; color: white; padding: 0.4em 0.6em; font-family: inherit; @@ -733,13 +804,13 @@ nav .logout:hover { display: flex; align-items: center; gap: 6px; - transition: opacity 0.2s; + transition: opacity 0.15s; } .apikeys-title button:hover { opacity: 0.8; } -.apikeys-title button:active { - opacity: 0.5; +.apikeys-title button:focus { + box-shadow: 0px 0px 0px 2px rgba(0, 122, 255, 0.5); } diff --git a/standalone/public/index.html b/standalone/public/index.html index c28dd5d..88d8e29 100644 --- a/standalone/public/index.html +++ b/standalone/public/index.html @@ -92,6 +92,26 @@
+ + + + +
+ +

About

+

+ + + + + Enjoying Cap? Star us on GitHub! + diff --git a/standalone/public/js/keyPage.js b/standalone/public/js/keyPage.js index 383a705..6f324c4 100644 --- a/standalone/public/js/keyPage.js +++ b/standalone/public/js/keyPage.js @@ -1,439 +1,433 @@ -import { createModal } from "./utils.js"; import makeApiRequest from "./api.js"; -import setState from "./state.js"; import initHomepage from "./homepage.js"; +import setState from "./state.js"; +import { createModal } from "./utils.js"; let currentChart = null; let currentSiteKey = null; export default async function loadKeyPage(siteKey) { - setState("loading"); - currentSiteKey = siteKey; + setState("loading"); + currentSiteKey = siteKey; - try { - const response = await makeApiRequest("GET", `/keys/${siteKey}`); + try { + const response = await makeApiRequest("GET", `/keys/${siteKey}`); - if (!response.success && response.error) { - throw new Error(response.error); - } + if (!response.success && response.error) { + throw new Error(response.error); + } - displayKeyData(response); - await loadChartData(); - setupEventListeners(); - setState("keyPage"); - } catch (error) { - createModal( - "Error", - `

Failed to load key data: ${error.message}

` - ); - setState("homepage"); - } + displayKeyData(response); + await loadChartData(); + setupEventListeners(); + setState("keyPage"); + } catch (error) { + createModal( + "Error", + `

Failed to load key data: ${error.message}

`, + ); + setState("homepage"); + } } function displayKeyData(data) { - const { key } = data; + const { key } = data; - const titleElement = document.querySelector(".state-key-page .topbar h1"); - titleElement.innerText = key.name; + const titleElement = document.querySelector(".state-key-page .topbar h1"); + titleElement.innerText = key.name; - const configElement = document.querySelector(".state-key-page .config"); - configElement.querySelector("#keyName").value = key.name; - configElement.querySelector("#difficulty").value = key.config.difficulty; - configElement.querySelector("#challengeCount").value = - key.config.challengeCount; - configElement.querySelector("#saltSize").value = key.config.saltSize; + const configElement = document.querySelector(".state-key-page .config"); + configElement.querySelector("#keyName").value = key.name; + configElement.querySelector("#difficulty").value = key.config.difficulty; + configElement.querySelector("#challengeCount").value = + key.config.challengeCount; + configElement.querySelector("#saltSize").value = key.config.saltSize; - configElement.querySelector("button").onclick = async () => { - const name = configElement.querySelector("#keyName").value.trim(); - const difficulty = parseInt( - configElement.querySelector("#difficulty").value, - 10 - ); - const challengeCount = parseInt( - configElement.querySelector("#challengeCount").value, - 10 - ); - const saltSize = parseInt( - configElement.querySelector("#saltSize").value, - 10 - ); + configElement.querySelector("button").onclick = async () => { + const name = configElement.querySelector("#keyName").value.trim(); + const difficulty = parseInt( + configElement.querySelector("#difficulty").value, + 10, + ); + const challengeCount = parseInt( + configElement.querySelector("#challengeCount").value, + 10, + ); + const saltSize = parseInt( + configElement.querySelector("#saltSize").value, + 10, + ); - if ( - !name || - isNaN(difficulty) || - isNaN(challengeCount) || - isNaN(saltSize) - ) { - createModal( - "Error", - `

Please fill in all fields correctly.

` - ); + if ( + !name || + isNaN(difficulty) || + isNaN(challengeCount) || + isNaN(saltSize) + ) { + createModal( + "Error", + `

Please fill in all fields correctly.

`, + ); - return; - } + return; + } - document.querySelector(".state-key-page h1").textContent = - configElement.querySelector("#keyName").value; - configElement.querySelector("button").disabled = true; + document.querySelector(".state-key-page h1").textContent = + configElement.querySelector("#keyName").value; + configElement.querySelector("button").disabled = true; - const { success } = await makeApiRequest( - "PUT", - `/keys/${key.siteKey}/config`, - { - name, - difficulty, - challengeCount, - saltSize, - } - ); + const { success } = await makeApiRequest( + "PUT", + `/keys/${key.siteKey}/config`, + { + name, + difficulty, + challengeCount, + saltSize, + }, + ); - if (!success) { - createModal( - "Error", - `

Failed to save key.

` - ); - } + if (!success) { + createModal( + "Error", + `

Failed to save key.

`, + ); + } - configElement.querySelector("button").disabled = false; - }; + configElement.querySelector("button").disabled = false; + }; - document - .querySelector(".danger-zone-buttons .delete-key") - .addEventListener("click", async () => { - const modal = createModal( - "Delete key?", - `

Are you sure you want to delete this key? This action cannot be undone.

+ document + .querySelector(".danger-zone-buttons .delete-key") + .addEventListener("click", async () => { + const modal = createModal( + "Delete key?", + `

Are you sure you want to delete this key? This action cannot be undone.

- ` - ); + `, + ); - document.body.appendChild(modal); + document.body.appendChild(modal); - modal - .querySelector(".delete-confirm-button") - .addEventListener("click", async () => { - modal.querySelector(".delete-confirm-button").disabled = true; - const { success } = await makeApiRequest( - "DELETE", - `/keys/${key.siteKey}` - ); + modal + .querySelector(".delete-confirm-button") + .addEventListener("click", async () => { + modal.querySelector(".delete-confirm-button").disabled = true; + const { success } = await makeApiRequest( + "DELETE", + `/keys/${key.siteKey}`, + ); - document.body.removeChild(modal); + document.body.removeChild(modal); - if (success) { - initHomepage(); - } else { - createModal( - "Error", - `

Failed to delete key

` - ); - } - }); + if (success) { + initHomepage(); + } else { + createModal( + "Error", + `

Failed to delete key

`, + ); + } + }); - modal - .querySelector(".delete-cancel-button") - .addEventListener("click", () => { - document.body.removeChild(modal); - }); - }); + modal + .querySelector(".delete-cancel-button") + .addEventListener("click", () => { + document.body.removeChild(modal); + }); + }); - document - .querySelector(".danger-zone-buttons .rotate-secret") - .addEventListener("click", async () => { - const modal = createModal( - "Rotate secret key?", - `

Are you sure you want to rotate this key's secret? This won't delete the key itself.

+ document + .querySelector(".danger-zone-buttons .rotate-secret") + .addEventListener("click", async () => { + const modal = createModal( + "Rotate secret key?", + `

Are you sure you want to rotate this key's secret? This won't delete the key itself.

- ` - ); + `, + ); - modal - .querySelector(".rotate-confirm-button") - .addEventListener("click", async () => { - modal.querySelector(".rotate-confirm-button").disabled = true; - const { secretKey } = await makeApiRequest( - "POST", - `/keys/${key.siteKey}/rotate-secret` - ); + modal + .querySelector(".rotate-confirm-button") + .addEventListener("click", async () => { + modal.querySelector(".rotate-confirm-button").disabled = true; + const { secretKey } = await makeApiRequest( + "POST", + `/keys/${key.siteKey}/rotate-secret`, + ); - document.body.removeChild(modal); + document.body.removeChild(modal); - if (!secretKey) { - createModal( - "Error", - `

Failed to rotate key secret.

` - ); - return; - } - const successModal = createModal( - "Secret key rotated", - `
+ if (!secretKey) { + createModal( + "Error", + `

Failed to rotate key secret.

`, + ); + return; + } + const successModal = createModal( + "Secret key rotated", + `

Make sure to copy your secret key as it will not be shown again later.

- ` - ); + `, + ); - successModal - .querySelector(".close-small-button") - .addEventListener("click", () => { - document.body.removeChild(successModal); - }); - }); + successModal + .querySelector(".close-small-button") + .addEventListener("click", () => { + document.body.removeChild(successModal); + }); + }); - modal - .querySelector(".rotate-cancel-button") - .addEventListener("click", () => { - document.body.removeChild(modal); - }); - }); + modal + .querySelector(".rotate-cancel-button") + .addEventListener("click", () => { + document.body.removeChild(modal); + }); + }); - const copyButton = document.querySelector(".state-key-page .topbar button"); - copyButton.innerHTML = ` Copy site key`; + const copyButton = document.querySelector(".state-key-page .topbar button"); + copyButton.innerHTML = ` Copy site key`; - copyButton.onclick = () => { - navigator.clipboard.writeText(key.siteKey); - const originalText = copyButton.innerHTML; - copyButton.innerHTML = ` Copied!`; - setTimeout(() => { - copyButton.innerHTML = originalText; - }, 2000); - }; + copyButton.onclick = () => { + navigator.clipboard.writeText(key.siteKey); + const originalText = copyButton.innerHTML; + copyButton.innerHTML = ` Copied!`; + setTimeout(() => { + copyButton.innerHTML = originalText; + }, 2000); + }; } async function loadChartData(duration = "today") { - document.querySelector(".chart-container").style.opacity = "0.3"; - document.querySelector(".chart-container").style.pointerEvents = "none"; + document.querySelector(".chart-container").style.opacity = "0.3"; + document.querySelector(".chart-container").style.pointerEvents = "none"; - const response = await makeApiRequest( - "GET", - `/keys/${currentSiteKey}?chartDuration=${duration}` - ); + const response = await makeApiRequest( + "GET", + `/keys/${currentSiteKey}?chartDuration=${duration}`, + ); - document.querySelector(".chart-container").style.opacity = "1"; - document.querySelector(".chart-container").style.pointerEvents = "all"; + document.querySelector(".chart-container").style.opacity = "1"; + document.querySelector(".chart-container").style.pointerEvents = "all"; - renderChart(response.chartData, duration); + renderChart(response.chartData, duration); } function formatDateLabel(timestamp, duration) { - const date = new Date(timestamp * 1000); + const date = new Date(timestamp * 1000); - switch (duration) { - case "today": - case "yesterday": - return date.toLocaleTimeString("en-US", { - hour: "numeric", - hour12: true, - }); - case "last7days": - case "last28days": - case "last91days": - case "alltime": - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - default: - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - } + switch (duration) { + case "today": + case "yesterday": + return date.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + case "last7days": + case "last28days": + case "last91days": + case "alltime": + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + default: + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + } } function renderChart(chartData, duration) { - const canvas = document.getElementById("chart"); - const ctx = canvas.getContext("2d"); + const canvas = document.getElementById("chart"); + const ctx = canvas.getContext("2d"); - if (currentChart) { - currentChart.destroy(); - } + if (currentChart) { + currentChart.destroy(); + } - const labels = []; - const dataPoints = []; + const labels = []; + const dataPoints = []; - const { bucketSize, data } = chartData; - const now = Math.floor(Date.now() / 1000); + const { bucketSize, data } = chartData; + const now = Math.floor(Date.now() / 1000); - let startTime, endTime, buckets; + let startTime, endTime, buckets; - switch (duration) { - case "today": - startTime = Math.floor(Date.now() / 1000 / 86400) * 86400; - endTime = startTime + 86400; - buckets = Math.floor((endTime - startTime) / bucketSize); - break; - case "yesterday": - startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400; - endTime = startTime + 86400; - buckets = Math.floor((endTime - startTime) / bucketSize); - break; - case "last7days": - startTime = now - 7 * 86400; - endTime = now; - buckets = 7; - break; - case "last28days": - startTime = now - 28 * 86400; - endTime = now; - buckets = 28; - break; - case "last91days": - startTime = now - 91 * 86400; - endTime = now; - buckets = 91; - break; - case "alltime": - if (data.length > 0) { - startTime = data[0].bucket; - endTime = now; - buckets = Math.ceil((endTime - startTime) / bucketSize); - } else { - startTime = now - 86400; - endTime = now; - buckets = 1; - } - break; - default: - startTime = now - 86400; - endTime = now; - buckets = 24; - } + switch (duration) { + case "today": + startTime = Math.floor(Date.now() / 1000 / 86400) * 86400; + endTime = startTime + 86400; + buckets = Math.floor((endTime - startTime) / bucketSize); + break; + case "yesterday": + startTime = Math.floor(Date.now() / 1000 / 86400) * 86400 - 86400; + endTime = startTime + 86400; + buckets = Math.floor((endTime - startTime) / bucketSize); + break; + case "last7days": + startTime = now - 7 * 86400; + endTime = now; + buckets = 7; + break; + case "last28days": + startTime = now - 28 * 86400; + endTime = now; + buckets = 28; + break; + case "last91days": + startTime = now - 91 * 86400; + endTime = now; + buckets = 91; + break; + case "alltime": + if (data.length > 0) { + startTime = data[0].bucket; + endTime = now; + buckets = Math.ceil((endTime - startTime) / bucketSize); + } else { + startTime = now - 86400; + endTime = now; + buckets = 1; + } + break; + default: + startTime = now - 86400; + endTime = now; + buckets = 24; + } - const dataMap = new Map(); - data.forEach((item) => { - dataMap.set(item.bucket, item.count); - }); + const dataMap = new Map(); + data.forEach((item) => { + dataMap.set(item.bucket, item.count); + }); - for (let i = 0; i < buckets; i++) { - const bucketTime = startTime + i * bucketSize; - labels.push(formatDateLabel(bucketTime, duration)); + for (let i = 0; i < buckets; i++) { + const bucketTime = startTime + i * bucketSize; + labels.push(formatDateLabel(bucketTime, duration)); - const alignedBucket = Math.floor(bucketTime / bucketSize) * bucketSize; - dataPoints.push(dataMap.get(alignedBucket) || 0); - } + const alignedBucket = Math.floor(bucketTime / bucketSize) * bucketSize; + dataPoints.push(dataMap.get(alignedBucket) || 0); + } - function createGradient(chart) { - const { ctx, chartArea } = chart; - if (!chartArea) { - return null; - } + function createGradient(chart) { + const { ctx, chartArea } = chart; + if (!chartArea) { + return null; + } - const gradient = ctx.createLinearGradient( - 0, - chartArea.bottom, - 0, - chartArea.top - ); + const gradient = ctx.createLinearGradient( + 0, + chartArea.bottom, + 0, + chartArea.top, + ); - gradient.addColorStop(0, "rgba(0, 118, 216, 0.05)"); - gradient.addColorStop(0.5, "rgba(0, 118, 216, 0.3)"); - gradient.addColorStop(1, "rgba(0, 118, 216, 0.7)"); + gradient.addColorStop(0, "rgba(0, 118, 216, 0.05)"); + gradient.addColorStop(0.5, "rgba(0, 118, 216, 0.3)"); + gradient.addColorStop(1, "rgba(0, 118, 216, 0.7)"); - return gradient; - } + return gradient; + } - currentChart = new Chart(ctx, { - type: "line", - data: { - labels: labels, - datasets: [ - { - label: "Daily Count", - data: dataPoints, - fill: true, - backgroundColor: function (context) { - return createGradient(context.chart); - }, - borderColor: "#0076D8", - borderWidth: 2, - pointRadius: 0, - pointBackgroundColor: "#0076D8", - pointBorderColor: "#fff", - pointBorderWidth: 1.5, - pointHoverRadius: 0, - pointHoverBackgroundColor: "#fff", - pointHoverBorderColor: "#0076D8", - pointHoverBorderWidth: 2, - tension: 0, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - mode: "index", - intersect: false, - }, - scales: { - x: { - grid: { - color: "rgba(255, 255, 255, 0.08)", - borderColor: "rgba(255, 255, 255, 0.15)", - drawTicks: false, - drawOnChartArea: false, - }, - ticks: { - color: "#b0b0b0", - maxRotation: 45, - minRotation: 0, - }, - }, - y: { - beginAtZero: true, - grid: { - color: "rgba(255, 255, 255, 0.08)", - borderColor: "rgba(255, 255, 255, 0.15)", - }, - ticks: { - color: "#b0b0b0", - }, - }, - }, - plugins: { - legend: { - display: false, - }, - tooltip: { - enabled: true, - displayColors: false, - callbacks: { - title: function (tooltipItems) { - return tooltipItems[0].label; - }, - label: function (tooltipItem) { - return `${tooltipItem.raw} solves`; - }, - }, - }, - }, - animation: { - duration: 0, - }, - }, - }); + currentChart = new Chart(ctx, { + type: "line", + data: { + labels: labels, + datasets: [ + { + label: "Daily Count", + data: dataPoints, + fill: true, + backgroundColor: (context) => createGradient(context.chart), + borderColor: "#007AFF", + borderWidth: 2, + pointRadius: 0, + pointBackgroundColor: "#007AFF", + pointBorderColor: "#fff", + pointBorderWidth: 1.5, + pointHoverRadius: 0, + pointHoverBackgroundColor: "#fff", + pointHoverBorderColor: "#007AFF", + pointHoverBorderWidth: 2, + tension: 0, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: "index", + intersect: false, + }, + scales: { + x: { + grid: { + color: "rgba(255, 255, 255, 0.08)", + borderColor: "rgba(255, 255, 255, 0.15)", + drawTicks: false, + drawOnChartArea: false, + }, + ticks: { + color: "#b0b0b0", + maxRotation: 45, + minRotation: 0, + }, + }, + y: { + beginAtZero: true, + grid: { + color: "rgba(255, 255, 255, 0.08)", + borderColor: "rgba(255, 255, 255, 0.15)", + }, + ticks: { + color: "#b0b0b0", + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: true, + displayColors: false, + callbacks: { + title: (tooltipItems) => tooltipItems[0].label, + label: (tooltipItem) => `${tooltipItem.raw} solves`, + }, + }, + }, + animation: { + duration: 0, + }, + }, + }); - const resizeObserver = new ResizeObserver(() => { - if (currentChart) { - currentChart.update("resize"); - } - }); + const resizeObserver = new ResizeObserver(() => { + if (currentChart) { + currentChart.update("resize"); + } + }); - resizeObserver.observe(canvas); + resizeObserver.observe(canvas); } function setupEventListeners() { - const timeDurationSelect = document.querySelector( - ".state-key-page .time-duration" - ); + const timeDurationSelect = document.querySelector( + ".state-key-page .time-duration", + ); - timeDurationSelect.onchange = (e) => { - const duration = e.target.value; - loadChartData(duration); - }; + timeDurationSelect.onchange = (e) => { + const duration = e.target.value; + loadChartData(duration); + }; } diff --git a/standalone/public/js/settings.js b/standalone/public/js/settings.js index 9b2e8c1..6a6a286 100644 --- a/standalone/public/js/settings.js +++ b/standalone/public/js/settings.js @@ -1,142 +1,176 @@ +import makeApiRequest from "./api.js"; import setState from "./state.js"; import { createModal } from "./utils.js"; -import makeApiRequest from "./api.js"; export const openSettings = () => { - setState("settings"); + setState("settings"); - const formatDate = (dateStr) => { - const date = new Date(dateStr); - return date.toLocaleString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }); - }; + const formatDate = (dateStr) => { + const date = new Date(dateStr); + return date.toLocaleString(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + }; - (async () => { - const sessions = await makeApiRequest("GET", "/settings/sessions"); + (async () => { + const sessions = await makeApiRequest("GET", "/settings/sessions"); - const sessionsList = document.querySelector( - ".state-settings .sessions-list" - ); - sessionsList.innerHTML = ""; + const sessionsList = document.querySelector( + ".state-settings .sessions-list", + ); + sessionsList.innerHTML = ""; + function formatRelative(date) { + const diff = new Date(date) - new Date(); + const past = diff < 0; + const d = Math.abs(diff); - sessions.forEach((session) => { - const sessionEl = document.createElement("div"); - sessionEl.classList.add("session"); + const ms = { + year: 365 * 24 * 60 * 60 * 1000, + month: 30 * 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + }; - sessionEl.innerHTML = ` + if (d >= ms.year) { + const y = Math.floor(d / ms.year); + const m = Math.floor((d - y * ms.year) / ms.month); + return past + ? `${y} year${y > 1 ? "s" : ""}${m ? " " + m + " month" + (m > 1 ? "s" : "") : ""} ago` + : `in ${y} year${y > 1 ? "s" : ""}${m ? " " + m + " month" + (m > 1 ? "s" : "") : ""}`; + } + + for (const [u, v] of Object.entries(ms)) { + if (d >= v) { + const val = Math.floor(d / v); + return past + ? `${val} ${u}${val > 1 ? "s" : ""} ago` + : `in ${val} ${u}${val > 1 ? "s" : ""}`; + } + } + + return past ? "just now" : "in a few seconds"; + } + + sessions.forEach((session) => { + const sessionEl = document.createElement("div"); + sessionEl.classList.add("session"); + + sessionEl.innerHTML = `
${session.token}... ${ - JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith( - session.token - ) - ? "current • " - : "" - }expires ${formatDate(session.expires)}${ - JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith( - session.token - ) - ? "" - : " • created " + formatDate(session.created) - } + JSON.parse(localStorage.getItem("cap_auth")).hash.endsWith( + session.token, + ) + ? "current • " + : "" + }expires ${formatRelative(session.expires)} • created ${formatRelative(session.created)}
`; - sessionsList.appendChild(sessionEl); + sessionsList.appendChild(sessionEl); - sessionEl - .querySelector(".session-delete-button") - .addEventListener("click", async () => { - const modal = createModal( - "Logout session?", - ` - ` - ); + sessionEl + .querySelector(".session-delete-button") + .addEventListener("click", async () => { + const modal = createModal( + "Logout session?", + ` + `, + ); - modal - .querySelector(".logout-confirm-button") - .addEventListener("click", async () => { - modal.querySelector(".logout-confirm-button").disabled = true; - await makeApiRequest("POST", `/logout`, { - session: session.token, - }); - document.body.removeChild(modal); - if (sessions.length === 1) { - localStorage.removeItem("cap_auth"); - location.reload(); - } - openSettings(); - }); + modal + .querySelector(".logout-confirm-button") + .addEventListener("click", async () => { + modal.querySelector(".logout-confirm-button").disabled = true; + await makeApiRequest("POST", `/logout`, { + session: session.token, + }); + document.body.removeChild(modal); + if (sessions.length === 1) { + localStorage.removeItem("cap_auth"); + location.reload(); + } + openSettings(); + }); - modal - .querySelector(".logout-cancel-button") - .addEventListener("click", () => { - document.body.removeChild(modal); - }); - }); - }); - })(); + modal + .querySelector(".logout-cancel-button") + .addEventListener("click", () => { + document.body.removeChild(modal); + }); + }); + }); + })(); - (async () => { - const apikeys = await makeApiRequest("GET", "/settings/apikeys"); + (async () => { + const apikeys = await makeApiRequest("GET", "/settings/apikeys"); - const apikeysList = document.querySelector(".state-settings .apikeys-list"); - apikeysList.innerHTML = ""; + const apikeysList = document.querySelector(".state-settings .apikeys-list"); + apikeysList.innerHTML = ""; - if (apikeys.length === 0) { - apikeysList.innerHTML = `

You don't have any API keys

`; - return; - } + if (apikeys.length === 0) { + apikeysList.innerHTML = `

You don't have any API keys

`; + return; + } - apikeys.forEach((key) => { - const keyEl = document.createElement("div"); - keyEl.classList.add("apikey"); + apikeys.forEach((key) => { + const keyEl = document.createElement("div"); + keyEl.classList.add("apikey"); - keyEl.innerHTML = ` + keyEl.innerHTML = `
${key.name - .replaceAll("<", "<") - .replaceAll(">", ">")} + .replaceAll("<", "<") + .replaceAll(">", ">")} ${key.id.slice( - 0, - 12 - )}... • created ${formatDate(key.created)} + 0, + 12, + )}... • created ${formatDate(key.created)}
`; - apikeysList.appendChild(keyEl); + apikeysList.appendChild(keyEl); - keyEl - .querySelector(".apikey-delete-button") - .addEventListener("click", async () => { - const modal = createModal( - "Delete API key?", - ` - ` - ); + keyEl + .querySelector(".apikey-delete-button") + .addEventListener("click", async () => { + const modal = createModal( + "Delete API key?", + ` + `, + ); - modal - .querySelector(".delete-confirm-button") - .addEventListener("click", async () => { - modal.querySelector(".delete-confirm-button").disabled = true; - await makeApiRequest("DELETE", `/settings/apikeys/${key.id}`); - document.body.removeChild(modal); - openSettings(); - }); + modal + .querySelector(".delete-confirm-button") + .addEventListener("click", async () => { + modal.querySelector(".delete-confirm-button").disabled = true; + await makeApiRequest("DELETE", `/settings/apikeys/${key.id}`); + document.body.removeChild(modal); + openSettings(); + }); - modal - .querySelector(".delete-cancel-button") - .addEventListener("click", () => { - document.body.removeChild(modal); - }); - }); - }); - })(); + modal + .querySelector(".delete-cancel-button") + .addEventListener("click", () => { + document.body.removeChild(modal); + }); + }); + }); + })(); + + (async () => { + const info = await makeApiRequest("GET", "/about"); + + document.querySelector(".about-info").innerHTML = + `You're using Cap Standalone ${info.ver} on Bun v${info.bun}`; + })(); }; diff --git a/standalone/public/login.html b/standalone/public/login.html index 855dc62..2092828 100644 --- a/standalone/public/login.html +++ b/standalone/public/login.html @@ -16,7 +16,7 @@ margin: 0px; padding: 0px; margin-top: 14vh; - background-color: #181818; + background-color: #111113; color: #e8e8ea; } form { @@ -27,20 +27,29 @@ width: 100%; text-align: left; } + form h1 { + font-size: 24px; + font-weight: 500; + text-align: center; + margin-bottom: 28px; + margin-top: 0; + } img { width: 60px; } label { font-size: 14px; line-height: 1.5; + font-weight: 500; display: block; - margin-bottom: 3px; - color: #d9dfe5eb; + margin-bottom: 8px; + color: #eee; } .password-wrapper { - border: 1px solid #e3e4e810; - background-color: #e3e4e807; + box-shadow: inset 0 0 0 1px color(display-p3 0.9608 0.9608 1 / 0.242); + color: #eee; + border: none; display: flex; border-radius: 8px; margin-bottom: 16px; @@ -53,7 +62,7 @@ color: white; background-color: transparent; padding: 0; - margin-right: 8px; + margin-right: 10px; } .password { @@ -80,7 +89,7 @@ } .password-wrapper:has(.password:focus) { - box-shadow: 0px 0px 0px 3px rgba(255, 255, 255, 0.5); + box-shadow: 0px 0px 0px 2px white; } .submit { @@ -90,8 +99,8 @@ text-align: center; font-weight: 500; border: none; - color: black; - background: white; + color: white; + background: #007aff; background-origin: border-box; user-select: none; -webkit-user-select: none; @@ -104,7 +113,7 @@ .submit:focus { box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2), 0px 0.5px 1.5px rgba(54, 122, 246, 0.25), - 0px 0px 0px 3.5px rgba(255, 255, 255, 0.5); + 0px 0px 0px 3.5px rgba(0, 122, 255, 0.5); outline: 0; } @@ -134,11 +143,13 @@ color: rgb(255, 255, 255); opacity: 0.4; font-size: 14px; - transition: opacity 0.2s; + text-underline-offset: 2px; + text-decoration: none; } a:hover { opacity: 1; + text-decoration: underline; } .logo { @@ -150,7 +161,7 @@ .logo img { width: 45px; height: 45px; - margin-bottom: 1em; + margin-bottom: 6px; } @@ -159,6 +170,7 @@ +

Welcome to Cap!

- +

+ + + + Need help? + rel="follow noopener" + > + Report issue + Powered by Cap @@ -211,16 +240,6 @@ let loading = false; document.querySelector("input").focus(); - document.querySelector(".password").addEventListener("input", function (e) { - if (document.querySelector(".password").value.trim().length < 30) { - document.querySelector("button[type=submit]").disabled = true; - return; - } - if (!loading) { - document.querySelector("button[type=submit]").disabled = false; - } - }); - document .querySelector("form") .addEventListener("submit", async function (e) { @@ -275,11 +294,11 @@ if (passwordInput.type === "password") { passwordInput.type = "text"; - passwordInput.placeholder = ""; + passwordInput.placeholder = "MyCapAdminKey1234567!"; showhideButton.innerHTML = ``; } else { passwordInput.type = "password"; - passwordInput.placeholder = "••••••••••••••"; + passwordInput.placeholder = "•••••••••••••••••••••"; showhideButton.innerHTML = ``; } }); diff --git a/standalone/src/server.js b/standalone/src/server.js index 585ed7e..e7934d3 100644 --- a/standalone/src/server.js +++ b/standalone/src/server.js @@ -505,6 +505,18 @@ export const server = new Elysia({ }, }, ) + .get( + "/about", + async () => { + const pkg = await import("../package.json", { assert: { type: "json" } }); + + return { + bun: Bun.version, + ver: pkg.default.version, + }; + }, + {}, + ) .post( "/logout", async ({ body, headers, set }) => {