Compare commits
No commits in common. "89cfe7bdec474db0c669c70f247a66feaeb9362c" and "c959f0d8486d4f7b085e40219921e58be4605271" have entirely different histories.
89cfe7bdec
...
c959f0d848
10 changed files with 28 additions and 372 deletions
|
|
@ -356,32 +356,28 @@ nav {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-block: var(--stack-small);
|
padding-block: var(--stack-small);
|
||||||
padding-inline: 0;
|
padding-inline: 1ch;
|
||||||
}
|
}
|
||||||
.display-prefs-nav summary::-webkit-details-marker {
|
.display-prefs-nav summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.display-prefs-nav summary::before {
|
.display-prefs-nav summary:hover {
|
||||||
color: transparent;
|
padding-inline: 0;
|
||||||
text-shadow: none;
|
|
||||||
content: "[" / "";
|
|
||||||
}
|
}
|
||||||
.display-prefs-nav summary::after {
|
.display-prefs-nav summary:hover::before {
|
||||||
color: transparent;
|
color: var(--color-glow);
|
||||||
text-shadow: none;
|
content: "[";
|
||||||
content: "]" / "";
|
|
||||||
}
|
}
|
||||||
.display-prefs-nav summary:hover::before,
|
|
||||||
.display-prefs-nav summary:hover::after {
|
.display-prefs-nav summary:hover::after {
|
||||||
color: var(--color-glow);
|
color: var(--color-glow);
|
||||||
text-shadow: inherit;
|
content: "]";
|
||||||
}
|
}
|
||||||
.display-prefs-nav summary > span {
|
.display-prefs-nav summary > span {
|
||||||
translate: 0 -0.225em;
|
translate: 0 -0.225em;
|
||||||
}
|
}
|
||||||
.display-prefs-panel {
|
.display-prefs-panel {
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
border: 1px solid var(--color-accent);
|
border: 2px solid var(--color-accent);
|
||||||
min-inline-size: 10em;
|
min-inline-size: 10em;
|
||||||
padding-block: var(--stack-small);
|
padding-block: var(--stack-small);
|
||||||
padding-inline: var(--gutter-small);
|
padding-inline: var(--gutter-small);
|
||||||
|
|
@ -418,30 +414,21 @@ form p {
|
||||||
a {
|
a {
|
||||||
color: var(--color-fg);
|
color: var(--color-fg);
|
||||||
margin: 0 -1ch;
|
margin: 0 -1ch;
|
||||||
|
padding: 0 1ch;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: var(--color-glow);
|
text-decoration-color: var(--color-glow);
|
||||||
|
|
||||||
/* Brackets always occupy their space and hover only reveals them. Swapping
|
&:hover::before {
|
||||||
ch-based padding for a rendered glyph shifts the text by a subpixel,
|
color: var(--color-glow);
|
||||||
because the fluid root font-size makes 1ch fractional.
|
content: "[";
|
||||||
The / "" keeps them out of the accessible name. */
|
|
||||||
&::before {
|
|
||||||
color: transparent;
|
|
||||||
text-shadow: none;
|
|
||||||
content: "[" / "";
|
|
||||||
}
|
}
|
||||||
&::after {
|
|
||||||
color: transparent;
|
|
||||||
text-shadow: none;
|
|
||||||
content: "]" / "";
|
|
||||||
}
|
|
||||||
&:hover::before,
|
|
||||||
&:hover::after {
|
&:hover::after {
|
||||||
color: var(--color-glow);
|
color: var(--color-glow);
|
||||||
text-shadow: inherit;
|
content: "]";
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-fg);
|
color: var(--color-fg);
|
||||||
|
padding: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -452,8 +439,10 @@ footer a {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
&::before,
|
&:hover::before {
|
||||||
&::after {
|
content: none;
|
||||||
|
}
|
||||||
|
&:hover::after {
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
interface Props {
|
|
||||||
/** Shell-style path shown as the heading, e.g. "~/projects" */
|
|
||||||
path: string;
|
|
||||||
/** Command echoed under the heading, without the "$ " prompt */
|
|
||||||
command?: string;
|
|
||||||
/** Heading level; pages with their own <h1> elsewhere should stay at 2 */
|
|
||||||
level?: 1 | 2;
|
|
||||||
}
|
|
||||||
const { path, command, level = 2 } = Astro.props;
|
|
||||||
const Heading = level === 1 ? "h1" : "h2";
|
|
||||||
---
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.page-header {
|
|
||||||
margin-block-end: var(--stack-large);
|
|
||||||
}
|
|
||||||
.listing-line {
|
|
||||||
color: var(--color-mg);
|
|
||||||
font-size: var(--pt-small-pica);
|
|
||||||
}
|
|
||||||
.prompt-cursor {
|
|
||||||
margin-inline-start: 0.667em;
|
|
||||||
animation: blink 1.2s steps(1) infinite;
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
@keyframes blink {
|
|
||||||
50% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-reduce-motion="on"] .prompt-cursor {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
:root:not([data-reduce-motion="off"]) .prompt-cursor {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-more-contrast="on"] .listing-line {
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
:root:not([data-more-contrast="off"]) .listing-line {
|
|
||||||
color: var(--color-fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<hgroup class="page-header">
|
|
||||||
<Heading>
|
|
||||||
{path}
|
|
||||||
<span class="prompt-cursor" aria-hidden="true">█</span>
|
|
||||||
</Heading>
|
|
||||||
{
|
|
||||||
command && (
|
|
||||||
<p class="listing-line" aria-hidden="true">
|
|
||||||
$ {command}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</hgroup>
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
---
|
|
||||||
import type { Project } from "@lib/ProjectsCache";
|
|
||||||
interface Props { project: Project }
|
|
||||||
const { project } = Astro.props;
|
|
||||||
// Space instead of "T" gives the browser somewhere to wrap; the datetime
|
|
||||||
// attribute keeps the real ISO value.
|
|
||||||
const committedAtLabel = project.lastCommitAt.replace("T", " ");
|
|
||||||
---
|
|
||||||
<style>
|
|
||||||
.project-card {
|
|
||||||
border: 1px solid var(--color-accent);
|
|
||||||
padding: var(--gutter-small) var(--gutter-large);
|
|
||||||
display: flex; flex-direction: column; gap: var(--stack-small);
|
|
||||||
box-shadow: 0 0 6px var(--color-accent-transparent);
|
|
||||||
}
|
|
||||||
.project-card:hover { box-shadow: 0 0 12px var(--color-accent-transparent); }
|
|
||||||
.project-card header { text-align: left; }
|
|
||||||
|
|
||||||
.project-meta { color: var(--color-mg); font-size: var(--pt-small-pica); display: flex; flex-wrap: wrap; gap: var(--gutter-small); }
|
|
||||||
.last-commit { color: var(--color-mg); font-size: var(--pt-long-primer); }
|
|
||||||
.last-commit .sha { color: var(--color-accent-bright); }
|
|
||||||
|
|
||||||
.lang-bar { display: flex; block-size: 0.75ex; border: 1px solid var(--color-mg); }
|
|
||||||
.lang-seg { inline-size: var(--w); background: var(--color-accent); }
|
|
||||||
.lang-seg:nth-child(2n) { background: var(--color-accent-bright); }
|
|
||||||
.lang-seg:nth-child(3n) { background: var(--color-mg); }
|
|
||||||
.lang-legend, .topic-list { font-size: var(--pt-long-primer); display: flex; flex-wrap: wrap; gap: var(--gutter-small); }
|
|
||||||
.topic-list { list-style: none; padding: 0; color: var(--color-accent-bright); }
|
|
||||||
|
|
||||||
:root[data-more-contrast="on"] .project-card { box-shadow: none; border-color: var(--color-fg); }
|
|
||||||
:root[data-more-contrast="on"] :is(.project-meta, .last-commit) { color: var(--color-fg); }
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
:root:not([data-more-contrast="off"]) .project-card { box-shadow: none; border-color: var(--color-fg); }
|
|
||||||
:root:not([data-more-contrast="off"]) :is(.project-meta, .last-commit) { color: var(--color-fg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<article class="project-card">
|
|
||||||
<header>
|
|
||||||
<h3>
|
|
||||||
<a href={project.url} target="_blank" rel="noopener">{project.name}</a>
|
|
||||||
{project.archived && <span class="badge badge-dim">[archived]</span>}
|
|
||||||
</h3>
|
|
||||||
<p class="project-meta">
|
|
||||||
<span title="Stars">★ {project.stars}</span>
|
|
||||||
<span title="Forks">⑂ {project.forks}</span>
|
|
||||||
<time datetime={project.lastCommitAt}>{committedAtLabel}</time>
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{project.description && <p class="project-desc">{project.description}</p>}
|
|
||||||
{project.lastCommitMessage && (
|
|
||||||
<p class="last-commit">
|
|
||||||
<span class="sha">{project.lastCommitSha}</span> {project.lastCommitMessage}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{project.languages.length > 0 && (
|
|
||||||
<div class="lang-bar" role="img" aria-label={`Languages: ${project.languages.map((l) => `${l.name} ${l.percent}%`).join(", ")}`}>
|
|
||||||
{project.languages.map((l) => (
|
|
||||||
<span class="lang-seg" style={`--w: ${l.percent}%`} title={`${l.name} ${l.percent}%`} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p class="lang-legend">
|
|
||||||
{project.languages.slice(0, 4).map((l) => <span>{l.name} {l.percent}%</span>)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{project.topics.length > 0 && (
|
|
||||||
<ul class="topic-list" aria-label="Topics">
|
|
||||||
{project.topics.map((t) => <li>#{t}</li>)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
|
|
@ -44,7 +44,6 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Home</a></li>
|
<li><a href="/">Home</a></li>
|
||||||
<li><a href="/contact">Contact</a></li>
|
<li><a href="/contact">Contact</a></li>
|
||||||
<li><a href="/projects">Projects</a></li>
|
|
||||||
<li><a href="https://git.badblocks.dev" target="_blank">Git</a></li>
|
<li><a href="https://git.badblocks.dev" target="_blank">Git</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -107,7 +106,7 @@
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
Made from scratch with BAAHz: Bun, Astro, Alpine.js, and Htmz!
|
Made from scratch with BAHz: Bun, Astro, and Htmz!
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import httpFetchClient from "@lib/HttpFetchClient";
|
|
||||||
|
|
||||||
const FORGEJO_BASE = "https://git.badblocks.dev";
|
|
||||||
const API = `${FORGEJO_BASE}/api/v1`;
|
|
||||||
const HEADERS = { Accept: "application/json" };
|
|
||||||
|
|
||||||
export interface ForgejoRepoDto {
|
|
||||||
name: string;
|
|
||||||
full_name: string;
|
|
||||||
description: string;
|
|
||||||
topics: string[];
|
|
||||||
stars_count: number;
|
|
||||||
forks_count: number;
|
|
||||||
updated_at: string;
|
|
||||||
language: string;
|
|
||||||
html_url: string;
|
|
||||||
archived: boolean;
|
|
||||||
default_branch: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LastCommit { date: string; sha: string; message: string }
|
|
||||||
|
|
||||||
export const toIsoSeconds = (ts: string) =>
|
|
||||||
new Date(ts).toISOString().replace(/\.\d{3}Z$/, "");
|
|
||||||
|
|
||||||
const forgejoClient = {
|
|
||||||
searchByTopic: async (topic: string): Promise<ForgejoRepoDto[]> => {
|
|
||||||
const res = await httpFetchClient.get(
|
|
||||||
`${API}/repos/search?topic=true&q=${encodeURIComponent(topic)}&limit=50`, HEADERS);
|
|
||||||
if (!res?.ok) throw new Error("Forgejo search returned not-ok");
|
|
||||||
return res.data as ForgejoRepoDto[];
|
|
||||||
},
|
|
||||||
getLanguages: async (fullName: string): Promise<Record<string, number>> =>
|
|
||||||
httpFetchClient.get(`${API}/repos/${fullName}/languages`, HEADERS),
|
|
||||||
getLastCommit: async (fullName: string, branch: string): Promise<LastCommit | null> => {
|
|
||||||
try {
|
|
||||||
const res = await httpFetchClient.get(
|
|
||||||
`${API}/repos/${fullName}/branches/${encodeURIComponent(branch)}`, HEADERS);
|
|
||||||
const c = res?.commit;
|
|
||||||
if (!c?.timestamp) return null;
|
|
||||||
return {
|
|
||||||
date: toIsoSeconds(c.timestamp),
|
|
||||||
sha: String(c.id ?? "").slice(0, 7),
|
|
||||||
message: String(c.message ?? "").split("\n")[0].trim(),
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error?.status === 404 || error?.statusCode === 404) return null;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default forgejoClient;
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
export interface ProjectLanguage { name: string; bytes: number; percent: number }
|
|
||||||
|
|
||||||
export interface Project {
|
|
||||||
name: string; fullName: string; description: string; url: string;
|
|
||||||
topics: string[];
|
|
||||||
stars: number; forks: number;
|
|
||||||
lastCommitAt: string;
|
|
||||||
lastCommitSha: string | null;
|
|
||||||
lastCommitMessage: string | null;
|
|
||||||
languages: ProjectLanguage[];
|
|
||||||
archived: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectsSnapshot {
|
|
||||||
projects: Project[];
|
|
||||||
fetchedAt: string;
|
|
||||||
stale: boolean;
|
|
||||||
}
|
|
||||||
import forgejoClient, { toIsoSeconds, type ForgejoRepoDto, type LastCommit } from "@lib/ForgejoClient";
|
|
||||||
|
|
||||||
const SHOWCASE_TOPIC = "showcase";
|
|
||||||
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
let snapshot: ProjectsSnapshot | null = null;
|
|
||||||
let inFlight: Promise<ProjectsSnapshot> | null = null;
|
|
||||||
|
|
||||||
export async function getProjects(): Promise<ProjectsSnapshot> {
|
|
||||||
if (snapshot && Date.now() - Date.parse(snapshot.fetchedAt) < TTL_MS) return snapshot;
|
|
||||||
if (inFlight) return inFlight; // dedupe concurrent refreshes
|
|
||||||
inFlight = refresh()
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("[projects] Forgejo refresh failed:", err?.message ?? err);
|
|
||||||
if (snapshot) return (snapshot = { ...snapshot, stale: true });
|
|
||||||
return { projects: [], fetchedAt: new Date(0).toISOString(), stale: true };
|
|
||||||
})
|
|
||||||
.finally(() => { inFlight = null; });
|
|
||||||
return inFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh(): Promise<ProjectsSnapshot> {
|
|
||||||
const repos = await forgejoClient.searchByTopic(SHOWCASE_TOPIC);
|
|
||||||
const projects = await Promise.all(
|
|
||||||
repos.map(async (repo) => {
|
|
||||||
// allSettled: a flaky enrichment call degrades that field, not the page
|
|
||||||
const [langs, commit] = await Promise.allSettled([
|
|
||||||
forgejoClient.getLanguages(repo.full_name),
|
|
||||||
forgejoClient.getLastCommit(repo.full_name, repo.default_branch),
|
|
||||||
]);
|
|
||||||
return normalize(repo,
|
|
||||||
langs.status === "fulfilled" ? langs.value : {},
|
|
||||||
commit.status === "fulfilled" ? commit.value : null);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
projects.sort((a, b) =>
|
|
||||||
Date.parse(b.lastCommitAt) - Date.parse(a.lastCommitAt));
|
|
||||||
return (snapshot = { projects, fetchedAt: new Date().toISOString(), stale: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize(
|
|
||||||
repo: ForgejoRepoDto, langBytes: Record<string, number>,
|
|
||||||
commit: LastCommit | null,
|
|
||||||
): Project {
|
|
||||||
const total = Object.values(langBytes).reduce((s, n) => s + n, 0);
|
|
||||||
return {
|
|
||||||
name: repo.name, fullName: repo.full_name,
|
|
||||||
description: repo.description ?? "", url: repo.html_url,
|
|
||||||
topics: (repo.topics ?? []).filter((t) => t !== SHOWCASE_TOPIC),
|
|
||||||
stars: repo.stars_count, forks: repo.forks_count,
|
|
||||||
// updated_at only as a last resort: it tracks metadata edits, not commits
|
|
||||||
lastCommitAt: commit?.date ?? toIsoSeconds(repo.updated_at),
|
|
||||||
lastCommitSha: commit?.sha ?? null,
|
|
||||||
lastCommitMessage: commit?.message ?? null,
|
|
||||||
languages: Object.entries(langBytes)
|
|
||||||
.map(([name, bytes]) => ({ name, bytes, percent: total ? Math.round((bytes / total) * 1000) / 10 : 0 }))
|
|
||||||
.sort((a, b) => b.bytes - a.bytes),
|
|
||||||
archived: repo.archived,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from "@layouts/BaseLayout.astro";
|
import Layout from "@layouts/BaseLayout.astro";
|
||||||
import PageHeader from "@components/PageHeader.astro";
|
|
||||||
import { actions, isInputError } from "astro:actions";
|
import { actions, isInputError } from "astro:actions";
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
|
|
@ -148,7 +147,7 @@ const msgValue = pickValue("msg") || (await Astro.session?.get("msg"));
|
||||||
</style>
|
</style>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<Fragment slot="main">
|
<Fragment slot="main">
|
||||||
<PageHeader path="~/contact" command="mail badblocks" />
|
<h2>Contact</h2>
|
||||||
{
|
{
|
||||||
(nextAction != "complete" && (
|
(nextAction != "complete" && (
|
||||||
<form method="post" x-data="{}" action={actions.contact.submitForm}>
|
<form method="post" x-data="{}" action={actions.contact.submitForm}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from "@layouts/BaseLayout.astro";
|
import Layout from "@layouts/BaseLayout.astro";
|
||||||
import PageHeader from "@components/PageHeader.astro";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
const txtToHtml = (string: string) => {
|
const txtToHtml = (string: string) => {
|
||||||
|
|
@ -24,11 +23,13 @@ const license_file_contents = txtToHtml(
|
||||||
<Layout>
|
<Layout>
|
||||||
<title slot="head">LICENSE | badblocks.dev</title>
|
<title slot="head">LICENSE | badblocks.dev</title>
|
||||||
<Fragment slot="main">
|
<Fragment slot="main">
|
||||||
<PageHeader path="~/license" command="cat LICENSE" level={1} />
|
<header>
|
||||||
<p>
|
<h1>
|
||||||
<a href="https://opensource.org/license/mit" target="_blank">MIT License</a
|
<a href="https://opensource.org/license/mit" target="_blank"
|
||||||
>
|
>MIT License</a
|
||||||
</p>
|
>
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
<div style="font-size: medium;" set:html={license_file_contents} />
|
<div style="font-size: medium;" set:html={license_file_contents} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
---
|
|
||||||
import Layout from "@layouts/BaseLayout.astro";
|
|
||||||
import ProjectCard from "@components/ProjectCard.astro";
|
|
||||||
import PageHeader from "@components/PageHeader.astro";
|
|
||||||
import { getProjects } from "@lib/ProjectsCache";
|
|
||||||
export const prerender = false;
|
|
||||||
|
|
||||||
const { projects, stale } = await getProjects();
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<title slot="head">Projects | badblocks.dev</title>
|
|
||||||
<style slot="head">
|
|
||||||
.projects-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 34ch), 1fr));
|
|
||||||
gap: var(--gutter-large);
|
|
||||||
margin-block-start: var(--stack-large);
|
|
||||||
}
|
|
||||||
.stale-note,
|
|
||||||
.empty-state {
|
|
||||||
color: var(--color-accent-bright);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<Fragment slot="main">
|
|
||||||
<section class="projects">
|
|
||||||
<PageHeader path="~/projects" command="ls -la --sort=committed" />
|
|
||||||
|
|
||||||
{
|
|
||||||
stale && projects.length > 0 && (
|
|
||||||
<p class="stale-note" role="status">
|
|
||||||
[!] showing cached data — git.badblocks.dev unreachable
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
projects.length === 0 && (
|
|
||||||
<div class="empty-state">
|
|
||||||
{stale ? (
|
|
||||||
<p>
|
|
||||||
[!] could not reach{" "}
|
|
||||||
<a href="https://git.badblocks.dev">git.badblocks.dev</a> —
|
|
||||||
projects live there; try again shortly.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
total 0 — nothing tagged <code>#showcase</code> yet. Browse
|
|
||||||
everything at{" "}
|
|
||||||
<a href="https://git.badblocks.dev/badblocks">
|
|
||||||
git.badblocks.dev
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="projects-grid">
|
|
||||||
{projects.map((p) => <ProjectCard project={p} />)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Fragment>
|
|
||||||
</Layout>
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
import Layout from "@layouts/BaseLayout.astro";
|
import Layout from "@layouts/BaseLayout.astro";
|
||||||
import PageHeader from "@components/PageHeader.astro";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
@ -16,7 +15,7 @@ import PageHeader from "@components/PageHeader.astro";
|
||||||
</style>
|
</style>
|
||||||
<Fragment slot="main">
|
<Fragment slot="main">
|
||||||
<section class="tools" id="tools">
|
<section class="tools" id="tools">
|
||||||
<PageHeader path="~/tools" command="ls" />
|
<h2>Tools</h2>
|
||||||
<div class="tools-grid">
|
<div class="tools-grid">
|
||||||
<div class="tool-card">Tool 1</div>
|
<div class="tool-card">Tool 1</div>
|
||||||
<div class="tool-card">Tool 2</div>
|
<div class="tool-card">Tool 2</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue