website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
5675102
— pyrossh
2026-07-08T19:53:35+05:30
feat: port UI components to hono/html tagged templates
- src/components/commit-entry.ts +31 -0
- src/components/file-layout.ts +59 -0
- src/components/file-tree.ts +50 -0
- src/components/footer.ts +18 -0
- src/components/formatted-date.ts +11 -0
- src/components/header.ts +35 -0
- src/components/issue-card.ts +30 -0
- src/components/repo-layout.ts +51 -0
src/components/commit-entry.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
|
|
3
|
+
interface CommitEntryProps {
|
|
4
|
+
repoId: string;
|
|
5
|
+
commit: {
|
|
6
|
+
hash: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
author_name?: string;
|
|
9
|
+
date?: string;
|
|
10
|
+
message: string;
|
|
11
|
+
body?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CommitEntry({ repoId, commit }: CommitEntryProps) {
|
|
16
|
+
const author = commit.author_name || commit.author || "";
|
|
17
|
+
return html`<div class="commit-entry">
|
|
18
|
+
<div>
|
|
19
|
+
<a href="/repos/${repoId}/commits/${commit.hash}" title="${commit.hash}" rel="nofollow">
|
|
20
|
+
${commit.hash.substring(0, 8)}
|
|
21
|
+
</a>
|
|
22
|
+
—<strong>${author}</strong>
|
|
23
|
+
<small class="pull-right">
|
|
24
|
+
${commit.date ? html`<a href="/repos/${repoId}/logs" rel="nofollow">
|
|
25
|
+
<span>${new Date(commit.date).toLocaleDateString("en", { year: "numeric", month: "short", day: "numeric" })}</span>
|
|
26
|
+
</a>` : ''}
|
|
27
|
+
</small>
|
|
28
|
+
</div>
|
|
29
|
+
<pre class="commit">${commit.message}</pre>
|
|
30
|
+
</div>`
|
|
31
|
+
}
|
src/components/file-layout.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { RuntimeRepo } from '../types'
|
|
3
|
+
import type { FileNode } from '../types'
|
|
4
|
+
|
|
5
|
+
interface FileLayoutProps {
|
|
6
|
+
repo: RuntimeRepo;
|
|
7
|
+
file: FileNode;
|
|
8
|
+
pathname: string;
|
|
9
|
+
children: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FileLayout({ repo, file, pathname, children }: FileLayoutProps) {
|
|
13
|
+
return html`<div class="file-layout">
|
|
14
|
+
<hr />
|
|
15
|
+
<div class="title">
|
|
16
|
+
<h3>${file.name}</h3>
|
|
17
|
+
</div>
|
|
18
|
+
<hr />
|
|
19
|
+
<div class="nav">
|
|
20
|
+
<div>
|
|
21
|
+
<a
|
|
22
|
+
aria-current="${pathname === `/repos/${repo.id}/files/${file.name}` ? "true" : "false"}"
|
|
23
|
+
href="/repos/${repo.id}/files/${file.name}"
|
|
24
|
+
hx-get="/repos/${repo.id}/files/${file.name}"
|
|
25
|
+
hx-target="#tab-content"
|
|
26
|
+
hx-push-url="true"
|
|
27
|
+
>
|
|
28
|
+
Contents
|
|
29
|
+
</a>
|
|
30
|
+
</div>
|
|
31
|
+
<div>|</div>
|
|
32
|
+
<div>
|
|
33
|
+
<a
|
|
34
|
+
aria-current="${pathname === `/repos/${repo.id}/files/${file.name}/history` ? "true" : "false"}"
|
|
35
|
+
href="/repos/${repo.id}/files/${file.name}/history"
|
|
36
|
+
hx-get="/repos/${repo.id}/files/${file.name}/history"
|
|
37
|
+
hx-target="#tab-content"
|
|
38
|
+
hx-push-url="true"
|
|
39
|
+
>
|
|
40
|
+
History
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
<div>|</div>
|
|
44
|
+
<div>
|
|
45
|
+
<a
|
|
46
|
+
aria-current="${pathname === `/repos/${repo.id}/files/${file.name}/blame` ? "true" : "false"}"
|
|
47
|
+
href="/repos/${repo.id}/files/${file.name}/blame"
|
|
48
|
+
hx-get="/repos/${repo.id}/files/${file.name}/blame"
|
|
49
|
+
hx-target="#tab-content"
|
|
50
|
+
hx-push-url="true"
|
|
51
|
+
>
|
|
52
|
+
Blame
|
|
53
|
+
</a>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<hr />
|
|
57
|
+
${children}
|
|
58
|
+
</div>`
|
|
59
|
+
}
|
src/components/file-tree.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { HtmlEscapedString } from 'hono/utils/html'
|
|
3
|
+
import type { FileNode } from '../types'
|
|
4
|
+
|
|
5
|
+
interface FileTreeProps {
|
|
6
|
+
nodes: FileNode[];
|
|
7
|
+
repoId: string;
|
|
8
|
+
basePath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function FileTreeItem({
|
|
12
|
+
node,
|
|
13
|
+
repoId,
|
|
14
|
+
basePath,
|
|
15
|
+
}: {
|
|
16
|
+
node: FileNode;
|
|
17
|
+
repoId: string;
|
|
18
|
+
basePath: string;
|
|
19
|
+
}): HtmlEscapedString | Promise<HtmlEscapedString> {
|
|
20
|
+
const href = `/repos/${repoId}/files/${node.path}`;
|
|
21
|
+
|
|
22
|
+
if (node.isDirectory && node.children) {
|
|
23
|
+
return html`<li>
|
|
24
|
+
<details open="true">
|
|
25
|
+
<summary>
|
|
26
|
+
<a href="${href}" hx-get="${href}" hx-target="#tab-content" hx-push-url="true">
|
|
27
|
+
${node.name}/
|
|
28
|
+
</a>
|
|
29
|
+
</summary>
|
|
30
|
+
<ul>
|
|
31
|
+
${node.children.map((child) => FileTreeItem({ node: child, repoId, basePath }))}
|
|
32
|
+
</ul>
|
|
33
|
+
</details>
|
|
34
|
+
</li>`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return html`<li>
|
|
38
|
+
<a href="${href}" hx-get="${href}" hx-target="#tab-content" hx-push-url="true">
|
|
39
|
+
${node.name}
|
|
40
|
+
</a>
|
|
41
|
+
</li>`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function FileTree({ nodes, repoId, basePath = "" }: FileTreeProps) {
|
|
45
|
+
return html`<div class="file-tree">
|
|
46
|
+
<ul>
|
|
47
|
+
${nodes.map((node) => FileTreeItem({ node, repoId, basePath }))}
|
|
48
|
+
</ul>
|
|
49
|
+
</div>`
|
|
50
|
+
}
|
src/components/footer.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
|
|
3
|
+
export function Footer() {
|
|
4
|
+
return html`<footer>
|
|
5
|
+
<div class="wrapper">
|
|
6
|
+
<div class="container">
|
|
7
|
+
<div class="spacer"></div>
|
|
8
|
+
<p class="copyright-container">
|
|
9
|
+
Copyright © <current-year>${new Date().getFullYear()}</current-year>
|
|
10
|
+
<a class="link" href="https://pyrossh.dev">
|
|
11
|
+
pyrossh
|
|
12
|
+
</a>
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</footer>
|
|
17
|
+
<div class="safe-area"></div>`
|
|
18
|
+
}
|
src/components/formatted-date.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
|
|
3
|
+
interface FormattedDateProps {
|
|
4
|
+
date: Date;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function FormattedDate({ date }: FormattedDateProps) {
|
|
8
|
+
return html`<time datetime="${date.toISOString()}">
|
|
9
|
+
${new Intl.DateTimeFormat("en-GB", { dateStyle: "long" }).format(new Date(date))}
|
|
10
|
+
</time>`
|
|
11
|
+
}
|
src/components/header.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import { NAV_ITEMS } from '../config'
|
|
3
|
+
|
|
4
|
+
interface HeaderProps {
|
|
5
|
+
currentPath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Header({ currentPath }: HeaderProps) {
|
|
9
|
+
const isActive = (href: string) => currentPath?.startsWith(href);
|
|
10
|
+
return html`<div class="safe-area"></div>
|
|
11
|
+
<header>
|
|
12
|
+
<div class="wrapper">
|
|
13
|
+
<nav>
|
|
14
|
+
<a href="/" class="logo${currentPath === "/" ? " active" : ""}">
|
|
15
|
+
木 pyrossh
|
|
16
|
+
</a>
|
|
17
|
+
<div class="links">
|
|
18
|
+
<span id="astro-color-scheme-switch">
|
|
19
|
+
<button
|
|
20
|
+
id="theme-change"
|
|
21
|
+
type="button"
|
|
22
|
+
onclick="document.documentElement.toggleAttribute('data-theme')"
|
|
23
|
+
>
|
|
24
|
+
<svg style="display:var(--btn-light)" viewBox="0 0 24 24" width="1.2em" height="1.2em"><path fill="currentColor" d="M11.288 4.713Q11 4.425 11 4V2q0-.425.288-.712T12 1t.713.288T13 2v2q0 .425-.288.713T12 5t-.712-.288M16.95 7.05q-.275-.275-.275-.687t.275-.713l1.4-1.425q.3-.3.712-.3t.713.3q.275.275.275.7t-.275.7L18.35 7.05q-.275.275-.7.275t-.7-.275M20 13q-.425 0-.713-.288T19 12t.288-.712T20 11h2q.425 0 .713.288T23 12t-.288.713T22 13zm-8.712 9.713Q11 22.425 11 22v-2q0-.425.288-.712T12 19t.713.288T13 20v2q0 .425-.288.713T12 23t-.712-.288M5.65 7.05l-1.425-1.4q-.3-.3-.3-.725t.3-.7q.275-.275.7-.275t.7.275L7.05 5.65q.275.275.275.7t-.275.7q-.3.275-.7.275t-.7-.275m12.7 12.725l-1.4-1.425q-.275-.3-.275-.712t.275-.688t.688-.275t.712.275l1.425 1.4q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3M2 13q-.425 0-.712-.288T1 12t.288-.712T2 11h2q.425 0 .713.288T5 12t-.288.713T4 13zm2.225 6.775q-.275-.275-.275-.7t.275-.7L5.65 16.95q.275-.275.687-.275t.713.275q.3.3.3.713t-.3.712l-1.4 1.4q-.3.3-.725.3t-.7-.3M7.75 16.25Q6 14.5 6 12t1.75-4.25T12 6t4.25 1.75T18 12t-1.75 4.25T12 18t-4.25-1.75"/></svg>
|
|
25
|
+
<svg style="display:var(--btn-dark)" viewBox="0 0 24 24" width="1.2em" height="1.2em"><path fill="currentColor" d="m15 8l-3-3l3-3l3 3zm5 3l-2-2l2-2l2 2zm-7.925 11q-2.1 0-3.937-.8t-3.2-2.162t-2.163-3.2t-.8-3.938q0-3.65 2.325-6.437T10.225 2q-.45 2.475.275 4.838t2.5 4.137t4.138 2.5t4.837.275q-.65 3.6-3.45 5.925T12.075 22"/></svg>
|
|
26
|
+
</button>
|
|
27
|
+
</span>
|
|
28
|
+
<div>|</div>
|
|
29
|
+
${NAV_ITEMS.map((item) => html`<a href="${item.href}" class="${isActive(item.href) ? "active" : ""}">${item.label}</a><div>|</div>`)}
|
|
30
|
+
</div>
|
|
31
|
+
</nav>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
<div class="safe-area"></div>`
|
|
35
|
+
}
|
src/components/issue-card.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { GitBugIssue } from '../types'
|
|
3
|
+
|
|
4
|
+
interface IssueCardProps {
|
|
5
|
+
repoId: string;
|
|
6
|
+
issue: GitBugIssue;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function IssueCard({ repoId, issue }: IssueCardProps) {
|
|
10
|
+
const date = issue.updatedAt ?? issue.createdAt;
|
|
11
|
+
return html`<article class="issue-card">
|
|
12
|
+
<div class="issue-title">
|
|
13
|
+
<a href="/repos/${repoId}/issues/${issue.id}" title="${issue.id}">
|
|
14
|
+
${issue.title}
|
|
15
|
+
</a>
|
|
16
|
+
<span class="state ${issue.state}">${issue.state}</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="meta">
|
|
19
|
+
<a href="/repos/${repoId}/issues/${issue.id}" title="${issue.id}">
|
|
20
|
+
${issue.id.substring(0, 8)}
|
|
21
|
+
</a>
|
|
22
|
+
${issue.author ? html`<span>by ${issue.author}</span>` : ''}
|
|
23
|
+
${date ? html`<span>${new Date(date).toLocaleDateString("en", { year: "numeric", month: "short", day: "numeric" })}</span>` : ''}
|
|
24
|
+
${issue.comments.length > 0 ? html`<span>${issue.comments.length} comments</span>` : ''}
|
|
25
|
+
</div>
|
|
26
|
+
${issue.labels.length > 0 ? html`<div class="labels">
|
|
27
|
+
${issue.labels.map((label) => html`<span class="label">#${label}</span>`)}
|
|
28
|
+
</div>` : ''}
|
|
29
|
+
</article>`
|
|
30
|
+
}
|
src/components/repo-layout.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { RuntimeRepo } from '../types'
|
|
3
|
+
|
|
4
|
+
interface RepoLayoutProps {
|
|
5
|
+
repo: RuntimeRepo;
|
|
6
|
+
children: any;
|
|
7
|
+
currentTab: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function RepoLayout({ repo, children, currentTab }: RepoLayoutProps) {
|
|
11
|
+
const { title, description, tags } = repo.data;
|
|
12
|
+
return html`<div class="repo">
|
|
13
|
+
<div class="header">
|
|
14
|
+
<h1>
|
|
15
|
+
<a href="/">~repos</a>/${title}
|
|
16
|
+
</h1>
|
|
17
|
+
<div class="tags">
|
|
18
|
+
${tags.map((tag) => html`<span class="tag">#${tag}</span>`)}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<h3>
|
|
22
|
+
GIT_CONFIG_PARAMETERS="'http.version=HTTP/1.1'" git clone{" "}
|
|
23
|
+
<a>https://git.pyrossh.dev/${title}/.git</a> ${title}
|
|
24
|
+
</h3>
|
|
25
|
+
<div class="headerExtension">
|
|
26
|
+
<h2>${description}</h2>
|
|
27
|
+
</div>
|
|
28
|
+
<hr />
|
|
29
|
+
<div class="nav">
|
|
30
|
+
${["Readme", "Commits", "Files"].map((tab) => {
|
|
31
|
+
const href =
|
|
32
|
+
tab === "Readme" ? `/repos/${title}` : `/repos/${title}/${tab.toLowerCase()}`;
|
|
33
|
+
const active = currentTab === tab;
|
|
34
|
+
return html`<div>
|
|
35
|
+
<a
|
|
36
|
+
href="${href}"
|
|
37
|
+
aria-current="${active ? "true" : "false"}"
|
|
38
|
+
hx-get="${href}"
|
|
39
|
+
hx-target="#tab-content"
|
|
40
|
+
hx-push-url="true"
|
|
41
|
+
style="${active ? "text-underline-offset:4px;text-decoration:underline" : ""}"
|
|
42
|
+
>
|
|
43
|
+
${tab}
|
|
44
|
+
</a>
|
|
45
|
+
<span>|</span>
|
|
46
|
+
</div>`
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div id="tab-content">${children}</div>`
|
|
51
|
+
}
|