@rmdes/indiekit-endpoint-github 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/assets/icon.svg +4 -0
- package/assets/styles.css +245 -0
- package/includes/@indiekit-endpoint-github-commits.njk +13 -0
- package/includes/@indiekit-endpoint-github-stars.njk +13 -0
- package/includes/@indiekit-endpoint-github-widget.njk +12 -0
- package/index.js +98 -0
- package/lib/controllers/activity.js +124 -0
- package/lib/controllers/commits.js +84 -0
- package/lib/controllers/contributions.js +60 -0
- package/lib/controllers/dashboard.js +138 -0
- package/lib/controllers/featured.js +116 -0
- package/lib/controllers/stars.js +84 -0
- package/lib/github-client.js +168 -0
- package/lib/utils.js +198 -0
- package/locales/en.json +38 -0
- package/package.json +51 -0
- package/views/activity.njk +22 -0
- package/views/commits.njk +21 -0
- package/views/contributions.njk +24 -0
- package/views/github.njk +155 -0
- package/views/stars.njk +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @rmdes/indiekit-endpoint-github
|
|
2
|
+
|
|
3
|
+
A GitHub activity endpoint plugin for [Indiekit](https://getindiekit.com). Display your GitHub commits, stars, contributions, and featured repositories in your Indiekit admin dashboard, with a public JSON API for use in your static site.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Featured Projects**: Showcase specific repositories with recent commits
|
|
8
|
+
- **Recent Commits**: Display your latest commits across repositories
|
|
9
|
+
- **Starred Repositories**: Show repos you've recently starred
|
|
10
|
+
- **PRs & Issues**: Track your open source contributions
|
|
11
|
+
- **Public API**: JSON endpoints for integrating with static site generators
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @rmdes/indiekit-endpoint-github
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Add the plugin to your Indiekit configuration:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
export default {
|
|
25
|
+
plugins: [
|
|
26
|
+
"@rmdes/indiekit-endpoint-github",
|
|
27
|
+
// ... other plugins
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
"@rmdes/indiekit-endpoint-github": {
|
|
31
|
+
// Required: Your GitHub username
|
|
32
|
+
username: "your-username",
|
|
33
|
+
|
|
34
|
+
// Optional: GitHub personal access token for higher rate limits
|
|
35
|
+
// and access to private repos (set via GITHUB_TOKEN env var)
|
|
36
|
+
token: process.env.GITHUB_TOKEN,
|
|
37
|
+
|
|
38
|
+
// Optional: Mount path for the endpoint (default: "/github")
|
|
39
|
+
mountPath: "/github-api",
|
|
40
|
+
|
|
41
|
+
// Optional: Cache duration in milliseconds (default: 15 minutes)
|
|
42
|
+
cacheTtl: 900_000,
|
|
43
|
+
|
|
44
|
+
// Optional: Limits for each section
|
|
45
|
+
limits: {
|
|
46
|
+
commits: 10,
|
|
47
|
+
stars: 20,
|
|
48
|
+
contributions: 10,
|
|
49
|
+
repos: 10,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Optional: Featured repositories to showcase with commits
|
|
53
|
+
featuredRepos: [
|
|
54
|
+
"owner/repo-name",
|
|
55
|
+
"owner/another-repo",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Endpoints
|
|
62
|
+
|
|
63
|
+
### Protected (require authentication)
|
|
64
|
+
|
|
65
|
+
- `GET /github-api/` - Dashboard view with all sections
|
|
66
|
+
- `GET /github-api/commits` - Recent commits page
|
|
67
|
+
- `GET /github-api/stars` - Starred repositories page
|
|
68
|
+
- `GET /github-api/contributions` - PRs & Issues page
|
|
69
|
+
- `GET /github-api/activity` - Activity feed page
|
|
70
|
+
|
|
71
|
+
### Public API (no authentication)
|
|
72
|
+
|
|
73
|
+
- `GET /github-api/api/commits` - JSON: Recent commits
|
|
74
|
+
- `GET /github-api/api/stars` - JSON: Starred repositories
|
|
75
|
+
- `GET /github-api/api/activity` - JSON: Activity feed
|
|
76
|
+
- `GET /github-api/api/featured` - JSON: Featured repositories with commits
|
|
77
|
+
|
|
78
|
+
## Using the Public API
|
|
79
|
+
|
|
80
|
+
The public API endpoints return JSON and can be used by your static site generator (Eleventy, Hugo, etc.) to display GitHub activity on your public site.
|
|
81
|
+
|
|
82
|
+
Example Eleventy data file:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
import EleventyFetch from "@11ty/eleventy-fetch";
|
|
86
|
+
|
|
87
|
+
export default async function() {
|
|
88
|
+
const url = "https://your-site.com/github-api/api/featured";
|
|
89
|
+
|
|
90
|
+
const data = await EleventyFetch(url, {
|
|
91
|
+
duration: "15m",
|
|
92
|
+
type: "json",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return data.featured || [];
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Environment Variables
|
|
100
|
+
|
|
101
|
+
- `GITHUB_TOKEN` - Optional GitHub personal access token for:
|
|
102
|
+
- Higher API rate limits (5000/hour vs 60/hour)
|
|
103
|
+
- Access to private repository information
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
|
2
|
+
<path fill="#24292f" d="M0 0h96v96H0z"/>
|
|
3
|
+
<path fill="#fff" d="M48 16c-17.7 0-32 14.3-32 32 0 14.1 9.2 26.1 21.9 30.4 1.6.3 2.2-.7 2.2-1.5v-5.3c-8.9 1.9-10.8-4.3-10.8-4.3-1.5-3.7-3.6-4.7-3.6-4.7-2.9-2 .2-2 .2-2 3.2.2 4.9 3.3 4.9 3.3 2.9 4.9 7.5 3.5 9.3 2.7.3-2.1 1.1-3.5 2-4.3-7.1-.8-14.6-3.6-14.6-15.8 0-3.5 1.2-6.3 3.3-8.6-.3-.8-1.4-4.1.3-8.5 0 0 2.7-.9 8.8 3.3 2.6-.7 5.3-1.1 8-1.1s5.5.4 8 1.1c6.1-4.1 8.8-3.3 8.8-3.3 1.7 4.4.7 7.7.3 8.5 2 2.3 3.3 5.1 3.3 8.6 0 12.3-7.5 15-14.6 15.8 1.1 1 2.2 2.9 2.2 5.9v8.7c0 .8.6 1.8 2.2 1.5C70.8 74.1 80 62.1 80 48c0-17.7-14.3-32-32-32z"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/* GitHub endpoint styles */
|
|
2
|
+
|
|
3
|
+
/* Section spacing */
|
|
4
|
+
.github-section {
|
|
5
|
+
margin-block-end: var(--space-xl);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.github-section h2 {
|
|
9
|
+
margin-block-end: var(--space-m);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.github-profile {
|
|
13
|
+
align-items: center;
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: var(--space-m);
|
|
16
|
+
margin-block-end: var(--space-l);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.github-profile__avatar {
|
|
20
|
+
border-radius: 50%;
|
|
21
|
+
flex-shrink: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.github-profile__info {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
gap: var(--space-2xs);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.github-profile__info p {
|
|
31
|
+
color: var(--color-text-secondary);
|
|
32
|
+
margin: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.github-profile__link {
|
|
36
|
+
font-size: var(--step--1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* GitHub list */
|
|
40
|
+
.github-list {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: var(--space-s);
|
|
44
|
+
list-style: none;
|
|
45
|
+
margin: 0;
|
|
46
|
+
padding: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.github-list--compact {
|
|
50
|
+
gap: var(--space-2xs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.github-list--grid {
|
|
54
|
+
display: grid;
|
|
55
|
+
gap: var(--space-m);
|
|
56
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.github-list__item {
|
|
60
|
+
align-items: baseline;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
gap: var(--space-xs);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Activity styles */
|
|
67
|
+
.github-activity {
|
|
68
|
+
align-items: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.github-activity__avatar {
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.github-activity__content {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-wrap: wrap;
|
|
79
|
+
gap: var(--space-xs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.github-activity__detail {
|
|
83
|
+
color: var(--color-text-secondary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Commit styles (in main list) */
|
|
87
|
+
.github-commit__message {
|
|
88
|
+
flex: 1;
|
|
89
|
+
min-inline-size: 200px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Commit list styles (inside featured repos) */
|
|
93
|
+
.github-commit-list {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
font-size: var(--step--1);
|
|
97
|
+
gap: var(--space-xs);
|
|
98
|
+
list-style: none;
|
|
99
|
+
margin: var(--space-s) 0 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.github-commit-list code {
|
|
104
|
+
background: var(--color-background);
|
|
105
|
+
border-radius: var(--radius-s);
|
|
106
|
+
padding: var(--space-3xs) var(--space-2xs);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.github-commit-list li {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
gap: var(--space-xs);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.github-commit-list small {
|
|
116
|
+
color: var(--color-text-secondary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Featured repos styles */
|
|
120
|
+
.github-featured {
|
|
121
|
+
display: grid;
|
|
122
|
+
gap: var(--space-l);
|
|
123
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
124
|
+
margin-block-end: var(--space-xl);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.github-featured__commits {
|
|
128
|
+
border-block-start: 1px solid var(--color-border);
|
|
129
|
+
margin-block-start: var(--space-s);
|
|
130
|
+
padding-block-start: var(--space-s);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.github-featured__commits summary {
|
|
134
|
+
color: var(--color-text-secondary);
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
font-size: var(--step--1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.github-featured__desc {
|
|
140
|
+
color: var(--color-text-secondary);
|
|
141
|
+
font-size: var(--step--1);
|
|
142
|
+
margin: 0 0 var(--space-s);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.github-featured__meta {
|
|
146
|
+
color: var(--color-text-secondary);
|
|
147
|
+
display: flex;
|
|
148
|
+
font-size: var(--step--1);
|
|
149
|
+
gap: var(--space-s);
|
|
150
|
+
margin-block-end: var(--space-s);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.github-featured__name {
|
|
154
|
+
font-size: var(--step-0);
|
|
155
|
+
margin: 0 0 var(--space-xs);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.github-featured__repo {
|
|
159
|
+
background: var(--color-offset);
|
|
160
|
+
border-radius: var(--radius-m);
|
|
161
|
+
padding: var(--space-m);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Language indicator */
|
|
165
|
+
.github-lang {
|
|
166
|
+
margin-inline-end: var(--space-xs);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Metadata */
|
|
170
|
+
.github-meta {
|
|
171
|
+
color: var(--color-text-secondary);
|
|
172
|
+
font-size: var(--step--1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Repo card styles */
|
|
176
|
+
.github-repo {
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.github-repo__desc {
|
|
181
|
+
color: var(--color-text-secondary);
|
|
182
|
+
font-size: var(--step--1);
|
|
183
|
+
margin: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.github-repo__forks,
|
|
187
|
+
.github-repo__stars {
|
|
188
|
+
margin-inline-start: var(--space-xs);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.github-repo__name {
|
|
192
|
+
font-weight: var(--font-weight-semibold);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* SHA badge styles */
|
|
196
|
+
.github-sha {
|
|
197
|
+
background: var(--color-offset);
|
|
198
|
+
border-radius: var(--radius-s);
|
|
199
|
+
font-size: var(--step--1);
|
|
200
|
+
padding-block: var(--space-3xs);
|
|
201
|
+
padding-inline: var(--space-2xs);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Star styles */
|
|
205
|
+
.github-star {
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.github-star__count {
|
|
210
|
+
margin-inline-start: var(--space-xs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.github-star__desc {
|
|
214
|
+
color: var(--color-text-secondary);
|
|
215
|
+
font-size: var(--step--1);
|
|
216
|
+
margin: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.github-star__name {
|
|
220
|
+
font-weight: var(--font-weight-semibold);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/*
|
|
224
|
+
* Link styles - grouped by specificity (ascending)
|
|
225
|
+
* Non-pseudo selectors first, then pseudo selectors
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
/* Specificity: 0,1,1 (one class, one element) */
|
|
229
|
+
.github-featured__name a {
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.github-sha a {
|
|
234
|
+
text-decoration: none;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Specificity: 0,1,2 (one class, two elements) */
|
|
238
|
+
.github-commit-list code a {
|
|
239
|
+
text-decoration: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* Pseudo-class selectors */
|
|
243
|
+
.github-featured__name a:hover {
|
|
244
|
+
text-decoration: underline;
|
|
245
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{# Commits partial for embedding in other templates #}
|
|
2
|
+
{% if commits and commits.length > 0 %}
|
|
3
|
+
<ul class="github-list github-list--compact">
|
|
4
|
+
{% for commit in commits %}
|
|
5
|
+
{% if loop.index <= 5 %}
|
|
6
|
+
<li class="github-list__item">
|
|
7
|
+
<code class="github-sha"><a href="{{ commit.url }}" target="_blank" rel="noopener">{{ commit.sha }}</a></code>
|
|
8
|
+
<span class="github-commit__message">{{ commit.message }}</span>
|
|
9
|
+
</li>
|
|
10
|
+
{% endif %}
|
|
11
|
+
{% endfor %}
|
|
12
|
+
</ul>
|
|
13
|
+
{% endif %}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{# Stars partial for embedding in other templates #}
|
|
2
|
+
{% if stars and stars.length > 0 %}
|
|
3
|
+
<ul class="github-list github-list--compact">
|
|
4
|
+
{% for star in stars %}
|
|
5
|
+
{% if loop.index <= 5 %}
|
|
6
|
+
<li class="github-list__item">
|
|
7
|
+
<a href="{{ star.url }}" target="_blank" rel="noopener">{{ star.name }}</a>
|
|
8
|
+
<small class="github-meta">{{ star.stars }} stars</small>
|
|
9
|
+
</li>
|
|
10
|
+
{% endif %}
|
|
11
|
+
{% endfor %}
|
|
12
|
+
</ul>
|
|
13
|
+
{% endif %}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{% call widget({
|
|
2
|
+
title: __("github.title")
|
|
3
|
+
}) %}
|
|
4
|
+
<p class="prose">{{ __("github.widget.description") }}</p>
|
|
5
|
+
<div class="button-grid">
|
|
6
|
+
{{ button({
|
|
7
|
+
classes: "button--secondary-on-offset",
|
|
8
|
+
href: application.githubEndpoint or "/github-api",
|
|
9
|
+
text: __("github.widget.view")
|
|
10
|
+
}) }}
|
|
11
|
+
</div>
|
|
12
|
+
{% endcall %}
|
package/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
import { activityController } from "./lib/controllers/activity.js";
|
|
4
|
+
import { commitsController } from "./lib/controllers/commits.js";
|
|
5
|
+
import { contributionsController } from "./lib/controllers/contributions.js";
|
|
6
|
+
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
7
|
+
import { featuredController } from "./lib/controllers/featured.js";
|
|
8
|
+
import { starsController } from "./lib/controllers/stars.js";
|
|
9
|
+
|
|
10
|
+
// Module-level routers (matching Indiekit's endpoint pattern)
|
|
11
|
+
const protectedRouter = express.Router();
|
|
12
|
+
const publicRouter = express.Router();
|
|
13
|
+
|
|
14
|
+
const defaults = {
|
|
15
|
+
mountPath: "/github",
|
|
16
|
+
username: "",
|
|
17
|
+
token: process.env.GITHUB_TOKEN,
|
|
18
|
+
cacheTtl: 900_000, // 15 minutes in ms
|
|
19
|
+
limits: {
|
|
20
|
+
commits: 10,
|
|
21
|
+
stars: 20,
|
|
22
|
+
contributions: 10,
|
|
23
|
+
activity: 20,
|
|
24
|
+
repos: 10,
|
|
25
|
+
},
|
|
26
|
+
repos: [], // Empty = all repos, or specify ['owner/repo', ...] for filtering activity
|
|
27
|
+
featuredRepos: [], // Repos to showcase with commits, e.g. ['owner/repo', ...]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default class GitHubEndpoint {
|
|
31
|
+
name = "GitHub activity endpoint";
|
|
32
|
+
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.options = { ...defaults, ...options };
|
|
35
|
+
this.mountPath = this.options.mountPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get environment() {
|
|
39
|
+
return ["GITHUB_TOKEN"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get navigationItems() {
|
|
43
|
+
return {
|
|
44
|
+
href: this.options.mountPath,
|
|
45
|
+
text: "github.title",
|
|
46
|
+
requiresDatabase: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get shortcutItems() {
|
|
51
|
+
return {
|
|
52
|
+
url: this.options.mountPath,
|
|
53
|
+
name: "github.activity",
|
|
54
|
+
iconName: "syndicate",
|
|
55
|
+
requiresDatabase: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Protected routes (require authentication)
|
|
61
|
+
* HTML pages for admin dashboard
|
|
62
|
+
*/
|
|
63
|
+
get routes() {
|
|
64
|
+
// Dashboard
|
|
65
|
+
protectedRouter.get("/", dashboardController.get);
|
|
66
|
+
|
|
67
|
+
// Individual sections (HTML pages)
|
|
68
|
+
protectedRouter.get("/commits", commitsController.get);
|
|
69
|
+
protectedRouter.get("/stars", starsController.get);
|
|
70
|
+
protectedRouter.get("/contributions", contributionsController.get);
|
|
71
|
+
protectedRouter.get("/activity", activityController.get);
|
|
72
|
+
protectedRouter.get("/featured", featuredController.get);
|
|
73
|
+
|
|
74
|
+
return protectedRouter;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Public routes (no authentication required)
|
|
79
|
+
* JSON API endpoints for Eleventy widgets
|
|
80
|
+
*/
|
|
81
|
+
get routesPublic() {
|
|
82
|
+
// JSON API for widgets - publicly accessible
|
|
83
|
+
publicRouter.get("/api/commits", commitsController.api);
|
|
84
|
+
publicRouter.get("/api/stars", starsController.api);
|
|
85
|
+
publicRouter.get("/api/activity", activityController.api);
|
|
86
|
+
publicRouter.get("/api/featured", featuredController.api);
|
|
87
|
+
|
|
88
|
+
return publicRouter;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
init(Indiekit) {
|
|
92
|
+
Indiekit.addEndpoint(this);
|
|
93
|
+
|
|
94
|
+
// Store GitHub config in application for controller access
|
|
95
|
+
Indiekit.config.application.githubConfig = this.options;
|
|
96
|
+
Indiekit.config.application.githubEndpoint = this.mountPath;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { GitHubClient } from "../github-client.js";
|
|
2
|
+
import * as utils from "../utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Display activity on user's repositories from others
|
|
6
|
+
* @type {import("express").RequestHandler}
|
|
7
|
+
*/
|
|
8
|
+
export const activityController = {
|
|
9
|
+
async get(request, response, next) {
|
|
10
|
+
try {
|
|
11
|
+
const { username, token, cacheTtl, limits, repos } =
|
|
12
|
+
request.app.locals.application.githubConfig;
|
|
13
|
+
|
|
14
|
+
if (!username) {
|
|
15
|
+
return response.render("activity", {
|
|
16
|
+
title: response.locals.__("github.activity.title"),
|
|
17
|
+
error: { message: response.locals.__("github.error.noUsername") },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
22
|
+
|
|
23
|
+
let activity = [];
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (repos.length > 0) {
|
|
27
|
+
// Fetch events for specific repos
|
|
28
|
+
const repoEventPromises = repos.map(async (repoPath) => {
|
|
29
|
+
const [owner, repo] = repoPath.split("/");
|
|
30
|
+
try {
|
|
31
|
+
return await client.getRepoEvents(owner, repo, 30);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const repoEvents = await Promise.all(repoEventPromises);
|
|
38
|
+
const allEvents = repoEvents.flat();
|
|
39
|
+
activity = utils.extractRepoActivity(allEvents, username);
|
|
40
|
+
} else {
|
|
41
|
+
// Use received events (events on user's repos)
|
|
42
|
+
const events = await client.fetch(
|
|
43
|
+
`/users/${username}/received_events?per_page=${limits.activity * 2}`,
|
|
44
|
+
);
|
|
45
|
+
activity = utils.extractRepoActivity(events, username);
|
|
46
|
+
}
|
|
47
|
+
} catch (apiError) {
|
|
48
|
+
console.error("GitHub API error:", apiError);
|
|
49
|
+
return response.render("activity", {
|
|
50
|
+
title: response.locals.__("github.activity.title"),
|
|
51
|
+
actions: [],
|
|
52
|
+
parent: {
|
|
53
|
+
href: request.baseUrl,
|
|
54
|
+
text: response.locals.__("github.title"),
|
|
55
|
+
},
|
|
56
|
+
error: { message: apiError.message || "Failed to fetch activity" },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
activity = activity.slice(0, limits.activity);
|
|
61
|
+
|
|
62
|
+
response.render("activity", {
|
|
63
|
+
title: response.locals.__("github.activity.title"),
|
|
64
|
+
actions: [],
|
|
65
|
+
parent: {
|
|
66
|
+
href: request.baseUrl,
|
|
67
|
+
text: response.locals.__("github.title"),
|
|
68
|
+
},
|
|
69
|
+
activity,
|
|
70
|
+
username,
|
|
71
|
+
mountPath: request.baseUrl,
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
next(error);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async api(request, response, next) {
|
|
79
|
+
try {
|
|
80
|
+
const { username, token, cacheTtl, limits, repos } =
|
|
81
|
+
request.app.locals.application.githubConfig;
|
|
82
|
+
|
|
83
|
+
if (!username) {
|
|
84
|
+
return response.status(400).json({ error: "No username configured" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
88
|
+
|
|
89
|
+
let activity = [];
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (repos.length > 0) {
|
|
93
|
+
const repoEventPromises = repos.map(async (repoPath) => {
|
|
94
|
+
const [owner, repo] = repoPath.split("/");
|
|
95
|
+
try {
|
|
96
|
+
return await client.getRepoEvents(owner, repo, 20);
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const repoEvents = await Promise.all(repoEventPromises);
|
|
103
|
+
const allEvents = repoEvents.flat();
|
|
104
|
+
activity = utils.extractRepoActivity(allEvents, username);
|
|
105
|
+
} else {
|
|
106
|
+
const events = await client.fetch(
|
|
107
|
+
`/users/${username}/received_events?per_page=${limits.activity}`,
|
|
108
|
+
);
|
|
109
|
+
activity = utils.extractRepoActivity(events, username);
|
|
110
|
+
}
|
|
111
|
+
} catch (apiError) {
|
|
112
|
+
return response
|
|
113
|
+
.status(apiError.status || 500)
|
|
114
|
+
.json({ error: apiError.message });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
activity = activity.slice(0, limits.activity);
|
|
118
|
+
|
|
119
|
+
response.json({ activity });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
next(error);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { GitHubClient } from "../github-client.js";
|
|
2
|
+
import * as utils from "../utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Display commits list
|
|
6
|
+
* @type {import("express").RequestHandler}
|
|
7
|
+
*/
|
|
8
|
+
export const commitsController = {
|
|
9
|
+
async get(request, response, next) {
|
|
10
|
+
try {
|
|
11
|
+
const { username, token, cacheTtl, limits } =
|
|
12
|
+
request.app.locals.application.githubConfig;
|
|
13
|
+
|
|
14
|
+
if (!username) {
|
|
15
|
+
return response.render("commits", {
|
|
16
|
+
title: response.locals.__("github.commits.title"),
|
|
17
|
+
error: { message: response.locals.__("github.error.noUsername") },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
22
|
+
|
|
23
|
+
let events = [];
|
|
24
|
+
try {
|
|
25
|
+
events = await client.getUserEvents(username, 100);
|
|
26
|
+
} catch (apiError) {
|
|
27
|
+
console.error("GitHub API error:", apiError);
|
|
28
|
+
return response.render("commits", {
|
|
29
|
+
title: response.locals.__("github.commits.title"),
|
|
30
|
+
actions: [],
|
|
31
|
+
parent: {
|
|
32
|
+
href: request.baseUrl,
|
|
33
|
+
text: response.locals.__("github.title"),
|
|
34
|
+
},
|
|
35
|
+
error: { message: apiError.message || "Failed to fetch commits" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const commits = utils.extractCommits(events).slice(0, limits.commits * 3);
|
|
40
|
+
|
|
41
|
+
response.render("commits", {
|
|
42
|
+
title: response.locals.__("github.commits.title"),
|
|
43
|
+
actions: [],
|
|
44
|
+
parent: {
|
|
45
|
+
href: request.baseUrl,
|
|
46
|
+
text: response.locals.__("github.title"),
|
|
47
|
+
},
|
|
48
|
+
commits,
|
|
49
|
+
username,
|
|
50
|
+
mountPath: request.baseUrl,
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
next(error);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async api(request, response, next) {
|
|
58
|
+
try {
|
|
59
|
+
const { username, token, cacheTtl, limits } =
|
|
60
|
+
request.app.locals.application.githubConfig;
|
|
61
|
+
|
|
62
|
+
if (!username) {
|
|
63
|
+
return response.status(400).json({ error: "No username configured" });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
67
|
+
|
|
68
|
+
let events = [];
|
|
69
|
+
try {
|
|
70
|
+
events = await client.getUserEvents(username, 50);
|
|
71
|
+
} catch (apiError) {
|
|
72
|
+
return response
|
|
73
|
+
.status(apiError.status || 500)
|
|
74
|
+
.json({ error: apiError.message });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const commits = utils.extractCommits(events).slice(0, limits.commits);
|
|
78
|
+
|
|
79
|
+
response.json({ commits });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
next(error);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|