Skip to content

Commit 12dd45a

Browse files
committed
feat(docs): policy pages, contributor + moderator guides, author profiles
Six new public pages land Wave D: - /terms — Terms of Use (ownership, acceptable use, no warranty, reports and takedowns, contact) - /privacy — honest "what we collect / what we don't" statement. No tracking, one session cookie, hourly rate-limit cleanup. - /code-of-conduct — Contributor Covenant 2.1 adapted for the marketplace, with four-tier enforcement guidelines and appeals. - /docs/contributors — long-form contributor guide covering quick-start, manifest.json schema, bundle layout, review pipeline, trust tiers, common rejection reasons, GitHub auto-submit, and version management. - /docs/moderators — handbook for volunteer moderators: when to approve, reject, revoke versions, revoke whole plugins, ban authors; report triage; transparency defaults; escalation. - /authors/[username] — public author profile listing all plugins and themes by a given GitHub username. Banned authors are hidden (treated as 404 with noindex). The plugin detail page author name now links to the profile. New queries getPublicAuthorByUsername, getPublicPluginsByAuthor, and getPublicThemesByAuthor live in src/lib/db/queries.ts. All use existing mappers so PluginCard and ThemeCard render the grid directly. BaseLayout footer Legal column gains links to all three policy pages alongside the existing Security Policy and MIT License entries. 372 tests across 22 files continue to pass.
1 parent baa845f commit 12dd45a

File tree

10 files changed

+1095
-3
lines changed

10 files changed

+1095
-3
lines changed

src/components/PluginCard.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ const showCautionTier =
2727
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
2828
</svg>
2929
)}
30-
<span class="text-xs text-zinc-500">{plugin.author.name}</span>
30+
<span class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors">
31+
{plugin.author.name}
32+
</span>
3133
</div>
3234

3335
<h3 class="mt-3 text-sm font-semibold text-zinc-100">{plugin.name}</h3>

