Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions web/bun.lock

Large diffs are not rendered by default.

1,540 changes: 1,449 additions & 91 deletions web/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"cmdk": "^1.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^9.1.0",
"react-router-dom": "^7.1.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ProfileSearch } from "./components/ProfileSearch";
import { ProfileView } from "./components/ProfileView";
import { Scoreboard } from "./components/Scoreboard";
import { Settings } from "./components/Settings";
import { SkillView } from "./components/SkillView";
import { WastelandProvider } from "./context/WastelandContext";

const MARKETPLACE_URL =
Expand All @@ -35,6 +36,7 @@ export function App() {
<Route path="/profile" element={<ProfileSearch />} />
<Route path="/profile/:handle" element={<ProfileView />} />
<Route path="/scoreboard" element={<Scoreboard />} />
<Route path="/skill" element={<SkillView />} />
<Route path="/settings" element={<Settings />} />
<Route path="/connect" element={<ConnectPage />} />
<Route path="/join" element={<ConnectPage />} />
Expand Down
10 changes: 5 additions & 5 deletions web/src/components/BrowseList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,6 @@
cursor: default;
}

.pendingIndicator:hover .pendingCard,
.pendingIndicator:focus-within .pendingCard {
display: block;
}

.pendingCard {
display: none;
position: absolute;
Expand All @@ -133,6 +128,11 @@
color: var(--fg);
}

.pendingIndicator:hover .pendingCard,
.pendingIndicator:focus-within .pendingCard {
display: block;
}

.pendingCardTitle {
color: var(--dim);
font-size: var(--text-xs);
Expand Down
9 changes: 2 additions & 7 deletions web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,9 @@ export function Layout() {
sign in
</NavLink>
)}
<a
href="https://github.com/gastownhall/marketplace/blob/main/plugins/wasteland/skills/wasteland/SKILL.md"
target="_blank"
rel="noopener noreferrer"
className={styles.navLink}
>
<NavLink to="/skill" className={({ isActive }) => (isActive ? styles.navLinkActive : styles.navLink)}>
skill
</a>
</NavLink>
</nav>
<main id="main-content" className={styles.main}>
<Outlet />
Expand Down
173 changes: 173 additions & 0 deletions web/src/components/SkillView.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
.container {
max-width: 800px;
margin: 0 auto;
}

.content {
line-height: 1.7;
color: var(--fg);
font-family: var(--font-body);
font-size: var(--text-base);
}

.content h1 {
font-family: var(--font-heading);
font-size: var(--text-2xl);
color: var(--fg);
border-bottom: 2px solid var(--border);
padding-bottom: var(--space-2);
margin: var(--space-7) 0 var(--space-4);
letter-spacing: 0.05em;
}

.content h2 {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--fg);
border-bottom: 1px solid var(--border);
padding-bottom: var(--space-1);
margin: var(--space-6) 0 var(--space-3);
letter-spacing: 0.04em;
}

.content h3 {
font-family: var(--font-heading);
font-size: var(--text-lg);
color: var(--fg);
margin: var(--space-5) 0 var(--space-2);
}

.content p {
margin: var(--space-3) 0;
}

.content a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}

.content a:hover {
color: var(--accent-hover);
}

.content code {
font-family: var(--font-mono);
font-size: var(--text-sm);
background: var(--bg-alt);
padding: 2px 6px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}

.content pre {
background: var(--bg-dark);
color: var(--fg-light);
padding: var(--space-4);
border-radius: var(--radius-md);
overflow-x: auto;
margin: var(--space-4) 0;
border: 1px solid var(--border-dark);
}

.content pre code {
background: none;
border: none;
padding: 0;
color: inherit;
}

.content ul,
.content ol {
padding-left: var(--space-6);
margin: var(--space-3) 0;
}

.content li {
margin: var(--space-1) 0;
}

.content strong {
font-weight: 700;
color: var(--fg);
}

.content blockquote {
border-left: 3px solid var(--accent);
padding-left: var(--space-4);
margin: var(--space-4) 0;
color: var(--fg-muted);
font-style: italic;
}

.content hr {
border: none;
border-top: 1px solid var(--border);
margin: var(--space-6) 0;
}

.content table {
width: 100%;
border-collapse: collapse;
margin: var(--space-4) 0;
}

.content th,
.content td {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--border);
text-align: left;
}

.content th {
background: var(--bg-alt);
font-weight: 700;
}

.actions {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-5);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--border);
}

.actionBtn {
padding: var(--space-2) var(--space-4);
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--fg-muted);
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.06em;
transition:
color var(--transition-fast),
border-color var(--transition-fast);
}

.actionBtn:hover {
color: var(--accent);
border-color: var(--accent);
}

.actionBtn:disabled {
opacity: 0.4;
cursor: default;
}

.error {
color: var(--red);
font-family: var(--font-body);
font-size: var(--text-base);
margin-bottom: var(--space-4);
}

.fallbackLink {
color: var(--accent);
font-family: var(--font-body);
text-decoration: underline;
}
89 changes: 89 additions & 0 deletions web/src/components/SkillView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useCallback, useEffect, useState } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { toast } from "sonner";
import { SkeletonRows } from "./Skeleton";
import styles from "./SkillView.module.css";

const RAW_URL =
"https://raw.githubusercontent.com/gastownhall/marketplace/main/plugins/wasteland/skills/wasteland/SKILL.md";
const REPO_URL = "https://github.com/gastownhall/marketplace/blob/main/plugins/wasteland/skills/wasteland/SKILL.md";

export function SkillView() {
const [markdown, setMarkdown] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const controller = new AbortController();

fetch(RAW_URL, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`Failed to fetch (${res.status})`);
return res.text();
})
.then((text) => text.replace(/^---\n[\s\S]*?\n---\n*/, ""))
.then(setMarkdown)
.catch((err) => {
if (err.name !== "AbortError") setError(err.message);
});

return () => controller.abort();
}, []);

const copyToClipboard = useCallback(() => {
if (!markdown) return;
navigator.clipboard.writeText(markdown).then(
() => toast.success("Copied to clipboard"),
() => toast.error("Failed to copy"),
);
}, [markdown]);

const downloadFile = useCallback(() => {
if (!markdown) return;
const blob = new Blob([markdown], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wasteland-skill.md";
a.click();
URL.revokeObjectURL(url);
}, [markdown]);

if (error) {
return (
<div className={styles.container}>
<p className={styles.error}>Failed to load skill documentation: {error}</p>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer" className={styles.fallbackLink}>
View on GitHub
</a>
</div>
);
}

if (markdown === null) {
return (
<div className={styles.container}>
<SkeletonRows count={8} />
</div>
);
}

return (
<div className={styles.container}>
<div className={styles.actions}>
<button type="button" className={styles.actionBtn} onClick={copyToClipboard}>
copy
</button>
<button type="button" className={styles.actionBtn} onClick={downloadFile}>
download
</button>
<a href={REPO_URL} target="_blank" rel="noopener noreferrer" className={styles.actionBtn}>
github
</a>
</div>
<div className={styles.content}>
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
</div>
</div>
);
}
Loading