next-site: browse page
This commit is contained in:
parent
84b4ac96f5
commit
c52eb517b8
3 changed files with 156 additions and 0 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { getAllChannels, getAllOverrides } from './db'
|
||||
import { renderBrowsePage } from './pages/browse'
|
||||
import { buildPublicIndex } from './publicIndex'
|
||||
import type { Env } from './env'
|
||||
import type { BaseIndex } from './publicIndex'
|
||||
|
|
@ -25,6 +26,22 @@ export default {
|
|||
})
|
||||
}
|
||||
|
||||
if (url.pathname === '/') {
|
||||
const baseRes = await env.ASSETS.fetch(
|
||||
new URL('/index.json', request.url),
|
||||
)
|
||||
const base = await baseRes.json<BaseIndex>()
|
||||
const [overrides, channels] = await Promise.all([
|
||||
getAllOverrides(env),
|
||||
getAllChannels(env),
|
||||
])
|
||||
const merged = buildPublicIndex(base, overrides, channels)
|
||||
|
||||
return new Response(renderBrowsePage(merged, overrides), {
|
||||
headers: { 'content-type': 'text/html; charset=utf-8' },
|
||||
})
|
||||
}
|
||||
|
||||
return env.ASSETS.fetch(request)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
51
worker/src/pages/browse.ts
Normal file
51
worker/src/pages/browse.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { escapeHtml, htmlShell } from '../theme'
|
||||
import type { Override } from '../db'
|
||||
import type { BaseIndex } from '../publicIndex'
|
||||
|
||||
export function renderBrowsePage(
|
||||
index: BaseIndex,
|
||||
overrides: Record<string, Override>,
|
||||
): string {
|
||||
const entries = Object.entries(index.plugins)
|
||||
|
||||
const cards = entries
|
||||
.map(([id, plugin]) => {
|
||||
const override = overrides[id]
|
||||
const version = plugin.channels.latest ?? Object.keys(plugin.versions)[0]
|
||||
const tagline = override?.tagline ?? plugin.description
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-top">
|
||||
<div class="card-icon">${plugin.icon ? escapeHtml(plugin.icon) : '\u{1F9E9}'}</div>
|
||||
<div class="badge">STABLE</div>
|
||||
</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-foot">
|
||||
<div class="card-meta">${escapeHtml(plugin.author)} · v${escapeHtml(version ?? '?')}</div>
|
||||
<a class="btn" href="/plugins/${encodeURIComponent(id)}">View</a>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
const body = `
|
||||
<header>
|
||||
<div class="brand">
|
||||
<div class="brand-mark">N</div>
|
||||
<div class="brand-name">${escapeHtml(index.name)}</div>
|
||||
</div>
|
||||
<nav><span>Plugins</span></nav>
|
||||
</header>
|
||||
<div class="wrap">
|
||||
<div class="page-title" style="margin-top: 22px;">Plugins</div>
|
||||
<div class="page-sub">${entries.length} available · installs directly into Revenge Next</div>
|
||||
<div class="grid">${cards}</div>
|
||||
</div>`
|
||||
|
||||
return htmlShell({
|
||||
title: index.name,
|
||||
description: index.description || 'Plugins for Revenge Next.',
|
||||
body,
|
||||
})
|
||||
}
|
||||
88
worker/src/theme.ts
Normal file
88
worker/src/theme.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
export const PAGE_CSS = `
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0b0c10;
|
||||
color: #e8e9ed;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.wrap { max-width: 960px; margin: 0 auto; padding: 0 24px; }
|
||||
header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 16px 24px; border-bottom: 1px solid rgba(255,255,255,0.07);
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.brand-mark {
|
||||
width: 22px; height: 22px; border-radius: 6px; background: #15161c;
|
||||
border: 1px solid rgba(255,255,255,0.12); display: flex; align-items: center;
|
||||
justify-content: center; font-size: 11px; font-weight: 700; color: #9a8cff;
|
||||
}
|
||||
.brand-name { font-weight: 600; font-size: 13.5px; letter-spacing: -0.01em; }
|
||||
nav { display: flex; align-items: center; gap: 18px; font-size: 12px; color: rgba(255,255,255,0.5); }
|
||||
.page-title { font-size: 19px; font-weight: 650; letter-spacing: -0.015em; margin-bottom: 4px; }
|
||||
.page-sub { font-size: 12.5px; color: rgba(255,255,255,0.45); margin-bottom: 16px; }
|
||||
.chip {
|
||||
background: transparent; border: 1px solid rgba(255,255,255,0.07); border-radius: 7px;
|
||||
padding: 5px 11px; font-size: 11.5px; color: rgba(255,255,255,0.4); display: inline-block;
|
||||
}
|
||||
.chip.active { background: #17181e; border-color: rgba(255,255,255,0.09); color: rgba(255,255,255,0.85); }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 4px 0 24px; }
|
||||
.card {
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.015));
|
||||
border: 1px solid rgba(255,255,255,0.08); border-radius: 11px; padding: 15px;
|
||||
}
|
||||
.card-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px; }
|
||||
.card-icon {
|
||||
width: 30px; height: 30px; border-radius: 8px; background: #1a1b22;
|
||||
border: 1px solid rgba(255,255,255,0.1); display: flex; align-items: center;
|
||||
justify-content: center; font-size: 13px;
|
||||
}
|
||||
.badge {
|
||||
background: rgba(154,140,255,0.12); color: #b6acff; border: 1px solid rgba(154,140,255,0.25);
|
||||
font-size: 9.5px; font-weight: 600; padding: 2px 7px; border-radius: 5px;
|
||||
}
|
||||
.card-name { font-size: 13.5px; font-weight: 600; letter-spacing: -0.005em; }
|
||||
.card-desc { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 3px; line-height: 1.4; }
|
||||
.card-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
|
||||
.card-meta { font-size: 10px; color: rgba(255,255,255,0.35); }
|
||||
.btn {
|
||||
background: #e8e9ed; color: #0b0c10; font-size: 10.5px; font-weight: 600;
|
||||
padding: 4px 10px; border-radius: 6px; display: inline-block;
|
||||
}
|
||||
`
|
||||
|
||||
export function htmlShell(opts: {
|
||||
title: string
|
||||
description: string
|
||||
ogTitle?: string
|
||||
ogDescription?: string
|
||||
body: string
|
||||
}): string {
|
||||
const ogTitle = opts.ogTitle ?? opts.title
|
||||
const ogDescription = opts.ogDescription ?? opts.description
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${escapeHtml(opts.title)}</title>
|
||||
<meta name="description" content="${escapeHtml(opts.description)}">
|
||||
<meta property="og:title" content="${escapeHtml(ogTitle)}">
|
||||
<meta property="og:description" content="${escapeHtml(ogDescription)}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="${escapeHtml(ogTitle)}">
|
||||
<meta name="twitter:description" content="${escapeHtml(ogDescription)}">
|
||||
<style>${PAGE_CSS}</style>
|
||||
</head>
|
||||
<body>${opts.body}</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
export function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue