next-site: fix badge/contrast/save-feedback bugs

This commit is contained in:
Ally Founder 2026-08-15 15:29:59 -04:00
parent 09e0b6e49b
commit a84aabb33b
3 changed files with 34 additions and 10 deletions

View file

@ -10,7 +10,7 @@ export function renderAdminPage(): string {
<div id="gate"> <div id="gate">
<div class="page-sub">Admin token</div> <div class="page-sub">Admin token</div>
<input id="token" type="password" style="${INPUT_STYLE}max-width:280px;"> <input id="token" type="password" style="${INPUT_STYLE}max-width:280px;">
<button id="unlock" class="btn" style="border:none;cursor:pointer;margin-left:8px;">Unlock</button> <button id="unlock" class="btn" style="margin-left:8px;">Unlock</button>
</div> </div>
<div id="panel" style="display:none;"></div> <div id="panel" style="display:none;"></div>
</div> </div>
@ -38,6 +38,17 @@ export function renderAdminPage(): string {
</div>\` </div>\`
} }
function flashSaved(btn) {
return new Promise((resolve) => {
const original = btn.textContent
btn.textContent = 'Saved'
setTimeout(() => {
btn.textContent = original
resolve()
}, 700)
})
}
async function loadState() { async function loadState() {
const res = await fetch('/api/admin/state', { const res = await fetch('/api/admin/state', {
headers: { authorization: \`Bearer \${token}\` }, headers: { authorization: \`Bearer \${token}\` },
@ -51,10 +62,10 @@ export function renderAdminPage(): string {
<div class="card-name">Site settings</div> <div class="card-name">Site settings</div>
\${fieldRow('site', 'name', 'Site name', state.site?.name)} \${fieldRow('site', 'name', 'Site name', state.site?.name)}
\${fieldRow('site', 'description', 'Site description', state.site?.description, true)} \${fieldRow('site', 'description', 'Site description', state.site?.description, true)}
<button id="save-site" class="btn" style="border:none;cursor:pointer;">Save site settings</button> <button id="save-site" class="btn">Save site settings</button>
</div>\` </div>\`
document.getElementById('save-site').addEventListener('click', async () => { document.getElementById('save-site').addEventListener('click', async (e) => {
const name = document.querySelector('[data-id="site"][data-field="name"]').value const name = document.querySelector('[data-id="site"][data-field="name"]').value
const description = document.querySelector('[data-id="site"][data-field="description"]').value const description = document.querySelector('[data-id="site"][data-field="description"]').value
await fetch('/api/admin/site', { await fetch('/api/admin/site', {
@ -62,6 +73,7 @@ export function renderAdminPage(): string {
headers: { 'content-type': 'application/json', authorization: \`Bearer \${token}\` }, headers: { 'content-type': 'application/json', authorization: \`Bearer \${token}\` },
body: JSON.stringify({ name, description }), body: JSON.stringify({ name, description }),
}) })
await flashSaved(e.currentTarget)
init() init()
}) })
} }
@ -73,7 +85,7 @@ export function renderAdminPage(): string {
<div style="display:flex;gap:8px;margin-bottom:6px;" class="channel-row"> <div style="display:flex;gap:8px;margin-bottom:6px;" class="channel-row">
<input placeholder="channel name" value="\${channel}" data-channel-name style="\${INPUT_STYLE}width:140px;flex:none;"> <input placeholder="channel name" value="\${channel}" data-channel-name style="\${INPUT_STYLE}width:140px;flex:none;">
<input placeholder="version" value="\${version}" data-channel-version style="\${INPUT_STYLE}width:100px;flex:none;"> <input placeholder="version" value="\${version}" data-channel-version style="\${INPUT_STYLE}width:100px;flex:none;">
<button data-id="\${id}" class="btn-ghost set-channel" style="border-width:1px;cursor:pointer;">Set</button> <button data-id="\${id}" class="btn-ghost set-channel">Set</button>
</div>\`).join('') </div>\`).join('')
} }
@ -92,12 +104,12 @@ export function renderAdminPage(): string {
<label style="font-size:11px;display:flex;align-items:center;gap:4px;margin-bottom:10px;"> <label style="font-size:11px;display:flex;align-items:center;gap:4px;margin-bottom:10px;">
<input type="checkbox" data-id="\${id}" data-field="hidden" \${o.hidden ? 'checked' : ''}> hidden <input type="checkbox" data-id="\${id}" data-field="hidden" \${o.hidden ? 'checked' : ''}> hidden
</label> </label>
<button data-id="\${id}" class="btn save" style="border:none;cursor:pointer;">Save</button> <button data-id="\${id}" class="btn save">Save</button>
</div> </div>
<div style="margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,0.08);"> <div style="margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,0.08);">
<div style="font-size:10.5px;color:rgba(255,255,255,0.4);margin-bottom:6px;">Channels (any name - Next's client lists these for the user to pick)</div> <div style="font-size:10.5px;color:rgba(255,255,255,0.4);margin-bottom:6px;">Channels (any name - Next's client lists these for the user to pick)</div>
<div class="channels" data-id="\${id}">\${channelRows(id, state.channels[id])}</div> <div class="channels" data-id="\${id}">\${channelRows(id, state.channels[id])}</div>
<button data-id="\${id}" class="btn-ghost add-channel-row" style="border-width:1px;cursor:pointer;margin-top:4px;">+ Add channel</button> <button data-id="\${id}" class="btn-ghost add-channel-row" style="margin-top:4px;">+ Add channel</button>
</div> </div>
</div>\` </div>\`
}).join('') }).join('')
@ -125,6 +137,7 @@ export function renderAdminPage(): string {
headers: { 'content-type': 'application/json', authorization: \`Bearer \${token}\` }, headers: { 'content-type': 'application/json', authorization: \`Bearer \${token}\` },
body: JSON.stringify(patch), body: JSON.stringify(patch),
}) })
await flashSaved(btn)
init() init()
})) }))
@ -157,6 +170,7 @@ export function renderAdminPage(): string {
alert(data.error) alert(data.error)
return return
} }
await flashSaved(btn)
init() init()
}) })
}) })

View file

@ -13,11 +13,17 @@ export function renderBrowsePage(
const override = overrides[id] const override = overrides[id]
const version = plugin.channels.latest ?? Object.keys(plugin.versions)[0] const version = plugin.channels.latest ?? Object.keys(plugin.versions)[0]
const tagline = override?.tagline ?? plugin.description const tagline = override?.tagline ?? plugin.description
const badges = Object.keys(plugin.channels)
.map(
channel =>
`<div class="badge">${escapeHtml(channel.toUpperCase())}</div>`,
)
.join('')
return ` return `
<div class="card"> <div class="card">
<div class="card-top"> <div class="card-top">
<div class="card-icon">${plugin.icon ? escapeHtml(plugin.icon) : '\u{1F9E9}'}</div> <div class="card-icon">${plugin.icon ? escapeHtml(plugin.icon) : '\u{1F9E9}'}</div>
<div class="badge">STABLE</div> <div class="badge-row">${badges}</div>
</div> </div>
<div class="card-name"><a href="/plugins/${encodeURIComponent(id)}">${escapeHtml(plugin.name)}</a></div> <div class="card-name"><a href="/plugins/${encodeURIComponent(id)}">${escapeHtml(plugin.name)}</a></div>
<div class="card-desc">${escapeHtml(tagline)}</div> <div class="card-desc">${escapeHtml(tagline)}</div>

View file

@ -90,6 +90,7 @@ export const PAGE_CSS = `
border: 1px solid rgba(255,255,255,0.1); display: flex; align-items: center; border: 1px solid rgba(255,255,255,0.1); display: flex; align-items: center;
justify-content: center; font-size: 15px; justify-content: center; font-size: 15px;
} }
.badge-row { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.badge { .badge {
background: rgba(154,140,255,0.12); color: #b6acff; border: 1px solid rgba(154,140,255,0.25); background: rgba(154,140,255,0.12); color: #b6acff; border: 1px solid rgba(154,140,255,0.25);
font-size: 10px; font-weight: 650; padding: 3px 8px; border-radius: 5px; font-size: 10px; font-weight: 650; padding: 3px 8px; border-radius: 5px;
@ -98,16 +99,19 @@ export const PAGE_CSS = `
.card-desc { position: relative; font-size: 13.5px; color: rgba(255,255,255,0.55); margin-top: 6px; line-height: 1.5; } .card-desc { position: relative; font-size: 13.5px; color: rgba(255,255,255,0.55); margin-top: 6px; line-height: 1.5; }
.card-foot { position: relative; display: flex; align-items: center; justify-content: space-between; margin-top: 18px; } .card-foot { position: relative; display: flex; align-items: center; justify-content: space-between; margin-top: 18px; }
.card-meta { font-size: 11.5px; color: rgba(255,255,255,0.4); } .card-meta { font-size: 11.5px; color: rgba(255,255,255,0.4); }
button { font-family: inherit; }
.btn { .btn {
background: #e8e9ed; color: #0b0c10; font-size: 13px; font-weight: 650; background: #e8e9ed; color: #0b0c10; font-size: 13px; font-weight: 650;
padding: 8px 16px; border-radius: 8px; display: inline-block; padding: 8px 16px; border-radius: 8px; display: inline-block; border: none; cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
} }
.btn:hover { transform: translateY(-2px); opacity: 0.92; } .btn:hover { transform: translateY(-2px); opacity: 0.92; }
.btn-ghost { .btn-ghost {
border: 1px solid rgba(255,255,255,0.16); color: #fff; font-size: 13px; font-weight: 600; background: transparent; border: 1px solid rgba(255,255,255,0.16); color: #fff;
padding: 8px 16px; border-radius: 8px; display: inline-block; font-size: 13px; font-weight: 600; padding: 8px 16px; border-radius: 8px;
display: inline-block; cursor: pointer; transition: border-color 0.2s ease, background 0.2s ease;
} }
.btn-ghost:hover { border-color: rgba(255,255,255,0.32); background: rgba(255,255,255,0.04); }
` `
export const SOURCE_URL = export const SOURCE_URL =