|
1 | | -import { marked } from "marked"; |
2 | | -import * as cheerio from "cheerio"; |
3 | 1 | import { cache } from "react"; |
| 2 | +import projectsData from "../../data/projects.enriched.json"; |
| 3 | + |
| 4 | +export type ProjectStatus = |
| 5 | + | "active" |
| 6 | + | "stale" |
| 7 | + | "inactive" |
| 8 | + | "archived" |
| 9 | + | "deprecated" |
| 10 | + | "deleted" |
| 11 | + | "unknown"; |
| 12 | + |
| 13 | +export type ManualStatus = "inactive" | "archived" | "deprecated" | "deleted"; |
| 14 | + |
| 15 | +export type Author = { |
| 16 | + name: string; |
| 17 | + link: string; |
| 18 | +}; |
4 | 19 |
|
5 | | -const getData = cache(async () => { |
6 | | - const res = await fetch( |
7 | | - "https://raw.githubusercontent.com/acekyd/made-in-nigeria/main/README.MD", |
8 | | - { next: { revalidate: false } } |
9 | | - ); |
10 | | - |
11 | | - if (!res.ok) { |
12 | | - // This will activate the closest `error.js` Error Boundary |
13 | | - throw new Error("Failed to fetch data"); |
14 | | - } |
15 | | - |
16 | | - const markdownData = await res.text(); |
17 | | - |
18 | | - // const markdownData = await fs.readFile(process.cwd() + '/README.MD', 'utf8'); |
19 | | - |
20 | | - const html = marked(markdownData); |
21 | | - const $ = cheerio.load(html); // load the html string into cheerio |
22 | | - |
23 | | - // Select all <li> elements using jQuery-like syntax and extract their text |
24 | | - const liTextArray = $("li") |
25 | | - .map((index, element) => $(element).html()) |
26 | | - .get(); |
| 20 | +export type Project = { |
| 21 | + name: string; |
| 22 | + repoUrl: string; |
| 23 | + description: string; |
| 24 | + authors: Author[]; |
| 25 | + manualStatus?: ManualStatus; |
| 26 | + computed?: { |
| 27 | + status: ProjectStatus; |
| 28 | + stars: number | null; |
| 29 | + lastPushed: string | null; |
| 30 | + language: string | null; |
| 31 | + checkedAt: string; |
| 32 | + error?: string; |
| 33 | + }; |
| 34 | + // Legacy shape — keeps existing components working without changes |
| 35 | + repoName: string; |
| 36 | + repoLink: string; |
| 37 | + repoDescription: string; |
| 38 | + repoAuthor: string; |
| 39 | + repoAuthorLink: string; |
| 40 | + isInactive: boolean; |
| 41 | + isArchived: boolean; |
| 42 | +}; |
27 | 43 |
|
28 | | - // process the text to get the data you want |
29 | | - const repositories = convertToJSON(liTextArray); |
| 44 | +function normalizeProject(raw: (typeof projectsData)[number]): Project { |
| 45 | + const primaryAuthor = raw.authors?.[0]; |
| 46 | + const status = raw.computed?.status as ProjectStatus; |
30 | 47 |
|
31 | | - return repositories; |
32 | | -}); |
| 48 | + return { |
| 49 | + ...raw, |
| 50 | + authors: (raw.authors ?? []) as Author[], |
| 51 | + computed: raw.computed as Project["computed"], |
| 52 | + manualStatus: raw.manualStatus as ManualStatus | undefined, |
| 53 | + repoName: raw.name, |
| 54 | + repoLink: raw.repoUrl, |
| 55 | + repoDescription: raw.description, |
| 56 | + repoAuthor: primaryAuthor?.name ?? "", |
| 57 | + repoAuthorLink: primaryAuthor?.link ?? "", |
| 58 | + isInactive: status === "inactive" || status === "stale", |
| 59 | + isArchived: status === "archived", |
| 60 | + }; |
| 61 | +} |
33 | 62 |
|
34 | | -function convertToJSON(repositories: string[]) { |
35 | | - return repositories.map((repository) => { |
36 | | - const $ = cheerio.load(repository); |
| 63 | +const getData = cache((): Project[] => |
| 64 | + (projectsData as (typeof projectsData)[number][]).map(normalizeProject) |
| 65 | +); |
37 | 66 |
|
38 | | - // Extract text content and href from <a> element |
39 | | - const repoName = $("a").first().text(); |
40 | | - const repoLink = $("a").first().attr("href"); |
| 67 | +export const useProjects = () => { |
| 68 | + const data = getData(); |
41 | 69 |
|
42 | | - // Status of the repo |
43 | | - const status = $("span").first().text(); |
| 70 | + const filterProjects = () => ({ |
| 71 | + byName: (input: string) => { |
| 72 | + const q = input.toLocaleLowerCase(); |
| 73 | + return data.filter((p) => p.repoName.toLocaleLowerCase().includes(q)); |
| 74 | + }, |
44 | 75 |
|
45 | | - const isInactive = status?.includes("Inactive"); |
46 | | - const isArchived = status?.includes("Archived"); |
| 76 | + byAuthor: (input: string) => { |
| 77 | + const normalized = input.trim().toLocaleLowerCase().replace(/^@/, ""); |
| 78 | + const q = `@${normalized}`; |
| 79 | + return data.filter((p) => p.repoAuthor.toLocaleLowerCase().includes(q)); |
| 80 | + }, |
47 | 81 |
|
48 | | - // @ts-ignore |
49 | | - let description = $("*").contents()[3].data; // I don't know why the fuck this works but if it's not broken, don't touch it. |
50 | | - const repoDescription = description.replace(/^ - /, ""); |
51 | | - const repoAuthor = $("strong a").text(); |
52 | | - const repoAuthorLink = $("strong a").attr("href"); |
| 82 | + byLetter: (input: string) => { |
| 83 | + const q = input.toLocaleLowerCase(); |
| 84 | + return data.filter((p) => p.repoName.toLocaleLowerCase().startsWith(q)); |
| 85 | + }, |
53 | 86 |
|
54 | | - // Create JSON object |
55 | | - return { |
56 | | - repoName, |
57 | | - repoLink, |
58 | | - repoDescription, |
59 | | - repoAuthor, |
60 | | - repoAuthorLink, |
61 | | - isInactive, |
62 | | - isArchived, |
63 | | - }; |
| 87 | + byStatus: (status: ProjectStatus) => |
| 88 | + data.filter((p) => p.computed?.status === status), |
64 | 89 | }); |
65 | | -} |
66 | | - |
67 | | -export const useProjects = async () => { |
68 | | - const data = await getData(); |
69 | | - |
70 | | - const filterProjects = () => { |
71 | | - return { |
72 | | - byName: (input: string) => { |
73 | | - return data.filter( |
74 | | - (projects) => |
75 | | - projects.repoName |
76 | | - .toLocaleLowerCase() |
77 | | - .includes(input.toLocaleLowerCase()) || |
78 | | - projects.repoName.toLocaleLowerCase() === input.toLocaleLowerCase() |
79 | | - ); |
80 | | - }, |
81 | | - |
82 | | - byAuthor: (input: string) => { |
83 | | - return data.filter( |
84 | | - (projects) => |
85 | | - projects.repoAuthor.toLocaleLowerCase() === |
86 | | - `@${input.toLocaleLowerCase()}` || |
87 | | - projects.repoAuthor |
88 | | - .toLocaleLowerCase() |
89 | | - .includes(`@${input.toLocaleLowerCase()}`) |
90 | | - ); |
91 | | - }, |
92 | 90 |
|
93 | | - byLetter: (input: string) => { |
94 | | - return data.filter((projects) => |
95 | | - projects.repoName |
96 | | - .toLocaleLowerCase() |
97 | | - .startsWith(input.toLocaleLowerCase()) |
98 | | - ); |
99 | | - }, |
100 | | - }; |
101 | | - }; |
102 | | - |
103 | | - return { |
104 | | - projects: data, |
105 | | - filterProjects, |
106 | | - }; |
| 91 | + return { projects: data, filterProjects }; |
107 | 92 | }; |
0 commit comments