Skip to content

Commit 3a9ab71

Browse files
committed
feat: add GitHub repository showcase with proxy and fallback data
1 parent 18af259 commit 3a9ab71

21 files changed

Lines changed: 16047 additions & 22 deletions

File tree

.documentation/specs/001-github-repo-showcase/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ required_gates: checklist, analyze, critic
1111

1212
**Feature Branch**: `001-github-repo-showcase`
1313
**Created**: 2026-04-16
14-
**Status**: Draft
14+
**Status**: In Progress
1515
**Input**: User description: "Create a MarkHazleton GitHub Repository app that showcases data from the generated repositories dataset in github-stats-spark, with strong Bootstrap flair and a world-class presentation similar in spirit to the existing Projects and Articles experiences."
1616

1717
## Rationale Summary

.documentation/specs/001-github-repo-showcase/tasks.md

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ description: "Executable task list for the GitHub Repository Showcase feature"
2222

2323
**Purpose**: Prepare the repository showcase file structure and feature-specific test surfaces.
2424

25-
- [ ] T001 Create repository model and service scaffolds in `src/models/Repository.ts` and `src/services/RepositoryService.ts`
26-
- [ ] T002 [P] Create repository page and route test scaffolds in `src/components/Repositories.tsx` and `tests/integration/repositories/RepositoriesPage.test.tsx`
27-
- [ ] T003 [P] Create repository proxy scaffolds in `api/proxy-repositories/function.json`, `api/proxy-repositories/index.js`, and `api/proxy-repositories/package.json`
28-
- [ ] T004 [P] Create repository data and test scaffolds in `src/data/repositories.json`, `tests/unit/models/Repository.test.ts`, `tests/unit/services/RepositoryService.test.ts`, and `tests/contract/repositories/repositoryFeed.contract.test.ts`
25+
- [X] T001 Create repository model and service scaffolds in `src/models/Repository.ts` and `src/services/RepositoryService.ts`
26+
- [X] T002 [P] Create repository page and route test scaffolds in `src/components/Repositories.tsx` and `tests/integration/repositories/RepositoriesPage.test.tsx`
27+
- [X] T003 [P] Create repository proxy scaffolds in `api/proxy-repositories/function.json`, `api/proxy-repositories/index.js`, and `api/proxy-repositories/package.json`
28+
- [X] T004 [P] Create repository data and test scaffolds in `src/data/repositories.json`, `tests/unit/models/Repository.test.ts`, `tests/unit/services/RepositoryService.test.ts`, and `tests/contract/repositories/repositoryFeed.contract.test.ts`
2929

3030
---
3131

@@ -35,16 +35,15 @@ description: "Executable task list for the GitHub Repository Showcase feature"
3535

3636
**⚠️ CRITICAL**: No user story work should start until this phase is complete.
3737

