Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/enrich-projects.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Enrich Projects

on:
schedule:
# Every Monday at 02:00 UTC
- cron: "0 2 * * 1"
Comment on lines +4 to +6

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github actions have a terrible reputation when it comes to running them as background jobs. this may end up running at a later time, which can be acceptable to some extent, but sometimes it may not even run at all, due to high demand at gh's end or so.

i'd suggest we expose this via an endpoint that we can hit with upstash. wdyt?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. Valid concerns. I'd like to give it a go for a bit and see how that goes first? But keeping the upstash option in the pocket if it's needed @kaf-lamed-beyt. Thanks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing 🫡

workflow_dispatch: {}

permissions:
contents: write
pull-requests: write

jobs:
enrich:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm ci

- name: Run enrichment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/enrich-projects.mjs

- name: Check for changes
id: diff
run: |
git diff --quiet data/projects.enriched.json && echo "changed=false" >> $GITHUB_OUTPUT || echo "changed=true" >> $GITHUB_OUTPUT

- name: Commit enriched data
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/projects.enriched.json
git commit -m "chore: update enriched project data [skip ci]"
git push
43 changes: 43 additions & 0 deletions .github/workflows/generate-readme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Generate README

on:
push:
branches:
- main
paths:
- "data/projects.json"
workflow_dispatch: {}

permissions:
contents: write

jobs:
generate:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm ci

- name: Regenerate README
run: node scripts/generate-readme.mjs

- name: Check for changes
id: diff
run: |
git diff --quiet README.MD && echo "changed=false" >> $GITHUB_OUTPUT || echo "changed=true" >> $GITHUB_OUTPUT

- name: Commit README
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.MD
git commit -m "chore: regenerate README from projects.json [skip ci]"
git push
23 changes: 23 additions & 0 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Validate PR

on:
pull_request:
paths:
- "data/projects.json"

jobs:
validate:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm ci

- name: Validate projects.json
run: node scripts/validate-projects.mjs
173 changes: 79 additions & 94 deletions app/utils/projects.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,92 @@
import { marked } from "marked";
import * as cheerio from "cheerio";
import { cache } from "react";
import projectsData from "../../data/projects.enriched.json";

export type ProjectStatus =
| "active"
| "stale"
| "inactive"
| "archived"
| "deprecated"
| "deleted"
| "unknown";

export type ManualStatus = "inactive" | "archived" | "deprecated" | "deleted";

export type Author = {
name: string;
link: string;
};

const getData = cache(async () => {
const res = await fetch(
"https://raw.githubusercontent.com/acekyd/made-in-nigeria/main/README.MD",
{ next: { revalidate: false } }
);

if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error("Failed to fetch data");
}

const markdownData = await res.text();

// const markdownData = await fs.readFile(process.cwd() + '/README.MD', 'utf8');

const html = marked(markdownData);
const $ = cheerio.load(html); // load the html string into cheerio

// Select all <li> elements using jQuery-like syntax and extract their text
const liTextArray = $("li")
.map((index, element) => $(element).html())
.get();
export type Project = {
name: string;
repoUrl: string;
description: string;
authors: Author[];
manualStatus?: ManualStatus;
computed?: {
status: ProjectStatus;
stars: number | null;
lastPushed: string | null;
language: string | null;
checkedAt: string;
error?: string;
};
// Legacy shape — keeps existing components working without changes
repoName: string;
repoLink: string;
repoDescription: string;
repoAuthor: string;
repoAuthorLink: string;
isInactive: boolean;
isArchived: boolean;
};

// process the text to get the data you want
const repositories = convertToJSON(liTextArray);
function normalizeProject(raw: (typeof projectsData)[number]): Project {
const primaryAuthor = raw.authors?.[0];
const status = raw.computed?.status as ProjectStatus;

return repositories;
});
return {
...raw,
authors: (raw.authors ?? []) as Author[],
computed: raw.computed as Project["computed"],
manualStatus: raw.manualStatus as ManualStatus | undefined,
repoName: raw.name,
repoLink: raw.repoUrl,
repoDescription: raw.description,
repoAuthor: primaryAuthor?.name ?? "",
repoAuthorLink: primaryAuthor?.link ?? "",
isInactive: status === "inactive" || status === "stale",
isArchived: status === "archived",
};
}

