website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


5675102pyrossh 2026-07-08T19:53:35+05:30
feat: port UI components to hono/html tagged templates
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
+ }