@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/lib/utils.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Truncate text with ellipsis
3
+ * @param {string} text - Text to truncate
4
+ * @param {number} [maxLength] - Maximum length
5
+ * @returns {string} - Truncated text
6
+ */
7
+ export function truncate(text, maxLength = 80) {
8
+ if (!text || text.length <= maxLength) return text || "";
9
+ return text.slice(0, maxLength - 1) + "…";
10
+ }
11
+
12
+ /**
13
+ * Extract commits from push events
14
+ * @param {Array} events - GitHub events
15
+ * @returns {Array} - Formatted commits
16
+ */
17
+ export function extractCommits(events) {
18
+ if (!Array.isArray(events)) return [];
19
+
20
+ return events
21
+ .filter((event) => event.type === "PushEvent")
22
+ .flatMap((event) =>
23
+ (event.payload?.commits || []).map((commit) => ({
24
+ sha: commit.sha.slice(0, 7),
25
+ message: truncate(commit.message.split("\n")[0]),
26
+ url: `https://github.com/${event.repo.name}/commit/${commit.sha}`,
27
+ repo: event.repo.name,
28
+ repoUrl: `https://github.com/${event.repo.name}`,
29
+ date: event.created_at,
30
+ })),
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Extract PRs/Issues created from events
36
+ * @param {Array} events - GitHub events
37
+ * @returns {Array} - Formatted contributions
38
+ */
39
+ export function extractContributions(events) {
40
+ if (!Array.isArray(events)) return [];
41
+
42
+ return events
43
+ .filter(
44
+ (event) =>
45
+ (event.type === "PullRequestEvent" || event.type === "IssuesEvent") &&
46
+ event.payload?.action === "opened",
47
+ )
48
+ .map((event) => {
49
+ const item = event.payload.pull_request || event.payload.issue;
50
+ return {
51
+ type: event.type === "PullRequestEvent" ? "pr" : "issue",
52
+ title: truncate(item?.title),
53
+ url: item?.html_url,
54
+ repo: event.repo.name,
55
+ repoUrl: `https://github.com/${event.repo.name}`,
56
+ number: item?.number,
57
+ date: event.created_at,
58
+ };
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Format starred repos for display
64
+ * @param {Array} repos - Starred repositories
65
+ * @returns {Array} - Formatted repos
66
+ */
67
+ export function formatStarred(repos) {
68
+ if (!Array.isArray(repos)) return [];
69
+
70
+ return repos.map((repo) => ({
71
+ name: repo.full_name,
72
+ description: truncate(repo.description, 120),
73
+ url: repo.html_url,
74
+ stars: repo.stargazers_count,
75
+ language: repo.language,
76
+ topics: repo.topics?.slice(0, 5) || [],
77
+ }));
78
+ }
79
+
80
+ /**
81
+ * Format user's own repositories for display
82
+ * @param {Array} repos - User repositories
83
+ * @returns {Array} - Formatted repos
84
+ */
85
+ export function formatRepos(repos) {
86
+ if (!Array.isArray(repos)) return [];
87
+
88
+ return repos.map((repo) => ({
89
+ name: repo.name,
90
+ fullName: repo.full_name,
91
+ description: truncate(repo.description, 120),
92
+ url: repo.html_url,
93
+ stars: repo.stargazers_count,
94
+ forks: repo.forks_count,
95
+ language: repo.language,
96
+ topics: repo.topics?.slice(0, 5) || [],
97
+ isPrivate: repo.private,
98
+ isFork: repo.fork,
99
+ pushedAt: repo.pushed_at,
100
+ updatedAt: repo.updated_at,
101
+ }));
102
+ }
103
+
104
+ /**
105
+ * Extract activity on user's repos from other users
106
+ * @param {Array} events - Repository events
107
+ * @param {string} username - Owner username to exclude
108
+ * @returns {Array} - Formatted activity
109
+ */
110
+ export function extractRepoActivity(events, username) {
111
+ if (!Array.isArray(events)) return [];
112
+
113
+ return events
114
+ .filter((event) => event.actor?.login !== username) // Exclude own activity
115
+ .map((event) => ({
116
+ type: event.type.replace("Event", "").toLowerCase(),
117
+ actor: event.actor?.login,
118
+ actorUrl: `https://github.com/${event.actor?.login}`,
119
+ actorAvatar: event.actor?.avatar_url,
120
+ repo: event.repo?.name,
121
+ repoUrl: `https://github.com/${event.repo?.name}`,
122
+ date: event.created_at,
123
+ detail: getEventDetail(event),
124
+ }));
125
+ }
126
+
127
+ /**
128
+ * Get human-readable detail for an event
129
+ * @param {object} event - GitHub event
130
+ * @returns {string} - Event description
131
+ */
132
+ function getEventDetail(event) {
133
+ switch (event.type) {
134
+ case "WatchEvent": {
135
+ return "starred";
136
+ }
137
+ case "ForkEvent": {
138
+ return "forked";
139
+ }
140
+ case "PullRequestEvent": {
141
+ return `${event.payload?.action} PR #${event.payload?.number}`;
142
+ }
143
+ case "IssuesEvent": {
144
+ return `${event.payload?.action} issue #${event.payload?.number}`;
145
+ }
146
+ case "IssueCommentEvent": {
147
+ return "commented";
148
+ }
149
+ case "CreateEvent": {
150
+ return `created ${event.payload?.ref_type}`;
151
+ }
152
+ case "DeleteEvent": {
153
+ return `deleted ${event.payload?.ref_type}`;
154
+ }
155
+ case "PushEvent": {
156
+ return `pushed ${event.payload?.size} commit(s)`;
157
+ }
158
+ default: {
159
+ return event.type?.replace("Event", "").toLowerCase() || "activity";
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Format date for display
166
+ * @param {string} dateString - ISO date string
167
+ * @param {string} [locale] - Locale for formatting
168
+ * @returns {string} - Formatted date
169
+ */
170
+ export function formatDate(dateString, locale = "en") {
171
+ const date = new Date(dateString);
172
+ return date.toLocaleDateString(locale, {
173
+ year: "numeric",
174
+ month: "short",
175
+ day: "numeric",
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Format relative time
181
+ * @param {string} dateString - ISO date string
182
+ * @returns {string} - Relative time string
183
+ */
184
+ export function formatRelativeTime(dateString) {
185
+ const date = new Date(dateString);
186
+ const now = new Date();
187
+ const diffMs = now - date;
188
+ const diffMins = Math.floor(diffMs / 60_000);
189
+ const diffHours = Math.floor(diffMs / 3_600_000);
190
+ const diffDays = Math.floor(diffMs / 86_400_000);
191
+
192
+ if (diffMins < 1) return "just now";
193
+ if (diffMins < 60) return `${diffMins}m ago`;
194
+ if (diffHours < 24) return `${diffHours}h ago`;
195
+ if (diffDays < 7) return `${diffDays}d ago`;
196
+
197
+ return formatDate(dateString);
198
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "github": {
3
+ "title": "GitHub",
4
+ "activity": "GitHub activity",
5
+ "viewAll": "View all",
6
+ "error": {
7
+ "noUsername": "No GitHub username configured. Add a username in your Indiekit configuration."
8
+ },
9
+ "featured": {
10
+ "title": "Featured Projects",
11
+ "none": "No featured repositories configured"
12
+ },
13
+ "commits": {
14
+ "title": "Recent commits",
15
+ "none": "No recent commits"
16
+ },
17
+ "stars": {
18
+ "title": "Recently starred",
19
+ "none": "No starred repositories"
20
+ },
21
+ "contributions": {
22
+ "title": "PRs & Issues",
23
+ "none": "No recent contributions"
24
+ },
25
+ "repos": {
26
+ "title": "My repositories",
27
+ "none": "No repositories found"
28
+ },
29
+ "activity": {
30
+ "title": "Activity on my repos",
31
+ "none": "No recent activity from others"
32
+ },
33
+ "widget": {
34
+ "description": "View your GitHub activity, commits, stars, and contributions.",
35
+ "view": "View activity"
36
+ }
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@rmdes/indiekit-endpoint-github",
3
+ "version": "1.0.0",
4
+ "description": "GitHub activity endpoint for Indiekit. Display commits, stars, contributions, and featured repositories.",
5
+ "keywords": [
6
+ "indiekit",
7
+ "indiekit-plugin",
8
+ "indieweb",
9
+ "github",
10
+ "activity"
11
+ ],
12
+ "homepage": "https://github.com/rmdes/indiekit-endpoint-github",
13
+ "bugs": {
14
+ "url": "https://github.com/rmdes/indiekit-endpoint-github/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/rmdes/indiekit-endpoint-github.git"
19
+ },
20
+ "author": {
21
+ "name": "Ricardo Mendes",
22
+ "url": "https://rmendes.net"
23
+ },
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "type": "module",
29
+ "main": "index.js",
30
+ "exports": {
31
+ ".": "./index.js"
32
+ },
33
+ "files": [
34
+ "assets",
35
+ "includes",
36
+ "lib",
37
+ "locales",
38
+ "views",
39
+ "index.js"
40
+ ],
41
+ "dependencies": {
42
+ "@indiekit/error": "^1.0.0-beta.25",
43
+ "express": "^5.0.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@indiekit/indiekit": ">=1.0.0-beta.25"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }
@@ -0,0 +1,22 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ {% if error %}
5
+ {{ prose({ text: error.message }) }}
6
+ {% elif activity and activity.length > 0 %}
7
+ <ul class="github-list">
8
+ {% for item in activity %}
9
+ <li class="github-list__item github-activity">
10
+ <img src="{{ item.actorAvatar }}" alt="{{ item.actor }}" class="github-activity__avatar" width="32" height="32">
11
+ <div class="github-activity__content">
12
+ <a href="{{ item.actorUrl }}" target="_blank" rel="noopener">{{ item.actor }}</a>
13
+ <span class="github-activity__detail">{{ item.detail }}</span>
14
+ <a href="{{ item.repoUrl }}" target="_blank" rel="noopener">{{ item.repo }}</a>
15
+ </div>
16
+ </li>
17
+ {% endfor %}
18
+ </ul>
19
+ {% else %}
20
+ {{ prose({ text: __("github.activity.none") }) }}
21
+ {% endif %}
22
+ {% endblock %}
@@ -0,0 +1,21 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ {% if error %}
5
+ {{ prose({ text: error.message }) }}
6
+ {% elif commits and commits.length > 0 %}
7
+ <ul class="github-list">
8
+ {% for commit in commits %}
9
+ <li class="github-list__item">
10
+ <code class="github-sha"><a href="{{ commit.url }}" target="_blank" rel="noopener">{{ commit.sha }}</a></code>
11
+ <span class="github-commit__message">{{ commit.message }}</span>
12
+ <small class="github-meta">
13
+ <a href="{{ commit.repoUrl }}" target="_blank" rel="noopener">{{ commit.repo }}</a>
14
+ </small>
15
+ </li>
16
+ {% endfor %}
17
+ </ul>
18
+ {% else %}
19
+ {{ prose({ text: __("github.commits.none") }) }}
20
+ {% endif %}
21
+ {% endblock %}
@@ -0,0 +1,24 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ {% if error %}
5
+ {{ prose({ text: error.message }) }}
6
+ {% elif contributions and contributions.length > 0 %}
7
+ <ul class="github-list">
8
+ {% for item in contributions %}
9
+ <li class="github-list__item">
10
+ {{ badge({
11
+ color: "green" if item.type === "pr" else "blue",
12
+ text: "PR" if item.type === "pr" else "Issue"
13
+ }) }}
14
+ <a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.title }}</a>
15
+ <small class="github-meta">
16
+ <a href="{{ item.repoUrl }}" target="_blank" rel="noopener">{{ item.repo }}#{{ item.number }}</a>
17
+ </small>
18
+ </li>
19
+ {% endfor %}
20
+ </ul>
21
+ {% else %}
22
+ {{ prose({ text: __("github.contributions.none") }) }}
23
+ {% endif %}
24
+ {% endblock %}
@@ -0,0 +1,155 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ {% if error %}
5
+ {{ prose({ text: error.message }) }}
6
+ {% else %}
7
+ {# Featured Projects #}
8
+ {% if featured and featured.length > 0 %}
9
+ <section class="github-section">
10
+ <h2>{{ __("github.featured.title") }}</h2>
11
+ <div class="github-featured">
12
+ {% for repo in featured %}
13
+ <article class="github-featured__repo">
14
+ <h3 class="github-featured__name">
15
+ <a href="{{ repo.url }}" target="_blank" rel="noopener">{{ repo.fullName }}</a>
16
+ {% if repo.isPrivate %}
17
+ {{ badge({ color: "blue", text: "Private" }) }}
18
+ {% endif %}
19
+ </h3>
20
+ {% if repo.description %}
21
+ <p class="github-featured__desc">{{ repo.description }}</p>
22
+ {% endif %}
23
+ <div class="github-featured__meta">
24
+ {% if repo.language %}
25
+ <span class="github-lang">{{ repo.language }}</span>
26
+ {% endif %}
27
+ <span>{{ repo.stars }} stars</span>
28
+ {% if repo.forks > 0 %}
29
+ <span>{{ repo.forks }} forks</span>
30
+ {% endif %}
31
+ </div>
32
+ {% if repo.commits and repo.commits.length > 0 %}
33
+ <details class="github-featured__commits">
34
+ <summary>Recent commits ({{ repo.commits.length }})</summary>
35
+ <ul class="github-commit-list">
36
+ {% for commit in repo.commits %}
37
+ <li>
38
+ <code><a href="{{ commit.url }}" target="_blank" rel="noopener">{{ commit.sha }}</a></code>
39
+ <span>{{ commit.message }}</span>
40
+ <small>by {{ commit.author }}</small>
41
+ </li>
42
+ {% endfor %}
43
+ </ul>
44
+ </details>
45
+ {% endif %}
46
+ </article>
47
+ {% endfor %}
48
+ </div>
49
+ </section>
50
+ {% endif %}
51
+
52
+ {# Recent Commits #}
53
+ <section class="github-section">
54
+ <h2>{{ __("github.commits.title") }}</h2>
55
+ {% if commits and commits.length > 0 %}
56
+ <ul class="github-list">
57
+ {% for commit in commits %}
58
+ <li class="github-list__item">
59
+ <code class="github-sha"><a href="{{ commit.url }}" target="_blank" rel="noopener">{{ commit.sha }}</a></code>
60
+ <span class="github-commit__message">{{ commit.message }}</span>
61
+ <small class="github-meta">
62
+ <a href="{{ commit.repoUrl }}" target="_blank" rel="noopener">{{ commit.repo }}</a>
63
+ </small>
64
+ </li>
65
+ {% endfor %}
66
+ </ul>
67
+ {% else %}
68
+ {{ prose({ text: __("github.commits.none") }) }}
69
+ {% endif %}
70
+ </section>
71
+
72
+ {# Recently Starred #}
73
+ <section class="github-section">
74
+ <h2>{{ __("github.stars.title") }}</h2>
75
+ {% if stars and stars.length > 0 %}
76
+ <ul class="github-list github-list--grid">
77
+ {% for star in stars %}
78
+ <li class="github-list__item github-star">
79
+ <a href="{{ star.url }}" class="github-star__name" target="_blank" rel="noopener">{{ star.name }}</a>
80
+ {% if star.description %}
81
+ <p class="github-star__desc">{{ star.description }}</p>
82
+ {% endif %}
83
+ <small class="github-meta">
84
+ {% if star.language %}
85
+ <span class="github-lang">{{ star.language }}</span>
86
+ {% endif %}
87
+ <span class="github-star__count">{{ star.stars }} stars</span>
88
+ </small>
89
+ </li>
90
+ {% endfor %}
91
+ </ul>
92
+ {% else %}
93
+ {{ prose({ text: __("github.stars.none") }) }}
94
+ {% endif %}
95
+ </section>
96
+
97
+ {# PRs & Issues #}
98
+ {% if contributions and contributions.length > 0 %}
99
+ <section class="github-section">
100
+ <h2>{{ __("github.contributions.title") }}</h2>
101
+ <ul class="github-list">
102
+ {% for item in contributions %}
103
+ <li class="github-list__item">
104
+ {% if item.type == "pr" %}
105
+ {{ badge({ color: "green", text: "PR" }) }}
106
+ {% else %}
107
+ {{ badge({ color: "purple", text: "Issue" }) }}
108
+ {% endif %}
109
+ <a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.title }}</a>
110
+ <small class="github-meta">
111
+ <a href="{{ item.repoUrl }}" target="_blank" rel="noopener">{{ item.repo }}#{{ item.number }}</a>
112
+ </small>
113
+ </li>
114
+ {% endfor %}
115
+ </ul>
116
+ </section>
117
+ {% endif %}
118
+
119
+ {# My Repositories #}
120
+ <section class="github-section">
121
+ <h2>{{ __("github.repos.title") }}</h2>
122
+ {% if repositories and repositories.length > 0 %}
123
+ <ul class="github-list github-list--grid">
124
+ {% for repo in repositories %}
125
+ <li class="github-list__item github-repo">
126
+ <a href="{{ repo.url }}" class="github-repo__name" target="_blank" rel="noopener">
127
+ {{ repo.name }}
128
+ </a>
129
+ {% if repo.isPrivate %}
130
+ {{ badge({ color: "gray", text: "Private" }) }}
131
+ {% endif %}
132
+ {% if repo.isFork %}
133
+ {{ badge({ color: "purple", text: "Fork" }) }}
134
+ {% endif %}
135
+ {% if repo.description %}
136
+ <p class="github-repo__desc">{{ repo.description }}</p>
137
+ {% endif %}
138
+ <small class="github-meta">
139
+ {% if repo.language %}
140
+ <span class="github-lang">{{ repo.language }}</span>
141
+ {% endif %}
142
+ <span class="github-repo__stars">{{ repo.stars }} stars</span>
143
+ {% if repo.forks > 0 %}
144
+ <span class="github-repo__forks">{{ repo.forks }} forks</span>
145
+ {% endif %}
146
+ </small>
147
+ </li>
148
+ {% endfor %}
149
+ </ul>
150
+ {% else %}
151
+ {{ prose({ text: __("github.repos.none") }) }}
152
+ {% endif %}
153
+ </section>
154
+ {% endif %}
155
+ {% endblock %}
@@ -0,0 +1,26 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ {% if error %}
5
+ {{ prose({ text: error.message }) }}
6
+ {% elif stars and stars.length > 0 %}
7
+ <ul class="github-list github-list--grid">
8
+ {% for star in stars %}
9
+ <li class="github-list__item github-star">
10
+ <a href="{{ star.url }}" class="github-star__name" target="_blank" rel="noopener">{{ star.name }}</a>
11
+ {% if star.description %}
12
+ <p class="github-star__desc">{{ star.description }}</p>
13
+ {% endif %}
14
+ <small class="github-meta">
15
+ {% if star.language %}
16
+ <span class="github-lang">{{ star.language }}</span>
17
+ {% endif %}
18
+ <span class="github-star__count">{{ star.stars }} stars</span>
19
+ </small>
20
+ </li>
21
+ {% endfor %}
22
+ </ul>
23
+ {% else %}
24
+ {{ prose({ text: __("github.stars.none") }) }}
25
+ {% endif %}
26
+ {% endblock %}