website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
d31af54
— pyrossh
2026-07-08T19:58:30+05:30
feat: create Hono app with all 23 routes
- src/index.ts +829 -0
src/index.ts
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { html, raw } from 'hono/html'
|
|
3
|
+
import { getS3 } from './s3'
|
|
4
|
+
import { getPosts, getPost, getRepos, getRepo } from './lib/content'
|
|
5
|
+
import { getRepoReadme } from './lib/repoContent'
|
|
6
|
+
import { renderMarkdown } from './lib/markdown'
|
|
7
|
+
import { getCommits, getFiles, getFileHistory, getFileContentData, generateHTMLDiff } from './lib/gitReader'
|
|
8
|
+
import { buildFileTree, sortChildren } from './lib/files'
|
|
9
|
+
import { getGitBugIssues, getGitBugIssue, createGitBugIssue, addGitBugIssueComment, setGitBugIssueState } from './lib/gitBug'
|
|
10
|
+
import { Header } from './components/header'
|
|
11
|
+
import { Footer } from './components/footer'
|
|
12
|
+
import { RepoLayout } from './components/repo-layout'
|
|
13
|
+
import { FileLayout } from './components/file-layout'
|
|
14
|
+
import { CommitEntry } from './components/commit-entry'
|
|
15
|
+
import { IssueCard } from './components/issue-card'
|
|
16
|
+
import { FileTree } from './components/file-tree'
|
|
17
|
+
import { SITE_TITLE, SITE_DESCRIPTION, SITE_URL, REPOS, TOOLS } from './config'
|
|
18
|
+
|
|
19
|
+
const app = new Hono()
|
|
20
|
+
|
|
21
|
+
const Layout = (props: {
|
|
22
|
+
title: string
|
|
23
|
+
description?: string
|
|
24
|
+
css?: string
|
|
25
|
+
currentPath?: string
|
|
26
|
+
children: any
|
|
27
|
+
}) => html`
|
|
28
|
+
<!DOCTYPE html>
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head>
|
|
31
|
+
<meta charset="utf-8" />
|
|
32
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
33
|
+
<meta name="theme-color" content="#131618" />
|
|
34
|
+
<link rel="icon" type="image/svg+xml" href="/assets/icons/icon.svg" />
|
|
35
|
+
<link rel="alternate" type="application/rss+xml" title="${props.title}" href="/rss.xml" />
|
|
36
|
+
<link rel="canonical" href="${SITE_URL}/" />
|
|
37
|
+
<link rel="stylesheet" href="/assets/css/shared.css" />
|
|
38
|
+
${props.css ? html`<link rel="stylesheet" href="${props.css}" />` : ''}
|
|
39
|
+
<title>${props.title}</title>
|
|
40
|
+
<meta name="title" content="${props.title}" />
|
|
41
|
+
<meta name="description" content="${props.description ?? SITE_DESCRIPTION}" />
|
|
42
|
+
<meta property="og:title" content="${props.title}" />
|
|
43
|
+
<meta property="og:description" content="${props.description ?? SITE_DESCRIPTION}" />
|
|
44
|
+
<meta property="og:image" content="/assets/icons/icon.svg" />
|
|
45
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
46
|
+
<script src="https://unpkg.com/[email protected]"></script>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
${Header({ currentPath: props.currentPath })}
|
|
50
|
+
<div class="wrapper">
|
|
51
|
+
<main>${props.children}</main>
|
|
52
|
+
</div>
|
|
53
|
+
${Footer()}
|
|
54
|
+
</body>
|
|
55
|
+
</html>`
|
|
56
|
+
|
|
57
|
+
// ─── 1. Simple routes ───────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
app.get('/', (c) => {
|
|
60
|
+
return c.html(Layout({
|
|
61
|
+
css: '/assets/css/workers/home.css',
|
|
62
|
+
title: SITE_TITLE,
|
|
63
|
+
description: 'Tech Lead from Bangalore who likes to create frameworks and programming languages.',
|
|
64
|
+
currentPath: c.req.path,
|
|
65
|
+
children: html`<div class="pageContainer">
|
|
66
|
+
<h1 class="title">Hello!</h1>
|
|
67
|
+
<p class="description">
|
|
68
|
+
<a href="https://www.linkedin.com/in/pyrossh" target="_blank" rel="noopener noreferrer" class="linkUnderline"><strong>I'm</strong></a>
|
|
69
|
+
a tech lead based out of Bangalore, India and have worked for startups and enterprises
|
|
70
|
+
that focus on products in the healthcare and finance domains. I like to build
|
|
71
|
+
frameworks and automate common tasks to make it easier to develop applications.
|
|
72
|
+
</p>
|
|
73
|
+
<p class="description">
|
|
74
|
+
I enjoy working on open-source projects, and during the last decade, I've created a
|
|
75
|
+
number of projects that are widely used.
|
|
76
|
+
</p>
|
|
77
|
+
<div class="gridContainer">
|
|
78
|
+
<section class="interests">
|
|
79
|
+
<h2>Interests</h2>
|
|
80
|
+
<ul style="list-style:none;padding:0">
|
|
81
|
+
${['HTML', 'CSS', 'Javascript', 'S3', 'SQL', 'Bun', 'Astro', 'Tauri', 'Remix', 'Zig', 'K8s', 'Iceberg'].map((i) => html`<li>${i}</li>`)}
|
|
82
|
+
</ul>
|
|
83
|
+
</section>
|
|
84
|
+
<section class="contact">
|
|
85
|
+
<h2>Contact</h2>
|
|
86
|
+
<ul style="list-style:none;padding:0">
|
|
87
|
+
<li><strong>email:</strong><span>[email protected]</span></li>
|
|
88
|
+
<li><strong>crates:</strong><a href="https://crates.io/users/pyrossh">https://crates.io/users/pyrossh</a></li>
|
|
89
|
+
<li><strong>npm:</strong><a href="https://www.npmjs.com/~pyrossh">https://www.npmjs.com/~pyrossh</a></li>
|
|
90
|
+
<li><strong>linkedin:</strong><a href="https://www.linkedin.com/in/pyrossh">https://www.linkedin.com/in/pyrossh</a></li>
|
|
91
|
+
<li><strong>google group:</strong><a href="https://groups.google.com/g/rust-embed-devs">rust-embed-devs</a></li>
|
|
92
|
+
</ul>
|
|
93
|
+
</section>
|
|
94
|
+
</div>
|
|
95
|
+
<section class="tools">
|
|
96
|
+
<h2>Tools</h2>
|
|
97
|
+
<ul style="list-style:none;padding:0">
|
|
98
|
+
${TOOLS.map((tool) => html`<li>
|
|
99
|
+
<a href="${tool.link}" target="_blank" rel="noopener noreferrer">
|
|
100
|
+
<div>${tool.name}</div>
|
|
101
|
+
${tool.image ? html`<img src="${tool.image}" alt="${tool.name}" />` : ''}
|
|
102
|
+
</a>
|
|
103
|
+
</li>`)}
|
|
104
|
+
</ul>
|
|
105
|
+
</section>
|
|
106
|
+
<section>
|
|
107
|
+
<h2>Git</h2>
|
|
108
|
+
<div class="repos">
|
|
109
|
+
${REPOS.map((repo) => html`<div class="repo">
|
|
110
|
+
<h4><a href="/repos/${repo.title}">${repo.title}</a></h4>
|
|
111
|
+
<p>${repo.description}</p>
|
|
112
|
+
<div class="tags">
|
|
113
|
+
${repo.tags.map((tag) => html`<span class="tag">#${tag}</span>`)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>`)}
|
|
116
|
+
</div>
|
|
117
|
+
</section>
|
|
118
|
+
</div>`
|
|
119
|
+
}))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
app.get('/cv', (c) => {
|
|
123
|
+
return c.html(Layout({
|
|
124
|
+
css: '/assets/css/workers/cv.css',
|
|
125
|
+
title: 'Curriculum Vitae',
|
|
126
|
+
description: 'Peter John CV',
|
|
127
|
+
currentPath: c.req.path,
|
|
128
|
+
children: html`<div>
|
|
129
|
+
<h1 style="display:flex;align-items:flex-start;font-weight:800;font-size:2rem;margin-bottom:1rem">
|
|
130
|
+
Curriculum Vitae
|
|
131
|
+
<a href="/assets/pdfs/resume.pdf" target="_blank" rel="noopener noreferrer" style="margin-left:0.4rem;margin-top:0.8rem;color:var(--color-link)">↗</a>
|
|
132
|
+
</h1>
|
|
133
|
+
<div style="border:1px solid black">
|
|
134
|
+
<img src="/assets/pdfs/resume.svg" alt="Resume" style="width:100%" />
|
|
135
|
+
</div>
|
|
136
|
+
</div>`
|
|
137
|
+
}))
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
app.get('/robots.txt', (c) => {
|
|
141
|
+
return c.text('User-agent: *\nAllow: /\n')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
app.notFound((c) => {
|
|
145
|
+
return c.html(Layout({
|
|
146
|
+
title: '404 - Not Found',
|
|
147
|
+
description: 'Page not found',
|
|
148
|
+
currentPath: c.req.path,
|
|
149
|
+
children: html`<h1>404 - Page Not Found</h1>
|
|
150
|
+
<p>Sorry, the page you are looking for does not exist.</p>
|
|
151
|
+
<a href="/">Go back home</a>`
|
|
152
|
+
}), 404)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
app.onError((err, c) => {
|
|
156
|
+
console.error('Unhandled error:', err)
|
|
157
|
+
return c.html(Layout({
|
|
158
|
+
title: '500 - Server Error',
|
|
159
|
+
description: 'Internal server error',
|
|
160
|
+
currentPath: c.req.path,
|
|
161
|
+
children: html`<h1>500 - Internal Server Error</h1>
|
|
162
|
+
<p>Something went wrong. Please try again later.</p>
|
|
163
|
+
<a href="/">Go back home</a>`
|
|
164
|
+
}), 500)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
app.get('/only-bible-app', (c) => {
|
|
168
|
+
return c.html(Layout({
|
|
169
|
+
css: '/assets/css/workers/only-bible-app.css',
|
|
170
|
+
title: 'Only Bible App',
|
|
171
|
+
description: 'The only bible app you will ever need',
|
|
172
|
+
currentPath: c.req.path,
|
|
173
|
+
children: html`<div style="display:flex;flex:1;flex-direction:column;padding-bottom:16rem">
|
|
174
|
+
<div style="display:flex;flex-direction:column;align-items:center">
|
|
175
|
+
<div style="display:block;margin-bottom:1rem">
|
|
176
|
+
<h1 style="display:flex;flex-direction:column;align-items:center;font-size:1.875rem;line-height:2.25rem;font-weight:700;text-align:center">
|
|
177
|
+
The only bible app you will ever need
|
|
178
|
+
</h1>
|
|
179
|
+
<div style="text-align:center;display:flex;flex-direction:column;font-size:1.125rem;line-height:1.75rem">
|
|
180
|
+
<span>No ads,</span>
|
|
181
|
+
<span>No in-app purchases,</span>
|
|
182
|
+
<span>No distractions.</span>
|
|
183
|
+
<span>Works completely offline.</span>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div style="display:flex;flex:1;flex-direction:column;align-items:center">
|
|
187
|
+
<h3 style="margin:0;margin-top:1rem;margin-bottom:1rem;font-weight:700;font-size:1.25rem;line-height:1.75rem">
|
|
188
|
+
What are you waiting for? Get it now! Quickly.
|
|
189
|
+
</h3>
|
|
190
|
+
<div style="display:flex;flex-direction:column">
|
|
191
|
+
<a style="margin-top:1rem" href="https://play.google.com/store/apps/details?id=dev.pyrossh.onlyBible" rel="noopener noreferrer" target="_blank">
|
|
192
|
+
<svg viewBox="0 0 135 40" width="196" xmlns="http://www.w3.org/2000/svg" fill="#000000">
|
|
193
|
+
<path fill="#a6a6a6" d="M130 40H5c-2.8 0-5-2.2-5-5V5c0-2.8 2.2-5 5-5h125c2.8 0 5 2.2 5 5v30c0 2.8-2.2 5-5 5z"></path>
|
|
194
|
+
<path fill="#fff" stroke="#fff" stroke-width="0.2" stroke-miterlimit="10" d="M47.4 10.2c0 .8-.2 1.5-.7 2-.6.6-1.3.9-2.2.9-.9 0-1.6-.3-2.2-.9-.6-.6-.9-1.3-.9-2.2 0-.9.3-1.6.9-2.2.6-.6 1.3-.9 2.2-.9.4 0 .8.1 1.2.3.4.2.7.4.9.7l-.5.5c-.4-.5-.9-.7-1.6-.7-.6 0-1.2.2-1.6.7-.5.4-.7 1-.7 1.7s.2 1.3.7 1.7c.5.4 1 .7 1.6.7.7 0 1.2-.2 1.7-.7.3-.3.5-.7.5-1.2h-2.2v-.8h2.9v.4zM52 7.7h-2.7v1.9h2.5v.7h-2.5v1.9H52v.8h-3.5V7H52v.7zM55.3 13h-.8V7.7h-1.7V7H57v.7h-1.7V13zM59.9 13V7h.8v6h-.8zM64.1 13h-.8V7.7h-1.7V7h4.1v.7H64V13zM73.6 12.2c-.6.6-1.3.9-2.2.9-.9 0-1.6-.3-2.2-.9-.6-.6-.9-1.3-.9-2.2s.3-1.6.9-2.2c.6-.6 1.3-.9 2.2-.9.9 0 1.6.3 2.2.9.6.6.9 1.3.9 2.2 0 .9-.3 1.6-.9 2.2zm-3.8-.5c.4.4 1 .7 1.6.7.6 0 1.2-.2 1.6-.7.4-.4.7-1 .7-1.7s-.2-1.3-.7-1.7c-.4-.4-1-.7-1.6-.7-.6 0-1.2.2-1.6.7-.4.4-.7 1-.7 1.7s.2 1.3.7 1.7zM75.6 13V7h.9l2.9 4.7V7h.8v6h-.8l-3.1-4.9V13h-.7z"></path>
|
|
195
|
+
<path fill="#fff" d="M68.1 21.8c-2.4 0-4.3 1.8-4.3 4.3 0 2.4 1.9 4.3 4.3 4.3s4.3-1.8 4.3-4.3c0-2.6-1.9-4.3-4.3-4.3zm0 6.8c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6c1.3 0 2.4 1 2.4 2.6 0 1.5-1.1 2.6-2.4 2.6zm-9.3-6.8c-2.4 0-4.3 1.8-4.3 4.3 0 2.4 1.9 4.3 4.3 4.3s4.3-1.8 4.3-4.3c0-2.6-1.9-4.3-4.3-4.3zm0 6.8c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6c1.3 0 2.4 1 2.4 2.6 0 1.5-1.1 2.6-2.4 2.6zm-11.1-5.5v1.8H52c-.1 1-.5 1.8-1 2.3-.6.6-1.6 1.3-3.3 1.3-2.7 0-4.7-2.1-4.7-4.8s2.1-4.8 4.7-4.8c1.4 0 2.5.6 3.3 1.3l1.3-1.3c-1.1-1-2.5-1.8-4.5-1.8-3.6 0-6.7 3-6.7 6.6 0 3.6 3.1 6.6 6.7 6.6 2 0 3.4-.6 4.6-1.9 1.2-1.2 1.6-2.9 1.6-4.2 0-.4 0-.8-.1-1.1h-6.2zm45.4 1.4c-.4-1-1.4-2.7-3.6-2.7s-4 1.7-4 4.3c0 2.4 1.8 4.3 4.2 4.3 1.9 0 3.1-1.2 3.5-1.9l-1.4-1c-.5.7-1.1 1.2-2.1 1.2s-1.6-.4-2.1-1.3l5.7-2.4-.2-.5zm-5.8 1.4c0-1.6 1.3-2.5 2.2-2.5.7 0 1.4.4 1.6.9l-3.8 1.6zM82.6 30h1.9V17.5h-1.9V30zm-3-7.3c-.5-.5-1.3-1-2.3-1-2.1 0-4.1 1.9-4.1 4.3s1.9 4.2 4.1 4.2c1 0 1.8-.5 2.2-1h.1v.6c0 1.6-.9 2.5-2.3 2.5-1.1 0-1.9-.8-2.1-1.5l-1.6.7c.5 1.1 1.7 2.5 3.8 2.5 2.2 0 4-1.3 4-4.4V22h-1.8v.7zm-2.2 5.9c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6c1.3 0 2.3 1.1 2.3 2.6s-1 2.6-2.3 2.6zm24.4-11.1h-4.5V30h1.9v-4.7h2.6c2.1 0 4.1-1.5 4.1-3.9s-2-3.9-4.1-3.9zm.1 6h-2.7v-4.3h2.7c1.4 0 2.2 1.2 2.2 2.1-.1 1.1-.9 2.2-2.2 2.2zm11.5-1.8c-1.4 0-2.8.6-3.3 1.9l1.7.7c.4-.7 1-.9 1.7-.9 1 0 1.9.6 2 1.6v.1c-.3-.2-1.1-.5-1.9-.5-1.8 0-3.6 1-3.6 2.8 0 1.7 1.5 2.8 3.1 2.8 1.3 0 1.9-.6 2.4-1.2h.1v1h1.8v-4.8c-.2-2.2-1.9-3.5-4-3.5zm-.2 6.9c-.6 0-1.5-.3-1.5-1.1 0-1 1.1-1.3 2-1.3.8 0 1.2.2 1.7.4-.2 1.2-1.2 2-2.2 2zm10.5-6.6l-2.1 5.4h-.1l-2.2-5.4h-2l3.3 7.6-1.9 4.2h1.9l5.1-11.8h-2zm-16.8 8h1.9V17.5h-1.9V30z"></path>
|
|
196
|
+
<g>
|
|
197
|
+
<linearGradient id="g1" gradientUnits="userSpaceOnUse" x1="21.8" y1="33.29" x2="5.017" y2="16.508" gradientTransform="matrix(1 0 0 -1 0 42)">
|
|
198
|
+
<stop offset="0" stop-color="#00a0ff"></stop>
|
|
199
|
+
<stop offset=".007" stop-color="#00a1ff"></stop>
|
|
200
|
+
<stop offset=".26" stop-color="#00beff"></stop>
|
|
201
|
+
<stop offset=".512" stop-color="#00d2ff"></stop>
|
|
202
|
+
<stop offset=".76" stop-color="#00dfff"></stop>
|
|
203
|
+
<stop offset="1" stop-color="#00e3ff"></stop>
|
|
204
|
+
</linearGradient>
|
|
205
|
+
<linearGradient id="g2" gradientUnits="userSpaceOnUse" x1="33.834" y1="21.999" x2="9.637" y2="21.999" gradientTransform="matrix(1 0 0 -1 0 42)">
|
|
206
|
+
<stop offset="0" stop-color="#ffe000"></stop>
|
|
207
|
+
<stop offset=".409" stop-color="#ffbd00"></stop>
|
|
208
|
+
<stop offset=".775" stop-color="orange"></stop>
|
|
209
|
+
<stop offset="1" stop-color="#ff9c00"></stop>
|
|
210
|
+
</linearGradient>
|
|
211
|
+
<linearGradient id="g3" gradientUnits="userSpaceOnUse" x1="24.827" y1="19.704" x2="2.069" y2="-3.054" gradientTransform="matrix(1 0 0 -1 0 42)">
|
|
212
|
+
<stop offset="0" stop-color="#ff3a44"></stop>
|
|
213
|
+
<stop offset="1" stop-color="#c31162"></stop>
|
|
214
|
+
</linearGradient>
|
|
215
|
+
<linearGradient id="g4" gradientUnits="userSpaceOnUse" x1="7.297" y1="41.824" x2="17.46" y2="31.661" gradientTransform="matrix(1 0 0 -1 0 42)">
|
|
216
|
+
<stop offset="0" stop-color="#32a071"></stop>
|
|
217
|
+
<stop offset=".069" stop-color="#2da771"></stop>
|
|
218
|
+
<stop offset=".476" stop-color="#15cf74"></stop>
|
|
219
|
+
<stop offset=".801" stop-color="#06e775"></stop>
|
|
220
|
+
<stop offset="1" stop-color="#00f076"></stop>
|
|
221
|
+
</linearGradient>
|
|
222
|
+
<path fill="url(#g1)" d="M10.4 7.5c-.3.3-.4.8-.4 1.4V31c0 .6.2 1.1.5 1.4l.1.1L23 20.1v-.2L10.4 7.5z"></path>
|
|
223
|
+
<path fill="url(#g2)" d="M27 24.3l-4.1-4.1V19.9l4.1-4.1.1.1 4.9 2.8c1.4.8 1.4 2.1 0 2.9l-5 2.7z"></path>
|
|
224
|
+
<path fill="url(#g3)" d="M27.1 24.2L22.9 20 10.4 32.5c.5.5 1.2.5 2.1.1l14.6-8.4"></path>
|
|
225
|
+
<path fill="url(#g4)" d="M27.1 15.8L12.5 7.5c-.9-.5-1.6-.4-2.1.1L22.9 20l4.2-4.2z"></path>
|
|
226
|
+
<path opacity="0.2" d="M27 24.1l-14.5 8.2c-.8.5-1.5.4-2 0l-.1.1.1.1c.5.4 1.2.5 2 0L27 24.1z"></path>
|
|
227
|
+
<path opacity="0.12" d="M10.4 32.3c-.3-.3-.4-.8-.4-1.4v.1c0 .6.2 1.1.5 1.4v-.1h-.1zM32 21.3l-5 2.8.1.1 4.9-2.8c.7-.4 1-.9 1-1.4 0 .5-.4.9-1 1.3z"></path>
|
|
228
|
+
<path opacity="0.25" fill="#fff" d="M12.5 7.6L32 18.7c.6.4 1 .8 1 1.3 0-.5-.3-1-1-1.4L12.5 7.5c-1.4-.8-2.5-.2-2.5 1.4V9c0-1.5 1.1-2.2 2.5-1.4z"></path>
|
|
229
|
+
</g>
|
|
230
|
+
</svg>
|
|
231
|
+
</a>
|
|
232
|
+
<a style="margin-top:2rem" href="https://apps.apple.com/us/app/only-bible-app/id6467606465?itsct=apps_box_badge&itscg=30200" rel="noopener noreferrer" target="_blank">
|
|
233
|
+
<svg viewBox="0 0 539.856 170" width="196" xmlns="http://www.w3.org/2000/svg" fill="#000000">
|
|
234
|
+
<g transform="scale(4.00216 4.0011)">
|
|
235
|
+
<path fill="#FFF" d="M134.032 35.268a3.83 3.83 0 0 1-3.834 3.83H4.729a3.835 3.835 0 0 1-3.839-3.83V4.725A3.84 3.84 0 0 1 4.729.89h125.468a3.834 3.834 0 0 1 3.834 3.835l.001 30.543z"></path>
|
|
236
|
+
<path fill="#A6A6A6" d="M130.198 39.989H4.729A4.73 4.73 0 0 1 0 35.268V4.726A4.733 4.733 0 0 1 4.729 0h125.468a4.735 4.735 0 0 1 4.729 4.726v30.542c.002 2.604-2.123 4.721-4.728 4.721z"></path>
|
|
237
|
+
<path d="M134.032 35.268a3.83 3.83 0 0 1-3.834 3.83H4.729a3.835 3.835 0 0 1-3.839-3.83V4.725A3.84 3.84 0 0 1 4.729.89h125.468a3.834 3.834 0 0 1 3.834 3.835l.001 30.543z"></path>
|
|
238
|
+
<path fill="#FFF" d="M30.128 19.784c-.029-3.223 2.639-4.791 2.761-4.864-1.511-2.203-3.853-2.504-4.676-2.528-1.967-.207-3.875 1.177-4.877 1.177-1.022 0-2.565-1.157-4.228-1.123-2.14.033-4.142 1.272-5.24 3.196-2.266 3.923-.576 9.688 1.595 12.859 1.086 1.554 2.355 3.287 4.016 3.226 1.625-.066 2.232-1.035 4.193-1.035 1.943 0 2.513 1.035 4.207.996 1.744-.027 2.842-1.56 3.89-3.127 1.255-1.779 1.759-3.533 1.779-3.623-.04-.014-3.386-1.292-3.42-5.154zM26.928 10.306c.874-1.093 1.472-2.58 1.306-4.089-1.265.056-2.847.875-3.758 1.944-.806.942-1.526 2.486-1.34 3.938 1.421.106 2.88-.717 3.792-1.793z"></path>
|
|
239
|
+
<g fill="#FFF">
|
|
240
|
+
<path d="M53.665 31.504h-2.271l-1.244-3.909h-4.324l-1.185 3.909H42.43l4.285-13.308h2.646l4.304 13.308zm-3.89-5.549L48.65 22.48c-.119-.355-.343-1.191-.671-2.507h-.04c-.132.566-.343 1.402-.632 2.507l-1.106 3.475h3.574zM64.663 26.588c0 1.632-.443 2.922-1.33 3.869-.794.843-1.781 1.264-2.958 1.264-1.271 0-2.185-.453-2.74-1.361v5.035h-2.132V25.062c0-1.025-.027-2.076-.079-3.154h1.875l.119 1.521h.04c.711-1.146 1.79-1.719 3.238-1.719 1.132 0 2.077.447 2.833 1.342.755.897 1.134 2.075 1.134 3.536zm-2.172.078c0-.934-.21-1.704-.632-2.311-.461-.631-1.08-.947-1.856-.947-.526 0-1.004.176-1.431.523-.428.35-.708.807-.839 1.373a2.784 2.784 0 0 0-.099.649v1.601c0 .697.214 1.286.642 1.768.428.48.984.721 1.668.721.803 0 1.428-.311 1.875-.928.448-.619.672-1.435.672-2.449zM75.7 26.588c0 1.632-.443 2.922-1.33 3.869-.795.843-1.781 1.264-2.959 1.264-1.271 0-2.185-.453-2.74-1.361v5.035h-2.132V25.062c0-1.025-.027-2.076-.079-3.154h1.875l.119 1.521h.04c.71-1.146 1.789-1.719 3.238-1.719 1.131 0 2.076.447 2.834 1.342.754.897 1.134 2.075 1.134 3.536zm-2.173.078c0-.934-.211-1.704-.633-2.311-.461-.631-1.078-.947-1.854-.947-.526 0-1.004.176-1.433.523-.428.35-.707.807-.838 1.373-.065.264-.1.479-.1.649v1.601c0 .697.215 1.286.641 1.768.428.479.984.721 1.67.721.804 0 1.429-.311 1.875-.928.448-.619.672-1.435.672-2.449zM88.04 27.771c0 1.133-.396 2.054-1.183 2.765-.866.776-2.075 1.165-3.625 1.165-1.432 0-2.58-.276-3.446-.829l.493-1.777c.935.554 1.962.83 3.08.83.804 0 1.429-.182 1.875-.543.447-.362.673-.846.673-1.45 0-.541-.187-.994-.554-1.363-.369-.368-.979-.711-1.836-1.026-2.33-.869-3.496-2.14-3.496-3.812 0-1.092.412-1.986 1.234-2.685.822-.698 1.912-1.047 3.268-1.047 1.211 0 2.22.211 3.021.632l-.535 1.738c-.754-.408-1.605-.612-2.557-.612-.752 0-1.342.185-1.764.553-.355.329-.535.73-.535 1.206 0 .525.205.961.613 1.303.354.315 1 .658 1.934 1.026 1.146.462 1.988 1 2.527 1.618.543.618.813 1.389.813 2.308zM95.107 23.508h-2.35v4.659c0 1.185.414 1.776 1.244 1.776.381 0 .697-.032.947-.099l.059-1.648c-.237.039-.414.059-.533.059-.415 0-.621-.265-.621-.795v-3.952h2.254v-1.621-.001zM97.567 28.265c0 .631.239.936.717.936.336 0 .609-.095.819-.283v-1.484c-.336-.158-.714-.237-1.137-.237-.289 0-.399.087-.399.261v.807zm3.505 2.343c.798 0 1.299-.362 1.7-1.144l.313.955h.876v-3.72c0-1.352-.941-2.202-2.437-2.202-1.1 0-2.044.436-2.609 1.29l.777.594c.38-.48.886-.754 1.587-.754.855 0 1.359.449 1.359 1.241v.053l-1.714.099c-1.367.079-2.201.781-2.201 1.814 0 1.104.876 1.774 1.949 1.774zm.226-1.4c-.521 0-.877-.314-.877-.754 0-.429.326-.683.991-.723l1.27-.069v.42c0 .643-.577 1.126-1.384 1.126z"></path>
|
|
241
|
+
</g>
|
|
242
|
+
</g>
|
|
243
|
+
</svg>
|
|
244
|
+
</a>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>`
|
|
249
|
+
}))
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
app.get('/only-bible-app/privacy-policy', (c) => {
|
|
253
|
+
return c.html(Layout({
|
|
254
|
+
css: '/assets/css/workers/only-bible-app-privacy.css',
|
|
255
|
+
title: 'Privacy Policy | Only Bible App',
|
|
256
|
+
description: 'Privacy Policy | Only Bible App',
|
|
257
|
+
currentPath: c.req.path,
|
|
258
|
+
children: html`<h1 style="font-size:1.875rem;line-height:2.25rem;font-weight:700">Privacy Policy</h1>
|
|
259
|
+
<p>
|
|
260
|
+
Only Bible App does not collect any information about you or your usage of the app. It
|
|
261
|
+
does not send any information about you to anyone. It does not contain ads. It will never
|
|
262
|
+
install additional products on your device or change your device's configuration. No ads,
|
|
263
|
+
No in-app purchases, No distractions.
|
|
264
|
+
</p>
|
|
265
|
+
<h4 style="font-weight:600;margin-top:0.5rem">Changes to this Privacy Policy</h4>
|
|
266
|
+
I may update our Privacy Policy from time to time. Thus, you are advised to review this page
|
|
267
|
+
periodically for any changes. I will notify you of any changes by posting the new Privacy
|
|
268
|
+
Policy on this page.
|
|
269
|
+
<p>
|
|
270
|
+
This policy is effective as of <strong style="font-weight:600">2023-09-08</strong>
|
|
271
|
+
</p>
|
|
272
|
+
<h4 style="font-weight:600;margin-top:0.5rem">Contact Us</h4>
|
|
273
|
+
If you have any questions or suggestions for us you can contact us at
|
|
274
|
+
<strong style="font-weight:600">[email protected]</strong>`
|
|
275
|
+
}))
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
app.get('/only-bible-app/terms-and-conditions', (c) => {
|
|
279
|
+
return c.html(Layout({
|
|
280
|
+
css: '/assets/css/workers/only-bible-app-terms.css',
|
|
281
|
+
title: 'Terms and Conditions | Only Bible App',
|
|
282
|
+
description: 'Terms and Conditions | Only Bible App',
|
|
283
|
+
currentPath: c.req.path,
|
|
284
|
+
children: html`<div style="padding-bottom:2.5rem;margin-top:2.5rem">
|
|
285
|
+
<h1 style="font-weight:700;font-size:1.875rem;line-height:2.25rem">Terms and Conditions</h1>
|
|
286
|
+
<p>
|
|
287
|
+
By downloading or using the app, these terms will automatically apply to you — you
|
|
288
|
+
should make sure therefore that you read them carefully before using the app.
|
|
289
|
+
You’re not allowed to copy or modify the app, any part of the app, or our
|
|
290
|
+
trademarks in any way. You’re not allowed to attempt to extract the source code of
|
|
291
|
+
the app, and you also shouldn’t try to translate the app into other languages or
|
|
292
|
+
make derivative versions. The app itself, and all the trademarks, copyright, database
|
|
293
|
+
rights, and other intellectual property rights related to it, still belong to us.
|
|
294
|
+
</p>
|
|
295
|
+
<p>
|
|
296
|
+
We are committed to ensuring that the app is as useful and efficient as possible. For
|
|
297
|
+
that reason, we reserve the right to make changes to the app or to charge for its
|
|
298
|
+
services, at any time and for any reason. We will never charge you for the app or its
|
|
299
|
+
services without making it very clear to you exactly what you’re paying for.
|
|
300
|
+
</p>
|
|
301
|
+
<p>
|
|
302
|
+
The Only Bible App app stores and processes personal data that you have provided to us,
|
|
303
|
+
to provide my Service. It’s your responsibility to keep your phone and access to
|
|
304
|
+
the app secure. We therefore recommend that you do not jailbreak or root your phone,
|
|
305
|
+
which is the process of removing software restrictions and limitations imposed by the
|
|
306
|
+
official operating system of your device. It could make your phone vulnerable to
|
|
307
|
+
malware/viruses/malicious programs, compromise your phone’s security features and
|
|
308
|
+
it could mean that the Only Bible App app won’t work properly or at all.
|
|
309
|
+
</p>
|
|
310
|
+
<h4 style="font-weight:600;margin-top:0.5rem;font-size:1.17em">Changes to this Terms and conditions</h4>
|
|
311
|
+
I may update our Terms and conditions from time to time. Thus, you are advised to review
|
|
312
|
+
this page periodically for any changes. I will notify you of any changes by posting the
|
|
313
|
+
new Terms and conditions on this page.
|
|
314
|
+
<p>
|
|
315
|
+
This policy is effective as of <strong style="font-weight:bold">2023-09-08</strong>
|
|
316
|
+
</p>
|
|
317
|
+
<h4 style="font-weight:600;margin-top:0.5rem;font-size:1.17em">Contact Us</h4>
|
|
318
|
+
If you have any questions or suggestions for us you can contact us at
|
|
319
|
+
<strong style="font-weight:bold">[email protected]</strong>
|
|
320
|
+
</div>`
|
|
321
|
+
}))
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// ─── 2. Blog ────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
app.get('/posts', async (c) => {
|
|
327
|
+
const s3 = getS3()
|
|
328
|
+
const posts = await getPosts(s3)
|
|
329
|
+
return c.html(Layout({
|
|
330
|
+
css: '/assets/css/workers/posts-index.css',
|
|
331
|
+
title: 'Posts',
|
|
332
|
+
description: 'Posts',
|
|
333
|
+
currentPath: c.req.path,
|
|
334
|
+
children: html`<h1 style="font-weight:800;font-size:2rem">Posts</h1>
|
|
335
|
+
<ul style="display:flex;flex-direction:column;list-style:none;padding:0">
|
|
336
|
+
${posts.map((post) => html`<li style="display:grid;grid-template-columns:1fr;gap:0.4rem;margin-top:1.25rem;line-height:1.5rem">
|
|
337
|
+
<div>
|
|
338
|
+
<a href="/posts/${post.id}" style="font-size:1.1rem;color:var(--color-post-link);text-underline-offset:4px">
|
|
339
|
+
${post.data.title}
|
|
340
|
+
</a>
|
|
341
|
+
<span style="font-size:13pt;display:block">${post.data.description}</span>
|
|
342
|
+
</div>
|
|
343
|
+
<time style="font-size:1.1rem">${post.data.pubDate.toLocaleDateString()}</time>
|
|
344
|
+
</li>`)}
|
|
345
|
+
</ul>`
|
|
346
|
+
}))
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
app.get('/posts/:id', async (c) => {
|
|
350
|
+
const s3 = getS3()
|
|
351
|
+
const postId = c.req.param('id')
|
|
352
|
+
const post = await getPost(s3, postId)
|
|
353
|
+
if (!post) {
|
|
354
|
+
return c.redirect('/404', 302)
|
|
355
|
+
}
|
|
356
|
+
return c.html(Layout({
|
|
357
|
+
css: '/assets/css/workers/posts-detail.css',
|
|
358
|
+
title: post.data.title,
|
|
359
|
+
description: post.data.description,
|
|
360
|
+
currentPath: c.req.path,
|
|
361
|
+
children: html`<article>
|
|
362
|
+
<h1 style="text-align:left;font-size:1.8rem;font-weight:bold;line-height:1;margin-top:0.5rem;margin-bottom:0.1rem">
|
|
363
|
+
${post.data.title}
|
|
364
|
+
</h1>
|
|
365
|
+
<h2 style="font-size:1.1rem;font-weight:500">${post.data.description}</h2>
|
|
366
|
+
<time style="font-size:1.1rem;color:light-dark(hsl(from var(--color-text) h s 40%), hsl(from var(--color-text) h s 80%))">
|
|
367
|
+
${post.data.pubDate.toLocaleDateString()}
|
|
368
|
+
</time>
|
|
369
|
+
<hr style="margin:0.5rem 0;border-color:var(--color-text)" />
|
|
370
|
+
${raw(post.html)}
|
|
371
|
+
</article>`
|
|
372
|
+
}))
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// ─── 3. RSS ─────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
app.get('/rss.xml', async (c) => {
|
|
378
|
+
const s3 = getS3()
|
|
379
|
+
const posts = await getPosts(s3)
|
|
380
|
+
const escapeXml = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
381
|
+
const items = posts.map((post) => `
|
|
382
|
+
<item>
|
|
383
|
+
<title>${escapeXml(post.data.title)}</title>
|
|
384
|
+
<link>${SITE_URL}/posts/${post.id}</link>
|
|
385
|
+
<guid isPermaLink="true">${SITE_URL}/posts/${post.id}</guid>
|
|
386
|
+
<pubDate>${post.data.pubDate.toUTCString()}</pubDate>
|
|
387
|
+
<description>${escapeXml(post.data.description)}</description>
|
|
388
|
+
</item>`).join('')
|
|
389
|
+
const rss = `<?xml version="1.0" encoding="UTF-8"?>
|
|
390
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
391
|
+
<channel>
|
|
392
|
+
<title>${escapeXml(SITE_TITLE)}</title>
|
|
393
|
+
<link>${SITE_URL}</link>
|
|
394
|
+
<description>${escapeXml(SITE_DESCRIPTION)}</description>
|
|
395
|
+
<atom:link href="${SITE_URL}/rss.xml" rel="self" type="application/rss+xml"/>
|
|
396
|
+
${items}
|
|
397
|
+
</channel>
|
|
398
|
+
</rss>`
|
|
399
|
+
return c.body(rss, 200, { 'Content-Type': 'application/rss+xml' })
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// ─── 4. Repo routes ─────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
app.get('/repos/:id', async (c) => {
|
|
405
|
+
const repoId = c.req.param('id')
|
|
406
|
+
const repo = getRepo(repoId)
|
|
407
|
+
if (!repo) return c.redirect('/404', 302)
|
|
408
|
+
|
|
409
|
+
const s3 = getS3()
|
|
410
|
+
const readmeRaw = await getRepoReadme(s3, repo.id)
|
|
411
|
+
const readmeHtml = readmeRaw ? await renderMarkdown(readmeRaw) : ''
|
|
412
|
+
|
|
413
|
+
return c.html(Layout({
|
|
414
|
+
title: `${repo.data.title}`,
|
|
415
|
+
description: repo.data.description,
|
|
416
|
+
css: '/assets/css/workers/readme.css',
|
|
417
|
+
currentPath: c.req.path,
|
|
418
|
+
children: RepoLayout({
|
|
419
|
+
repo,
|
|
420
|
+
currentTab: 'Readme',
|
|
421
|
+
children: html`<article>
|
|
422
|
+
${readmeHtml ? raw(readmeHtml) : html`<p>No README found.</p>`}
|
|
423
|
+
</article>`
|
|
424
|
+
})
|
|
425
|
+
}))
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
app.get('/repos/:id/commits', async (c) => {
|
|
429
|
+
const repoId = c.req.param('id')
|
|
430
|
+
const repo = getRepo(repoId)
|
|
431
|
+
if (!repo) return c.redirect('/404', 302)
|
|
432
|
+
|
|
433
|
+
const s3 = getS3()
|
|
434
|
+
const commits = await getCommits(s3, repo.id)
|
|
435
|
+
|
|
436
|
+
return c.html(Layout({
|
|
437
|
+
title: `Commits | ${repo.data.title}`,
|
|
438
|
+
css: '/assets/css/workers/commits-index.css',
|
|
439
|
+
currentPath: c.req.path,
|
|
440
|
+
children: RepoLayout({
|
|
441
|
+
repo,
|
|
442
|
+
currentTab: 'Commits',
|
|
443
|
+
children: html`${commits.map((commit) => html`<div class="event">
|
|
444
|
+
<div>
|
|
445
|
+
<a href="/repos/${repo.id}/commits/${commit.hash}">${commit.hash.substring(0, 7)}</a>
|
|
446
|
+
—<strong>${commit.author_name}</strong>
|
|
447
|
+
<small class="pull-right">
|
|
448
|
+
<span>${commit.date}</span>
|
|
449
|
+
</small>
|
|
450
|
+
</div>
|
|
451
|
+
<pre class="commit">${commit.message}</pre>
|
|
452
|
+
</div>`)}`
|
|
453
|
+
})
|
|
454
|
+
}))
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
app.get('/repos/:id/commits/:hash', async (c) => {
|
|
458
|
+
const repoId = c.req.param('id')
|
|
459
|
+
const hash = c.req.param('hash')
|
|
460
|
+
const repo = getRepo(repoId)
|
|
461
|
+
if (!repo) return c.redirect('/404', 302)
|
|
462
|
+
if (!hash) return c.redirect(`/repos/${repo.id}/commits`, 302)
|
|
463
|
+
|
|
464
|
+
const s3 = getS3()
|
|
465
|
+
const commits = await getCommits(s3, repo.id)
|
|
466
|
+
const commit = commits.find((c2) => c2.hash === hash)
|
|
467
|
+
if (!commit) return c.redirect(`/repos/${repo.id}/commits`, 302)
|
|
468
|
+
|
|
469
|
+
const diffHtml = await generateHTMLDiff(s3, repo.id, commit.hash)
|
|
470
|
+
|
|
471
|
+
return c.html(Layout({
|
|
472
|
+
title: `${commit.hash.substring(0, 7)} | ${repo.data.title}`,
|
|
473
|
+
css: '/assets/css/workers/commits-detail.css',
|
|
474
|
+
currentPath: c.req.path,
|
|
475
|
+
children: RepoLayout({
|
|
476
|
+
repo,
|
|
477
|
+
currentTab: 'Commits',
|
|
478
|
+
children: html`<div class="event">
|
|
479
|
+
<div>
|
|
480
|
+
<a href="/repos/${repo.id}/commits/${commit.hash}">${commit.hash.substring(0, 7)}</a>—
|
|
481
|
+
<strong>${commit.author_name}</strong>
|
|
482
|
+
<small class="pull-right">
|
|
483
|
+
<span>${commit.date}</span>
|
|
484
|
+
</small>
|
|
485
|
+
</div>
|
|
486
|
+
<pre class="commit">${commit.message}</pre>
|
|
487
|
+
</div>
|
|
488
|
+
${raw(diffHtml)}`
|
|
489
|
+
})
|
|
490
|
+
}))
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
app.get('/repos/:id/files', async (c) => {
|
|
494
|
+
const repoId = c.req.param('id')
|
|
495
|
+
const repo = getRepo(repoId)
|
|
496
|
+
if (!repo) return c.redirect('/404', 302)
|
|
497
|
+
|
|
498
|
+
const s3 = getS3()
|
|
499
|
+
const files = await getFiles(s3, repo.id)
|
|
500
|
+
const sortedFiles = sortChildren(buildFileTree(files))
|
|
501
|
+
|
|
502
|
+
return c.html(Layout({
|
|
503
|
+
title: `Files | ${repo.data.title}`,
|
|
504
|
+
css: '/assets/css/workers/files-index.css',
|
|
505
|
+
currentPath: c.req.path,
|
|
506
|
+
children: RepoLayout({
|
|
507
|
+
repo,
|
|
508
|
+
currentTab: 'Files',
|
|
509
|
+
children: FileTree({ nodes: sortedFiles, repoId: repo.id })
|
|
510
|
+
})
|
|
511
|
+
}))
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const BINARY_EXTS = ['apk', 'dex', 'ap_', 'jar', 'fnt']
|
|
515
|
+
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'icns', 'curve', 'atlas']
|
|
516
|
+
const LARGE_SIZE = 1024 * 512
|
|
517
|
+
|
|
518
|
+
const toBase64 = (data: Uint8Array) => btoa(String.fromCharCode(...data))
|
|
519
|
+
|
|
520
|
+
app.get('/repos/:id/files/:path{.+}/history', async (c) => {
|
|
521
|
+
const repoId = c.req.param('id')
|
|
522
|
+
const filePath = c.req.param('path')
|
|
523
|
+
const repo = getRepo(repoId)
|
|
524
|
+
if (!repo) return c.redirect('/404', 302)
|
|
525
|
+
|
|
526
|
+
const s3 = getS3()
|
|
527
|
+
const files = await getFiles(s3, repo.id)
|
|
528
|
+
const file = files.find((f) => f.absolutePath === filePath)
|
|
529
|
+
if (!file) return c.redirect(`/repos/${repo.id}/files`, 302)
|
|
530
|
+
|
|
531
|
+
const history = await getFileHistory(s3, repo.id, file.absolutePath)
|
|
532
|
+
|
|
533
|
+
return c.html(Layout({
|
|
534
|
+
title: `${file.name} history | ${repo.data.title}`,
|
|
535
|
+
currentPath: c.req.path,
|
|
536
|
+
children: RepoLayout({
|
|
537
|
+
repo,
|
|
538
|
+
currentTab: 'Files',
|
|
539
|
+
children: FileLayout({
|
|
540
|
+
repo,
|
|
541
|
+
file,
|
|
542
|
+
pathname: c.req.path,
|
|
543
|
+
children: html`${history.map((commit) => CommitEntry({ repoId: repo.id, commit }))}`
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
}))
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
app.get('/repos/:id/files/:path{.+}/blame', async (c) => {
|
|
550
|
+
const repoId = c.req.param('id')
|
|
551
|
+
const filePath = c.req.param('path')
|
|
552
|
+
const repo = getRepo(repoId)
|
|
553
|
+
if (!repo) return c.redirect('/404', 302)
|
|
554
|
+
|
|
555
|
+
const s3 = getS3()
|
|
556
|
+
const files = await getFiles(s3, repo.id)
|
|
557
|
+
const file = files.find((f) => f.absolutePath === filePath)
|
|
558
|
+
if (!file) return c.redirect(`/repos/${repo.id}/files`, 302)
|
|
559
|
+
|
|
560
|
+
return c.html(Layout({
|
|
561
|
+
title: `${file.name} blame | ${repo.data.title}`,
|
|
562
|
+
currentPath: c.req.path,
|
|
563
|
+
children: RepoLayout({
|
|
564
|
+
repo,
|
|
565
|
+
currentTab: 'Files',
|
|
566
|
+
children: FileLayout({
|
|
567
|
+
repo,
|
|
568
|
+
file,
|
|
569
|
+
pathname: c.req.path,
|
|
570
|
+
children: html`<p>WIP: Work in Progress...</p>`
|
|
571
|
+
})
|
|
572
|
+
})
|
|
573
|
+
}))
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
app.get('/repos/:id/files/:path{.+}', async (c) => {
|
|
577
|
+
const repoId = c.req.param('id')
|
|
578
|
+
const filePath = c.req.param('path')
|
|
579
|
+
const repo = getRepo(repoId)
|
|
580
|
+
if (!repo) return c.redirect('/404', 302)
|
|
581
|
+
|
|
582
|
+
const s3 = getS3()
|
|
583
|
+
const files = await getFiles(s3, repo.id)
|
|
584
|
+
const file = files.find((f) => f.absolutePath === filePath)
|
|
585
|
+
if (!file) return c.redirect(`/repos/${repo.id}/files`, 302)
|
|
586
|
+
|
|
587
|
+
const contentBuffer = await getFileContentData(s3, repo.id, file.absolutePath)
|
|
588
|
+
const isBinary = BINARY_EXTS.includes(file.ext)
|
|
589
|
+
const isLarge = file.size > LARGE_SIZE
|
|
590
|
+
const isImage = IMAGE_EXTS.includes(file.ext)
|
|
591
|
+
|
|
592
|
+
let content: any
|
|
593
|
+
if (isBinary || isLarge) {
|
|
594
|
+
content = html`<p>The file is too large to be displayed (${file.size} bytes).</p>`
|
|
595
|
+
} else if (isImage && contentBuffer) {
|
|
596
|
+
content = html`<img src="data:image/${file.ext};base64,${toBase64(contentBuffer)}" alt="${file.path}" />`
|
|
597
|
+
} else if (contentBuffer) {
|
|
598
|
+
const text = new TextDecoder('utf-8').decode(contentBuffer).trim() || 'No content to display.'
|
|
599
|
+
content = html`<pre><code class="language-${file.ext}">${text}</code></pre>`
|
|
600
|
+
} else {
|
|
601
|
+
content = html`<p>Unable to load file content.</p>`
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return c.html(Layout({
|
|
605
|
+
title: `${file.name} | ${repo.data.title}`,
|
|
606
|
+
currentPath: c.req.path,
|
|
607
|
+
children: RepoLayout({
|
|
608
|
+
repo,
|
|
609
|
+
currentTab: 'Files',
|
|
610
|
+
children: FileLayout({
|
|
611
|
+
repo,
|
|
612
|
+
file,
|
|
613
|
+
pathname: c.req.path,
|
|
614
|
+
children: content
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
}))
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
// ─── 5. Issue routes ────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
app.get('/repos/:id/issues', async (c) => {
|
|
623
|
+
const repoId = c.req.param('id')
|
|
624
|
+
const repo = getRepo(repoId)
|
|
625
|
+
if (!repo) return c.redirect('/404', 302)
|
|
626
|
+
|
|
627
|
+
const s3 = getS3()
|
|
628
|
+
const { issues, isAvailable } = await getGitBugIssues(s3, repo.id)
|
|
629
|
+
const openIssues = issues.filter((issue) => issue.state === 'open')
|
|
630
|
+
const closedIssues = issues.filter((issue) => issue.state === 'closed')
|
|
631
|
+
|
|
632
|
+
return c.html(Layout({
|
|
633
|
+
title: `Issues | ${repo.data.title}`,
|
|
634
|
+
css: '/assets/css/workers/issues-index.css',
|
|
635
|
+
currentPath: c.req.path,
|
|
636
|
+
children: RepoLayout({
|
|
637
|
+
repo,
|
|
638
|
+
currentTab: 'Issues',
|
|
639
|
+
children: html`<section class="issues">
|
|
640
|
+
<div class="summary">
|
|
641
|
+
<strong>${issues.length}</strong> issues
|
|
642
|
+
<span>${openIssues.length} open</span>
|
|
643
|
+
<span>${closedIssues.length} closed</span>
|
|
644
|
+
</div>
|
|
645
|
+
<form class="new-issue" hx-post="/repos/${repo.id}/issues" hx-target="#issues-list" hx-swap="innerHTML">
|
|
646
|
+
<input name="repoId" type="hidden" value="${repo.id}" />
|
|
647
|
+
<input name="title" placeholder="Title" required="true" />
|
|
648
|
+
<textarea name="body" placeholder="Description"></textarea>
|
|
649
|
+
<div class="form-row">
|
|
650
|
+
<input name="author" placeholder="Author" />
|
|
651
|
+
<input name="labels" placeholder="labels, comma-separated" />
|
|
652
|
+
<button type="submit">Create</button>
|
|
653
|
+
</div>
|
|
654
|
+
</form>
|
|
655
|
+
<div id="issues-list">
|
|
656
|
+
${issues.length > 0
|
|
657
|
+
? issues.map((issue) => IssueCard({ repoId: repo.id, issue }))
|
|
658
|
+
: html`<p class="empty">
|
|
659
|
+
${isAvailable
|
|
660
|
+
? 'No git-bug issues found in this repository.'
|
|
661
|
+
: 'No git-bug issues could be loaded from R2.'}
|
|
662
|
+
</p>`}
|
|
663
|
+
</div>
|
|
664
|
+
</section>`
|
|
665
|
+
})
|
|
666
|
+
}))
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
app.post('/repos/:id/issues', async (c) => {
|
|
670
|
+
const repoId = c.req.param('id')
|
|
671
|
+
const repo = getRepo(repoId)
|
|
672
|
+
if (!repo) return c.redirect('/404', 302)
|
|
673
|
+
|
|
674
|
+
if (!c.req.header('HX-Request')) {
|
|
675
|
+
return c.redirect(c.req.url, 303)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const s3 = getS3()
|
|
679
|
+
const formData = await c.req.formData()
|
|
680
|
+
const title = formData.get('title') as string
|
|
681
|
+
const body = formData.get('body') as string
|
|
682
|
+
const author = formData.get('author') as string
|
|
683
|
+
const labelsRaw = formData.get('labels') as string
|
|
684
|
+
const labels = labelsRaw
|
|
685
|
+
? labelsRaw.split(',').map((l) => l.trim()).filter(Boolean)
|
|
686
|
+
: undefined
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
await createGitBugIssue(s3, repo.id, { title, body, author, labels })
|
|
690
|
+
const { issues } = await getGitBugIssues(s3, repo.id)
|
|
691
|
+
const htmlContent = issues.map((issue) => IssueCard({ repoId: repo.id, issue })).join('')
|
|
692
|
+
return c.html(htmlContent)
|
|
693
|
+
} catch (err) {
|
|
694
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
695
|
+
return c.html(`<div class="error" style="color:red;padding:0.5rem">${message}</div>`)
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
app.get('/repos/:id/issues/:issueId', async (c) => {
|
|
700
|
+
const repoId = c.req.param('id')
|
|
701
|
+
const issueId = c.req.param('issueId')
|
|
702
|
+
const repo = getRepo(repoId)
|
|
703
|
+
if (!repo) return c.redirect('/404', 302)
|
|
704
|
+
|
|
705
|
+
if (!/^[a-f0-9]{16}$/.test(issueId)) {
|
|
706
|
+
return c.redirect(`/repos/${repo.id}/issues`, 302)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const s3 = getS3()
|
|
710
|
+
const issue = await getGitBugIssue(s3, repo.id, issueId)
|
|
711
|
+
if (!issue) return c.redirect(`/repos/${repo.id}/issues`, 302)
|
|
712
|
+
|
|
713
|
+
return c.html(Layout({
|
|
714
|
+
title: `${issue.title} | ${repo.data.title}`,
|
|
715
|
+
css: '/assets/css/workers/issues-detail.css',
|
|
716
|
+
currentPath: c.req.path,
|
|
717
|
+
children: RepoLayout({
|
|
718
|
+
repo,
|
|
719
|
+
currentTab: 'Issues',
|
|
720
|
+
children: html`${StateBadge(issue)}
|
|
721
|
+
${IssueHeaderSection(repo.id, issue)}
|
|
722
|
+
${StateFormSection(repo.id, issue)}
|
|
723
|
+
${issue.body ? html`<section class="body"><pre>${issue.body}</pre></section>` : ''}
|
|
724
|
+
${CommentsSection(repo.id, issue)}`
|
|
725
|
+
})
|
|
726
|
+
}))
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
app.post('/repos/:id/issues/:issueId', async (c) => {
|
|
730
|
+
const repoId = c.req.param('id')
|
|
731
|
+
const issueId = c.req.param('issueId')
|
|
732
|
+
const repo = getRepo(repoId)
|
|
733
|
+
if (!repo) return c.redirect('/404', 302)
|
|
734
|
+
|
|
735
|
+
if (!c.req.header('HX-Request')) {
|
|
736
|
+
return c.redirect(c.req.url, 303)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const s3 = getS3()
|
|
740
|
+
const formData = await c.req.formData()
|
|
741
|
+
const body = formData.get('body') as string
|
|
742
|
+
const author = formData.get('author') as string
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
const updated = await addGitBugIssueComment(s3, repo.id, issueId, { body, author })
|
|
746
|
+
return c.html(CommentsSection(repo.id, updated))
|
|
747
|
+
} catch (err) {
|
|
748
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
749
|
+
return c.html(`<div class="error" style="color:red;padding:0.5rem">${message}</div>`)
|
|
750
|
+
}
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
app.put('/repos/:id/issues/:issueId', async (c) => {
|
|
754
|
+
const repoId = c.req.param('id')
|
|
755
|
+
const issueId = c.req.param('issueId')
|
|
756
|
+
const repo = getRepo(repoId)
|
|
757
|
+
if (!repo) return c.redirect('/404', 302)
|
|
758
|
+
|
|
759
|
+
if (!c.req.header('HX-Request')) {
|
|
760
|
+
return c.redirect(c.req.url, 303)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const s3 = getS3()
|
|
764
|
+
const formData = await c.req.formData()
|
|
765
|
+
const state = formData.get('state') as 'open' | 'closed'
|
|
766
|
+
const author = formData.get('author') as string
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const updated = await setGitBugIssueState(s3, repo.id, issueId, state, author)
|
|
770
|
+
return c.html(StateBadge(updated))
|
|
771
|
+
} catch (err) {
|
|
772
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
773
|
+
return c.html(`<div class="error" style="color:red;padding:0.5rem">${message}</div>`)
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
// ─── Issue helper components ────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
const fmtDate = (date: string) => new Date(date).toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' })
|
|
780
|
+
|
|
781
|
+
const StateBadge = (issue: { state: string }) => html`<span id="state-badge" class="state ${issue.state}">${issue.state}</span>`
|
|
782
|
+
|
|
783
|
+
const IssueHeaderSection = (repoId: string, issue: any) => html`<article class="issue">
|
|
784
|
+
<div class="issue-title">
|
|
785
|
+
<a href="/repos/${repoId}/issues/${issue.id}" title="${issue.id}">${issue.title}</a>
|
|
786
|
+
${StateBadge(issue)}
|
|
787
|
+
</div>
|
|
788
|
+
<div class="meta">
|
|
789
|
+
<a href="/repos/${repoId}/issues/${issue.id}" title="${issue.id}">${issue.id.substring(0, 8)}</a>
|
|
790
|
+
${issue.author ? html`<span>by ${issue.author}</span>` : ''}
|
|
791
|
+
${issue.createdAt ? html`<span>${fmtDate(issue.createdAt)}</span>` : ''}
|
|
792
|
+
${issue.comments.length > 0 ? html`<span>${issue.comments.length} comments</span>` : ''}
|
|
793
|
+
</div>
|
|
794
|
+
${issue.labels.length > 0 ? html`<div class="labels">
|
|
795
|
+
${issue.labels.map((label: string) => html`<span class="label">#${label}</span>`)}
|
|
796
|
+
</div>` : ''}
|
|
797
|
+
</article>`
|
|
798
|
+
|
|
799
|
+
const StateFormSection = (repoId: string, issue: any) => html`<form class="state-form" hx-put="/repos/${repoId}/issues/${issue.id}" hx-target="#state-badge" hx-swap="outerHTML">
|
|
800
|
+
<input name="repoId" type="hidden" value="${repoId}" />
|
|
801
|
+
<input name="issueId" type="hidden" value="${issue.id}" />
|
|
802
|
+
<input name="state" type="hidden" value="${issue.state === 'open' ? 'closed' : 'open'}" />
|
|
803
|
+
<input name="author" type="hidden" value="${issue.author || ''}" />
|
|
804
|
+
<button type="submit">${issue.state === 'open' ? 'Close' : 'Reopen'}</button>
|
|
805
|
+
</form>`
|
|
806
|
+
|
|
807
|
+
const CommentsSection = (repoId: string, issue: any) => html`<section id="comments-section" class="comments">
|
|
808
|
+
${issue.comments.length > 0 ? html`
|
|
809
|
+
<h3>Comments</h3>
|
|
810
|
+
${issue.comments.map((comment: any) => html`<article class="comment">
|
|
811
|
+
<div class="comment-meta">
|
|
812
|
+
${comment.author ? html`<strong>${comment.author}</strong>` : ''}
|
|
813
|
+
${comment.createdAt ? html`<span>${fmtDate(comment.createdAt)}</span>` : ''}
|
|
814
|
+
</div>
|
|
815
|
+
<pre>${comment.body}</pre>
|
|
816
|
+
</article>`)}
|
|
817
|
+
` : ''}
|
|
818
|
+
<form class="comment-form" hx-post="/repos/${repoId}/issues/${issue.id}" hx-target="#comments-section" hx-swap="outerHTML">
|
|
819
|
+
<input name="repoId" type="hidden" value="${repoId}" />
|
|
820
|
+
<input name="issueId" type="hidden" value="${issue.id}" />
|
|
821
|
+
<textarea name="body" placeholder="Comment" required="true"></textarea>
|
|
822
|
+
<div class="comment-actions">
|
|
823
|
+
<input name="author" placeholder="Author" />
|
|
824
|
+
<button type="submit">Comment</button>
|
|
825
|
+
</div>
|
|
826
|
+
</form>
|
|
827
|
+
</section>`
|
|
828
|
+
|
|
829
|
+
export default app
|