function convertToJSON(repositories: string[]) {
return repositories.map((repository) => {
const $ = cheerio.load(repository);
const getData = cache((): Project[] =>
(projectsData as (typeof projectsData)[number][]).map(normalizeProject)
);

// Extract text content and href from <a> element
const repoName = $("a").first().text();
const repoLink = $("a").first().attr("href");
export const useProjects = () => {
const data = getData();

// Status of the repo
const status = $("span").first().text();
const filterProjects = () => ({
byName: (input: string) => {
const q = input.toLocaleLowerCase();
return data.filter((p) => p.repoName.toLocaleLowerCase().includes(q));
},

const isInactive = status?.includes("Inactive");
const isArchived = status?.includes("Archived");
byAuthor: (input: string) => {
const normalized = input.trim().toLocaleLowerCase().replace(/^@/, "");
const q = `@${normalized}`;
return data.filter((p) => p.repoAuthor.toLocaleLowerCase().includes(q));
},

// @ts-ignore
let description = $("*").contents()[3].data; // I don't know why the fuck this works but if it's not broken, don't touch it.
const repoDescription = description.replace(/^ - /, "");
const repoAuthor = $("strong a").text();
const repoAuthorLink = $("strong a").attr("href");
byLetter: (input: string) => {
const q = input.toLocaleLowerCase();
return data.filter((p) => p.repoName.toLocaleLowerCase().startsWith(q));
},

// Create JSON object
return {
repoName,
repoLink,
repoDescription,
repoAuthor,
repoAuthorLink,
isInactive,
isArchived,
};
byStatus: (status: ProjectStatus) =>
data.filter((p) => p.computed?.status === status),
});
}

export const useProjects = async () => {
const data = await getData();

const filterProjects = () => {
return {
byName: (input: string) => {
return data.filter(
(projects) =>
projects.repoName
.toLocaleLowerCase()
.includes(input.toLocaleLowerCase()) ||
projects.repoName.toLocaleLowerCase() === input.toLocaleLowerCase()
);
},

byAuthor: (input: string) => {
return data.filter(
(projects) =>
projects.repoAuthor.toLocaleLowerCase() ===
`@${input.toLocaleLowerCase()}` ||
projects.repoAuthor
.toLocaleLowerCase()
.includes(`@${input.toLocaleLowerCase()}`)
);
},

byLetter: (input: string) => {
return data.filter((projects) =>
projects.repoName
.toLocaleLowerCase()
.startsWith(input.toLocaleLowerCase())
);
},
};
};

return {
projects: data,
filterProjects,
};
return { projects: data, filterProjects };
};
77 changes: 59 additions & 18 deletions contributing.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,77 @@
# Made in Nigeria Github collection Guidelines
# Made in Nigeria — Contribution Guide

## Contributing a Project
## What qualifies a project for inclusion?
To be included in the Made in Nigeria list, a project must meet the following criteria:

To add a new project to the collection, please ensure they meet the following requirements arranged in order of importance
- **Made in Nigeria** — created or primarily maintained by a Nigerian developer.
- **Open source** — publicly available source code.
- **Global use** — not limited to a Nigerian audience or use case.
- **At least 20 GitHub stars** — a signal that others find it useful.
- **Not a list** — curated lists (alone) of links don't qualify.
- Provide a social media or personal site link for the author(s) - outside GitHub is preferred.

* The project must be **made in Nigeria** as the name implies :grin:.

* The project must be open source.
## Adding a project
Open `data/projects.json` and add a new entry anywhere in the array (it will be sorted alphabetically on merge):

* The project must have global use - meaning it's not made just for use by Nigerians and there really is no geographical limitation for anyone that may be interested in using this project.
```json
{
"name": "Your Project Name",
"repoUrl": "https://github.com/your-username/your-repo",
"description": "One or two sentences describing what the project does.",
"authors": [
{ "name": "@yourhandle", "link": "https://twitter.com/yourhandle" }
]
}
```

* The project should have at least 20 stars. This is to serve as a way to determine that people actually have a use for the project and it does what it says it does. [ Stars likely to go up as time goes on :smiley: ].
That's all. The automated pipeline will:
- validate your entry on the PR
- fetch real GitHub data (stars, last push, language) on the next weekly run
- regenerate `README.MD` automatically after your PR is merged

* The project should not be a list of some sort.
### Multiple authors

* Ensure to provide a social media url of the creator of the project outside GitHub.
```json
"authors": [
{ "name": "@alice", "link": "https://twitter.com/alice" },
{ "name": "@bob", "link": "https://github.com/bob" }
]
```

* Try to add the project to it's appropriate alphabetical location.
### Optional: manual status

* You might want to include the **made in Nigeria** badge to your project. Place the following code in your **README** file:
`[![Made in Nigeria](https://img.shields.io/badge/made%20in-nigeria-008751.svg?style=flat-square)](https://github.com/acekyd/made-in-nigeria)`
If you know a project has been deprecated or deleted, you can include:

More requirements may be added as time goes on.
```json
"manualStatus": "deprecated"
```

Valid values: `inactive`, `archived`, `deprecated`, `deleted`.
Leave it out for active projects — the weekly enrichment job infers status automatically from GitHub activity.

## Contributing to Repo showcase website
---

There is a webpage that has been developed by awesome members of the community used to view the contents of this repo in a very pleasing manner. If you'd like to contribute to this website, please try to follow these steps:
## How status is tracked automatically

* If you are fixing a bug or adding a new feature, please create a branch for it.
A scheduled GitHub Action runs every Monday, queries the GitHub API for every project, and writes computed data to `data/projects.enriched.json`:

* Make Pull requests as descriptive as possible.
| Condition | Computed status |
|---|---|
| GitHub repo is archived | `archived` |
| No push in > 2 years | `inactive` |
| No push in > 6 months | `stale` |
| Active | `active` |
| Non-GitHub or API error | `unknown` |

* Every other best practice recommended when contributing to a project.
`manualStatus` always overrides the computed status for `archived`, `deprecated`, and `deleted`.

---

## Contributing to the website

- Create a branch for each bug fix or feature.
- Make pull requests as descriptive as possible.
- Run `npm run build` and check for errors before opening a PR.
- Remove all `console.log` statements.
- For UI changes, include screenshots or a short recording.
Loading
Loading