src/layouts/BaseLayout.astro

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ const navLinks = [
145145
<div>
146146
<h4 class="text-white mb-4 uppercase tracking-widest text-[10px]">Legal</h4>
147147
<ul class="text-zinc-400 space-y-2">
148-
<li><a href="/docs/security" class="hover:text-white transition-colors">Security &amp; Review Policy</a></li>
148+
<li><a href="/terms" class="hover:text-white transition-colors">Terms of Use</a></li>
149+
<li><a href="/privacy" class="hover:text-white transition-colors">Privacy Policy</a></li>
150+
<li><a href="/code-of-conduct" class="hover:text-white transition-colors">Code of Conduct</a></li>
151+
<li><a href="/docs/security" class="hover:text-white transition-colors">Security Policy</a></li>
149152
<li><a href="https://github.com/chrisjohnleah/emdashcms-org/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">MIT License</a></li>
150153
</ul>
151154
</div>

src/lib/db/queries.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,138 @@ export async function getPluginDetail(
196196
return mapPluginDetail(pluginRows[0], versionRow);
197197
}
198198

199+
// ---------------------------------------------------------------------------
200+
// Public author profile queries
201+
// ---------------------------------------------------------------------------
202+
203+
export interface PublicAuthorProfile {
204+
id: string;
205+
githubUsername: string;
206+
avatarUrl: string | null;
207+
verified: boolean;
208+
banned: boolean;
209+
createdAt: string;
210+
}
211+
212+
/**
213+
* Look up a public author by GitHub username. Returns null when no author
214+
* matches OR when the author is banned — banned authors are invisible on
215+
* the public profile surface (they can still browse).
216+
*/
217+
export async function getPublicAuthorByUsername(
218+
db: D1Database,
219+
username: string,
220+
): Promise<PublicAuthorProfile | null> {
221+
const row = await db
222+
.prepare(
223+
`SELECT id, github_username, avatar_url, verified, banned, created_at
224+
FROM authors
225+
WHERE github_username = ? COLLATE NOCASE
226+
AND COALESCE(banned, 0) = 0`,
227+
)
228+
.bind(username)
229+
.first<{
230+
id: string;
231+
github_username: string;
232+
avatar_url: string | null;
233+
verified: number;
234+
banned: number;
235+
created_at: string;
236+
}>();
237+
238+
if (!row) return null;
239+
240+
return {
241+
id: row.id,
242+
githubUsername: row.github_username,
243+
avatarUrl: row.avatar_url,
244+
verified: Boolean(row.verified),
245+
banned: Boolean(row.banned),
246+
createdAt: row.created_at,
247+
};
248+
}
249+
250+
/**
251+
* Return all publicly-listed plugins by a given author id. Only plugins
252+
* with at least one published/flagged version are returned (same rules
253+
* as searchPlugins). Shape matches MarketplacePluginSummary so PluginCard
254+
* can render them directly.
255+
*/
256+
export async function getPublicPluginsByAuthor(
257+
db: D1Database,
258+
authorId: string,
259+
): Promise<MarketplacePluginSummary[]> {
260+
const result = await db
261+
.prepare(
262+
`SELECT
263+
p.*,
264+
a.github_username,
265+
a.avatar_url,
266+
a.verified,
267+
(
268+
SELECT pv.version
269+
FROM plugin_versions pv
270+
WHERE pv.plugin_id = p.id AND pv.status IN ('published', 'flagged')
271+
ORDER BY pv.created_at DESC LIMIT 1
272+
) AS latest_version,
273+
(
274+
SELECT pv.status
275+
FROM plugin_versions pv
276+
WHERE pv.plugin_id = p.id AND pv.status IN ('published', 'flagged')
277+
ORDER BY pv.created_at DESC LIMIT 1
278+
) AS latest_version_status,
279+
(
280+
SELECT pa.verdict
281+
FROM plugin_versions pv2
282+
LEFT JOIN plugin_audits pa ON pa.plugin_version_id = pv2.id
283+
WHERE pv2.plugin_id = p.id AND pv2.status IN ('published', 'flagged')
284+
ORDER BY pv2.created_at DESC LIMIT 1
285+
) AS latest_audit_verdict,
286+
(
287+
SELECT pa.risk_score
288+
FROM plugin_versions pv3
289+
LEFT JOIN plugin_audits pa ON pa.plugin_version_id = pv3.id
290+
WHERE pv3.plugin_id = p.id AND pv3.status IN ('published', 'flagged')
291+
ORDER BY pv3.created_at DESC LIMIT 1
292+
) AS latest_audit_risk_score
293+
FROM plugins p
294+
JOIN authors a ON p.author_id = a.id
295+
WHERE p.author_id = ?
296+
AND COALESCE(p.status, 'active') = 'active'
297+
AND EXISTS (
298+
SELECT 1 FROM plugin_versions pv0
299+
WHERE pv0.plugin_id = p.id AND pv0.status IN ('published', 'flagged')
300+
)
301+
ORDER BY p.installs_count DESC, p.created_at DESC`,
302+
)
303+
.bind(authorId)
304+
.all();
305+
306+
return (result.results as Record<string, unknown>[]).map(mapPluginSummary);
307+
}
308+
309+
/**
310+
* Return all themes by a given author. Matches the searchThemes shape so
311+
* ThemeCard renders them directly.
312+
*/
313+
export async function getPublicThemesByAuthor(
314+
db: D1Database,
315+
authorId: string,
316+
): Promise<MarketplaceThemeSummary[]> {
317+
const result = await db
318+
.prepare(
319+
`SELECT t.*, a.github_username, a.avatar_url, a.verified
320+
FROM themes t
321+
JOIN authors a ON t.author_id = a.id
322+
WHERE t.author_id = ?
323+
ORDER BY t.created_at DESC`,
324+
)
325+
.bind(authorId)
326+
.all();
327+
328+
return (result.results as Record<string, unknown>[]).map(mapThemeSummary);
329+
}
330+
199331
export async function getPluginVersions(
200332
db: D1Database,
201333
pluginId: string,

src/pages/authors/[username].astro

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
export const prerender = false;
3+
4+
import { env } from 'cloudflare:workers';
5+
import BaseLayout from '../../layouts/BaseLayout.astro';
6+
import PluginCard from '../../components/PluginCard.astro';
7+
import ThemeCard from '../../components/ThemeCard.astro';
8+
import {
9+
getPublicAuthorByUsername,
10+
getPublicPluginsByAuthor,
11+
getPublicThemesByAuthor,
12+
} from '../../lib/db/queries';
13+
14+
const { username } = Astro.params;
15+
16+
const author = username
17+
? await getPublicAuthorByUsername(env.DB, username)
18+
: null;
19+
20+
if (!author) {
21+
Astro.response.status = 404;
22+
}
23+
24+
const [plugins, themes] = author
25+
? await Promise.all([
26+
getPublicPluginsByAuthor(env.DB, author.id),
27+
getPublicThemesByAuthor(env.DB, author.id),
28+
])
29+
: [[], []];
30+
31+
const formatDate = (iso: string) =>
32+
new Date(iso).toLocaleDateString('en-GB', {
33+
year: 'numeric',
34+
month: 'long',
35+
});
36+
37+
const title = author
38+
? `${author.githubUsername} - emdashcms.org`
39+
: 'Author not found - emdashcms.org';
40+
const description = author
41+
? `Plugins and themes published by ${author.githubUsername} on the emdashcms.org community marketplace.`
42+
: undefined;
43+
---
44+
<BaseLayout title={title} description={description} noindex={!author}>
45+
<section class="pt-24 pb-16 px-6">
46+
<div class="mx-auto max-w-5xl">
47+
48+
{!author ? (
49+
<div class="text-center py-16">
50+
<h1 class="text-2xl font-semibold text-zinc-100">Author not found</h1>
51+
<p class="mt-2 text-sm text-zinc-400">
52+
No public profile exists for that username. The author may not yet be registered, or their profile is
53+
hidden.
54+
</p>
55+
<a
56+
href="/plugins"
57+
class="mt-6 inline-flex items-center gap-2 rounded-lg bg-zinc-100 px-5 py-2.5 text-sm font-semibold text-zinc-950 hover:bg-white transition-colors"
58+
>
59+
Browse plugins
60+
</a>
61+
</div>
62+
) : (
63+
<>
64+
<a href="/plugins" class="text-sm text-zinc-400 hover:text-zinc-100 transition-colors">
65+
&larr; Back to Plugins
66+
</a>
67+
68+
<!-- Profile header -->
69+
<div class="mt-6 flex items-start gap-6">
70+
{author.avatarUrl ? (
71+
<img
72+
src={author.avatarUrl}
73+
alt={author.githubUsername}
74+
class="w-20 h-20 rounded-full border border-zinc-800"
75+
/>
76+
) : (
77+
<div class="w-20 h-20 rounded-full bg-zinc-800 border border-zinc-700 flex items-center justify-center">
78+
<svg class="w-10 h-10 text-zinc-500" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
79+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
80+
</svg>
81+
</div>
82+
)}
83+
<div class="flex-1 min-w-0">
84+
<div class="flex items-center gap-3 flex-wrap">
85+
<h1 class="text-3xl font-semibold tracking-tight text-zinc-100">
86+
{author.githubUsername}
87+
</h1>
88+
{author.verified && (
89+
<span class="inline-flex items-center gap-1 rounded-full border border-emerald-800/60 bg-emerald-950/40 px-2.5 py-0.5 text-[11px] font-medium uppercase tracking-wide text-emerald-400">
90+
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
91+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75 10.5 18.75 19.5 5.25" />
92+
</svg>
93+
Verified
94+
</span>
95+
)}
96+
</div>
97+
<p class="mt-1 text-sm text-zinc-500">
98+
Member since {formatDate(author.createdAt)}
99+
</p>
100+
<a
101+
href={`https://github.com/${author.githubUsername}`}
102+
target="_blank"
103+
rel="noopener noreferrer"
104+
class="mt-3 inline-flex items-center gap-1.5 text-sm text-zinc-400 hover:text-zinc-100 transition-colors"
105+
>
106+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16">
107+
<path d="M8 .2a8 8 0 0 0-2.53 15.59c.4.07.55-.17.55-.38l-.01-1.49c-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.42 7.42 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48l-.01 2.2c0 .21.15.46.55.38A8.01 8.01 0 0 0 8 .2Z" />
108+
</svg>
109+
View on GitHub
110+
</a>
111+
</div>
112+
</div>
113+
114+
<!-- Summary stats -->
115+
<div class="mt-8 flex gap-4 text-sm text-zinc-500">
116+
<span><strong class="text-zinc-100">{plugins.length}</strong> plugin{plugins.length === 1 ? '' : 's'}</span>
117+
<span><strong class="text-zinc-100">{themes.length}</strong> theme{themes.length === 1 ? '' : 's'}</span>
118+
</div>
119+
120+
<!-- Plugins -->
121+
{plugins.length > 0 && (
122+
<div class="mt-12">
123+
<h2 class="text-xl font-semibold text-zinc-100 mb-4">Plugins</h2>
124+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
125+
{plugins.map((plugin) => (
126+
<PluginCard plugin={plugin} />
127+
))}
128+
</div>
129+
</div>
130+
)}
131+
132+
<!-- Themes -->
133+
{themes.length > 0 && (
134+
<div class="mt-12">
135+
<h2 class="text-xl font-semibold text-zinc-100 mb-4">Themes</h2>
136+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
137+
{themes.map((theme) => (
138+
<ThemeCard theme={theme} />
139+
))}
140+
</div>
141+
</div>
142+
)}
143+
144+
{plugins.length === 0 && themes.length === 0 && (
145+
<div class="mt-12 rounded-xl border border-zinc-800/60 bg-zinc-900/40 p-12 text-center">
146+
<p class="text-sm text-zinc-400">
147+
{author.githubUsername} hasn't published any plugins or themes yet.
148+
</p>
149+
</div>
150+
)}
151+
</>
152+
)}
153+
154+
</div>
155+
</section>
156+
</BaseLayout>

0 commit comments

Comments
 (0)