38-
- [ ] T005 Implement Zod schemas and typed repository feed exports in `src/models/Repository.ts`
39-
- [ ] T005 Implement Zod schemas and typed repository feed exports, including explicit `FeedMetadata` fields for freshness and schema compatibility, in `src/models/Repository.ts`
40-
- [ ] T006 [P] Add the embedded repository fallback snapshot in `src/data/repositories.json`
41-
- [ ] T007 Implement repository fetch, validation, cache metadata, private-repository exclusion, curated-first featured selection with automatic fallback ranking, and fallback mapping in `src/services/RepositoryService.ts`
42-
- [ ] T008 [P] Add the development proxy route for `/api/repositories` in `vite.config.ts`
43-
- [ ] T009 Implement the production repository proxy in `api/proxy-repositories/index.js`
44-
- [ ] T010 [P] Configure the repository proxy function metadata and dependencies in `api/proxy-repositories/function.json` and `api/proxy-repositories/package.json`
45-
- [ ] T011 Add contract coverage for the feed shape, explicit metadata fields, and minimum required repository fields in `tests/contract/repositories/repositoryFeed.contract.test.ts`
46-
- [ ] T012 Add unit coverage for schema parsing, view-model derivation, and invalid feed rejection in `tests/unit/models/Repository.test.ts`
47-
- [ ] T013 Add unit coverage for remote, cache, and local fallback behavior, private-repository exclusion, and retry-ready failure handling in `tests/unit/services/RepositoryService.test.ts`
38+
- [X] T005 Implement Zod schemas and typed repository feed exports, including explicit `FeedMetadata` fields for freshness and schema compatibility, in `src/models/Repository.ts`
39+
- [X] T006 [P] Add the embedded repository fallback snapshot in `src/data/repositories.json`
40+
- [X] T007 Implement repository fetch, validation, cache metadata, private-repository exclusion, curated-first featured selection with automatic fallback ranking, and fallback mapping in `src/services/RepositoryService.ts`
41+
- [X] T008 [P] Add the development proxy route for `/api/repositories` in `vite.config.ts`
42+
- [X] T009 Implement the production repository proxy in `api/proxy-repositories/index.js`
43+
- [X] T010 [P] Configure the repository proxy function metadata and dependencies in `api/proxy-repositories/function.json` and `api/proxy-repositories/package.json`
44+
- [X] T011 Add contract coverage for the feed shape, explicit metadata fields, and minimum required repository fields in `tests/contract/repositories/repositoryFeed.contract.test.ts`
45+
- [X] T012 Add unit coverage for schema parsing, view-model derivation, and invalid feed rejection in `tests/unit/models/Repository.test.ts`
46+
- [X] T013 Add unit coverage for remote, cache, and local fallback behavior, private-repository exclusion, and retry-ready failure handling in `tests/unit/services/RepositoryService.test.ts`
4847

4948
**Checkpoint**: Repository data can be loaded, validated, cached, and served through both development and production integration paths.
5049

@@ -58,15 +57,15 @@ description: "Executable task list for the GitHub Repository Showcase feature"
5857

5958
### Tests for User Story 1
6059

61-
- [ ] T014 [P] [US1] Add route and first-render integration coverage for hero metrics, recent activity context, and featured spotlight rendering in `tests/integration/repositories/RepositoriesPage.test.tsx`
60+
- [X] T014 [P] [US1] Add route and first-render integration coverage for hero metrics, recent activity context, and featured spotlight rendering in `tests/integration/repositories/RepositoriesPage.test.tsx`
6261

6362
### Implementation for User Story 1
6463

65-
- [ ] T015 [US1] Register the lazy-loaded repository route in `src/App.tsx`
66-
- [ ] T016 [US1] Add the repository showcase entry to the Apps navigation and generated sitemap route list in `src/components/Header.tsx` and `src/utils/generateSitemap.ts`
67-
- [ ] T017 [US1] Implement the repository page shell, hero metrics, recent activity summary, source-status messaging, retry-capable error state, and featured spotlight section in `src/components/Repositories.tsx`
68-
- [ ] T018 [US1] Connect the repository page to `RepositoryService` and render the base repository collection in `src/components/Repositories.tsx`
69-
- [ ] T019 [US1] Add page-level SEO copy and accessible section structure in `src/components/Repositories.tsx`
64+
- [X] T015 [US1] Register the lazy-loaded repository route in `src/App.tsx`
65+
- [X] T016 [US1] Add the repository showcase entry to the Apps navigation and generated sitemap route list in `src/components/Header.tsx` and `src/utils/generateSitemap.ts`
66+
- [X] T017 [US1] Implement the repository page shell, hero metrics, recent activity summary, source-status messaging, retry-capable error state, and featured spotlight section in `src/components/Repositories.tsx`
67+
- [X] T018 [US1] Connect the repository page to `RepositoryService` and render the base repository collection in `src/components/Repositories.tsx`
68+
- [X] T019 [US1] Add page-level SEO copy and accessible section structure in `src/components/Repositories.tsx`
7069

7170
**Checkpoint**: User Story 1 is independently functional and demoable as the MVP.
7271

@@ -205,6 +204,10 @@ Task: "T026 [US3] Enrich repository cards with summary text, activity badges, an
205204
- The requirements checklist exists and is complete.
206205
- No existing `analyze.md`, `critic.md`, or other gate findings were present when tasks were generated.
207206

207+
## Gate Acknowledgements
208+
209+
- 2026-04-17: User approved proceeding with implementation despite unresolved `analyze` and `critic` gate artifacts.
210+
208211
---
209212

210213
## Notes

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ local.settings.json
4545

4646
# Build artifacts
4747
/build
48+
/.build
4849
/.next
4950
/out
5051
coverage/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"bindings": [
3+
{
4+
"authLevel": "anonymous",
5+
"type": "httpTrigger",
6+
"direction": "in",
7+
"name": "req",
8+
"methods": ["get", "options"]
9+
},
10+
{
11+
"type": "http",
12+
"direction": "out",
13+
"name": "res"
14+
}
15+
]
16+
}

api/proxy-repositories/index.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
const axios = require("axios");
2+
3+
const ALLOWED_ORIGINS = [
4+
"https://bootstrapspark.markhazleton.com",
5+
"https://markhazleton.github.io",
6+
"http://localhost:3000",
7+
"http://127.0.0.1:3000",
8+
];
9+
10+
const REPOSITORY_SOURCE_URL = "https://markhazleton.com/repositories.json";
11+
12+
module.exports = async function (context, req) {
13+
context.log("Processing repositories proxy request");
14+
15+
const origin = req.headers.origin || req.headers.referer;
16+
const allowedOrigin = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
17+
18+
const baseHeaders = {
19+
"Access-Control-Allow-Origin": allowedOrigin,
20+
"Access-Control-Allow-Methods": "GET, OPTIONS",
21+
"Access-Control-Allow-Headers": "Content-Type",
22+
"Access-Control-Allow-Credentials": "false",
23+
"Content-Type": "application/json",
24+
};
25+
26+
if (req.method === "OPTIONS") {
27+
context.res = {
28+
status: 200,
29+
headers: baseHeaders,
30+
};
31+
return;
32+
}
33+
34+
try {
35+
const response = await axios.get(REPOSITORY_SOURCE_URL, {
36+
timeout: 8000,
37+
headers: {
38+
"User-Agent": "BootstrapSpark/1.0 Repository-Proxy",
39+
Accept: "application/json",
40+
},
41+
});
42+
43+
const payload = response.data;
44+
const hasMinimumShape =
45+
payload && payload.profile && Array.isArray(payload.repositories) && payload.metadata;
46+
47+
if (!hasMinimumShape) {
48+
throw new Error("Repository feed does not contain profile/repositories/metadata");
49+
}
50+
51+
context.res = {
52+
status: 200,
53+
headers: baseHeaders,
54+
body: payload,
55+
};
56+
} catch (error) {
57+
context.log.error("Error fetching repositories:", error.message);
58+
59+
context.res = {
60+
status: 502,
61+
headers: baseHeaders,
62+
body: {
63+
error: "Failed to retrieve repository feed",
64+
message: error.message,
65+
timestamp: new Date().toISOString(),
66+
},
67+
};
68+
}
69+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "proxy-repositories",
3+
"version": "1.0.0",
4+
"description": "Azure Function to proxy repository showcase feed requests",
5+
"main": "index.js",
6+
"dependencies": {
7+
"axios": "^1.15.0"
8+
}
9+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
"dev": "concurrently \"vite\" \"npm run watch-css\"",
99
"dev:swa": "swa start",
1010
"dev:debug": "vite --debug",
11-
"build": "npm run clean && npm run build-css && tsc -b && npm run generate-seo-files && vite build --mode production",
11+
"build": "npm run clean && npm run build-css && npm run sync:repositories-data && tsc -b && npm run generate-seo-files && vite build --mode production",
1212
"build:analyze": "npm run build && npx vite-bundle-analyzer docs",
1313
"lint": "eslint .",
1414
"lint:fix": "eslint . --fix",
1515
"preview": "vite preview",
1616
"preview:swa": "swa start docs --run \"vite preview\"",
1717
"clean": "rimraf docs && rimraf node_modules/.vite",
1818
"sync:bootswatch-themes": "node ./scripts/sync-bootswatch-themes.mjs",
19+
"sync:repositories-data": "node ./scripts/sync-repositories-data.mjs",
1920
"build-css": "sass --load-path=node_modules --quiet-deps src/scss/styles.scss src/css/styles.css",
2021
"watch-css": "sass --load-path=node_modules --quiet-deps --watch src/scss/styles.scss:src/css/styles.css",
2122
"generate-sitemap": "node --import tsx ./src/utils/generateSitemap.ts",

scripts/sync-repositories-data.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
const workspaceRoot = process.cwd();
5+
const outputPath = path.join(workspaceRoot, ".build", "data", "repositories.json");
6+
const sourceUrl = process.env.REPOSITORIES_FEED_URL || "https://markhazleton.com/repositories.json";
7+
8+
const isValidFeed = (payload) => {
9+
if (!payload || typeof payload !== "object") {
10+
return false;
11+
}
12+
13+
if (!payload.profile || typeof payload.profile !== "object") {
14+
return false;
15+
}
16+
17+
if (!Array.isArray(payload.repositories)) {
18+
return false;
19+
}
20+
21+
if (!payload.metadata || typeof payload.metadata !== "object") {
22+
return false;
23+
}
24+
25+
return true;
26+
};
27+
28+
const run = async () => {
29+
console.log(`Syncing repositories data from ${sourceUrl}`);
30+
31+
const response = await fetch(sourceUrl, {
32+
headers: {
33+
Accept: "application/json",
34+
"Cache-Control": "no-cache",
35+
},
36+
});
37+
38+
if (!response.ok) {
39+
throw new Error(`Failed to fetch repositories feed: ${response.status} ${response.statusText}`);
40+
}
41+
42+
const payload = await response.json();
43+
44+
if (!isValidFeed(payload)) {
45+
throw new Error("Fetched repositories feed is missing required profile/repositories/metadata shape.");
46+
}
47+
48+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
49+
fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
50+
console.log(`Wrote fresh repositories build artifact to ${outputPath}`);
51+
};
52+
53+
run().catch((error) => {
54+
console.error("Repository sync failed:", error.message);
55+
process.exit(1);
56+
});

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import "./utils/imageUtils";
1919
const Hero = lazy(() => import("./components/Hero"));
2020
const About = lazy(() => import("./components/About"));
2121
const Projects = lazy(() => import("./components/Projects"));
22+
const Repositories = lazy(() => import("./components/Repositories"));
2223
const Articles = lazy(() => import("./components/Articles"));
2324
const Joke = lazy(() => import("./components/Joke"));
2425
const WeatherForecast = lazy(() => import("./components/WeatherForecast"));
@@ -64,6 +65,7 @@ const AppWithVersionCheck: React.FC = () => {
6465
<Route path="/" element={<Hero />} />
6566
<Route path="/about" element={<About />} />
6667
<Route path="/projects" element={<Projects />} />
68+
<Route path="/repositories" element={<Repositories />} />
6769
<Route path="/joke" element={<Joke />} />
6870
<Route path="/articles" element={<Articles />} />
6971
<Route path="/weather" element={<WeatherForecast />} />

src/components/Header.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
House,
44
Person,
55
Briefcase,
6+
Github,
67
JournalText,
78
EmojiLaughing,
89
Cloud,
@@ -39,6 +40,7 @@ const Header: React.FC = () => {
3940

4041
const isAppsActive =
4142
location.pathname === "/projects" ||
43+
location.pathname === "/repositories" ||
4244
location.pathname === "/articles" ||
4345
location.pathname === "/joke" ||
4446
location.pathname === "/weather" ||
@@ -113,6 +115,15 @@ const Header: React.FC = () => {
113115
<Briefcase className="me-2" /> Projects
114116
</Link>
115117
</li>
118+
<li>
119+
<Link
120+
className="dropdown-item"
121+
to="/repositories"
122+
aria-label="Repositories page"
123+
>
124+
<Github className="me-2" /> Repositories
125+
</Link>
126+
</li>
116127
<li>
117128
<Link className="dropdown-item" to="/articles" aria-label="Articles page">
118129
<JournalText className="me-2" /> Articles

0 commit comments

Comments
 (0)