Skip to content

Commit 650321b

Browse files
committed
docs: implement proper documentation
1 parent 52bbb36 commit 650321b

96 files changed

Lines changed: 10264 additions & 559 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# deps
2+
/node_modules
3+
4+
# generated content
5+
.source
6+
7+
# test & build
8+
/coverage
9+
/.next/
10+
/out/
11+
/build
12+
*.tsbuildinfo
13+
14+
# misc
15+
.DS_Store
16+
*.pem
17+
/.pnp
18+
.pnp.js
19+
npm-debug.log*
20+
yarn-debug.log*
21+
yarn-error.log*
22+
23+
# others
24+
.env*.local
25+
.vercel
26+
next-env.d.ts

docs/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# docs
2+
3+
This is a Next.js application generated with
4+
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
5+
6+
Run development server:
7+
8+
```bash
9+
npm run dev
10+
# or
11+
pnpm dev
12+
# or
13+
yarn dev
14+
```
15+
16+
Open http://localhost:3000 with your browser to see the result.
17+
18+
## Explore
19+
20+
In the project, you can see:
21+
22+
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
23+
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
24+
25+
| Route | Description |
26+
| ------------------------- | ------------------------------------------------------ |
27+
| `app/(home)` | The route group for your landing page and other pages. |
28+
| `app/docs` | The documentation layout and pages. |
29+
| `app/api/search/route.ts` | The Route Handler for search. |
30+
31+
### Fumadocs MDX
32+
33+
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
34+
35+
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
36+
37+
## Learn More
38+
39+
To learn more about Next.js and Fumadocs, take a look at the following
40+
resources:
41+
42+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
43+
features and API.
44+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
45+
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs

docs/app/(home)/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { HomeLayout } from 'fumadocs-ui/layouts/home';
2+
import { baseOptions } from '@/lib/layout.shared';
3+
4+
export default function Layout({ children }: LayoutProps<'/'>) {
5+
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
6+
}

docs/app/(home)/page.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
import { Hero } from '../../components/landing/Hero';
3+
import { FeatureSection } from '../../components/landing/FeatureSection';
4+
5+
export default function HomePage() {
6+
return (
7+
8+
<div className="flex flex-col min-h-screen bg-fd-background">
9+
<Hero />
10+
<FeatureSection />
11+
</div>
12+
13+
);
14+
}

docs/app/api/contributors/route.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export const revalidate = 86400; // Cache response for 24 hours (Maximum rate-limit friendliness)
4+
5+
export async function GET() {
6+
try {
7+
console.log('[API] Fetching contributors from GitHub...');
8+
const headers: Record<string, string> = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {};
9+
10+
const res = await fetch('https://api.github.com/repos/margelo/react-native-quick-crypto/contributors?per_page=100', {
11+
headers,
12+
next: { revalidate: 86400 }
13+
});
14+
15+
if (!res.ok) {
16+
console.error('[API] GitHub Contributors fetch failed:', res.status, res.statusText);
17+
throw new Error(`GitHub API Error: ${res.status} ${res.statusText}`);
18+
}
19+
20+
const data = await res.json();
21+
22+
if (!Array.isArray(data)) return NextResponse.json([]);
23+
24+
const humans = data.filter((c: any) => !c.login.toLowerCase().includes('[bot]'));
25+
26+
return NextResponse.json(humans);
27+
} catch (error) {
28+
console.error('[API] Handler error:', error);
29+
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
30+
}
31+
}

docs/app/api/releases/route.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { NextResponse } from 'next/server';
2+
import { unstable_cache } from 'next/cache';
3+
4+
export const revalidate = 86400; // Cache response for 24 hours (releases don't change often)
5+
6+
function extractContributors(text: string): string[] {
7+
const mentionRegex = /@([a-zA-Z0-9-]+)/g;
8+
const matches = text ? text.match(mentionRegex) : null;
9+
if (!matches) return [];
10+
11+
// Clean up matches, remove duplicates, and filter out common bots/keywords
12+
const uniqueUsers = Array.from(new Set(matches.map(m => m.substring(1)))); // remove @
13+
const banned = ['dependabot', 'github-actions', 'channel', 'here', 'all']; // common non-user mentions
14+
15+
return uniqueUsers.filter(u => !u.includes('[bot]') && !banned.includes(u.toLowerCase()));
16+
}
17+
18+
interface ContributorDetails {
19+
login: string;
20+
name?: string;
21+
avatar_url: string;
22+
html_url: string;
23+
bio?: string;
24+
location?: string;
25+
company?: string;
26+
}
27+
28+
// Persistently cache user details forever (or until manually invalidated)
29+
// This fetches the "real name" and other details from GitHub User API
30+
const getContributorDetails = unstable_cache(
31+
async (login: string): Promise<ContributorDetails | null> => {
32+
try {
33+
console.log(`[API] Fetching user details for ${login}...`);
34+
const headers: Record<string, string> = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {};
35+
const res = await fetch(`https://api.github.com/users/${login}`, { headers });
36+
37+
if (!res.ok) {
38+
if (res.status === 404) return null;
39+
throw new Error(`GitHub User API Error: ${res.status}`);
40+
}
41+
42+
const data = await res.json();
43+
return {
44+
login: data.login,
45+
name: data.name,
46+
avatar_url: data.avatar_url,
47+
html_url: data.html_url,
48+
bio: data.bio,
49+
location: data.location,
50+
company: data.company
51+
};
52+
} catch (e) {
53+
console.error(`[API] Failed to fetch user ${login}:`, e);
54+
// Fallback to basic details if API fails
55+
return {
56+
login,
57+
avatar_url: `https://github.com/${login}.png`,
58+
html_url: `https://github.com/${login}`
59+
};
60+
}
61+
},
62+
['github-user-details'], // Cache key namespace
63+
{
64+
revalidate: false, // Cache forever (never revalidate automatically)
65+
tags: ['contributors']
66+
}
67+
);
68+
69+
export async function GET() {
70+
try {
71+
console.log('[API] Fetching releases from GitHub...');
72+
const headers: Record<string, string> = process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {};
73+
74+
const releasesRes = await fetch('https://api.github.com/repos/margelo/react-native-quick-crypto/releases?per_page=10', {
75+
headers,
76+
next: { revalidate: 86400 }
77+
});
78+
79+
if (!releasesRes.ok) {
80+
console.error('[API] GitHub Releases fetch failed:', releasesRes.status, releasesRes.statusText);
81+
throw new Error(`GitHub API Error: ${releasesRes.status} ${releasesRes.statusText}`);
82+
}
83+
84+
const releases = await releasesRes.json();
85+
if (!Array.isArray(releases)) return NextResponse.json([]);
86+
87+
// Process releases in parallel to fetch contributors
88+
const enhanced = await Promise.all(releases.map(async (release: any, index: number) => {
89+
const previousTag = releases[index + 1]?.tag_name;
90+
const contributorsMap = new Map<string, { login: string, commits: number }>();
91+
92+
// 1. Text mentions (legacy method, but good for shoutouts)
93+
extractContributors(release.body).forEach(user => {
94+
contributorsMap.set(user, { login: user, commits: 0 });
95+
});
96+
97+
// 2. Compare API (if previous tag exists) to get actual committers
98+
if (previousTag) {
99+
try {
100+
const compareUrl = `https://api.github.com/repos/margelo/react-native-quick-crypto/compare/${previousTag}...${release.tag_name}`;
101+
const compareRes = await fetch(compareUrl, { headers, next: { revalidate: 86400 } });
102+
103+
if (compareRes.ok) {
104+
const data = await compareRes.json();
105+
if (data.commits && Array.isArray(data.commits)) {
106+
data.commits.forEach((commit: any) => {
107+
if (commit.author && commit.author.login) {
108+
if (!commit.author.login.includes('[bot]')) {
109+
const login = commit.author.login;
110+
const current = contributorsMap.get(login) || { login, commits: 0 };
111+
current.commits++;
112+
contributorsMap.set(login, current);
113+
}
114+
}
115+
});
116+
}
117+
} else if (compareRes.status === 403 && process.env.NODE_ENV === 'development') {
118+
// Mock data for development when rate limited
119+
console.warn(`[API] Rate limited. Using mock contributors for ${release.tag_name}.`);
120+
['mrousavy', 'szymonkapala', 'ospfranco'].forEach(login => {
121+
contributorsMap.set(login, { login, commits: Math.floor(Math.random() * 5) + 1 });
122+
});
123+
} else {
124+
console.warn(`[API] Compare fetch failed for ${previousTag}...${release.tag_name}: ${compareRes.status}`);
125+
}
126+
} catch (e) {
127+
console.error('[API] Compare fetch error:', e);
128+
}
129+
}
130+
131+
// 3. Hydrate with full user details (Names, Bios, etc.) using persistent cache
132+
const hydratedContributors = await Promise.all(
133+
Array.from(contributorsMap.values()).map(async (c) => {
134+
const details = await getContributorDetails(c.login);
135+
return {
136+
...details,
137+
commits: c.commits
138+
};
139+
})
140+
);
141+
142+
// Sort: High commits first, then by name
143+
const sortedContributors = hydratedContributors
144+
.filter(c => c !== null)
145+
.sort((a: any, b: any) => b.commits - a.commits);
146+
147+
return {
148+
...release,
149+
contributors: sortedContributors
150+
};
151+
}));
152+
153+
return NextResponse.json(enhanced);
154+
} catch (error) {
155+
console.error('[API] Handler error:', error);
156+
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
157+
}
158+
}

docs/app/api/search/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { source } from '@/lib/source';
2+
import { createFromSource } from 'fumadocs-core/search/server';
3+
4+
export const { GET } = createFromSource(source, {
5+
// https://docs.orama.com/docs/orama-js/supported-languages
6+
language: 'english',
7+
});

docs/app/docs/[[...slug]]/page.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { getPageImage, source } from '@/lib/source';
2+
import {
3+
DocsBody,
4+
DocsDescription,
5+
DocsPage,
6+
DocsTitle,
7+
} from 'fumadocs-ui/layouts/docs/page';
8+
import { notFound } from 'next/navigation';
9+
import { getMDXComponents } from '@/mdx-components';
10+
import type { Metadata } from 'next';
11+
import { createRelativeLink } from 'fumadocs-ui/mdx';
12+
13+
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
14+
const params = await props.params;
15+
const page = source.getPage(params.slug);
16+
if (!page) notFound();
17+
18+
const MDX = page.data.body;
19+
20+
return (
21+
<DocsPage toc={page.data.toc} full={page.data.full}>
22+
<DocsTitle>{page.data.title}</DocsTitle>
23+
<DocsDescription>{page.data.description}</DocsDescription>
24+
<DocsBody>
25+
<MDX
26+
components={getMDXComponents({
27+
// this allows you to link to other pages with relative file paths
28+
a: createRelativeLink(source, page),
29+
})}
30+
/>
31+
</DocsBody>
32+
</DocsPage>
33+
);
34+
}
35+
36+
export async function generateStaticParams() {
37+
return source.generateParams();
38+
}
39+
40+
export async function generateMetadata(
41+
props: PageProps<'/docs/[[...slug]]'>,
42+
): Promise<Metadata> {
43+
const params = await props.params;
44+
const page = source.getPage(params.slug);
45+
if (!page) notFound();
46+
47+
return {
48+
title: page.data.title,
49+
description: page.data.description,
50+
openGraph: {
51+
images: getPageImage(page).url,
52+
},
53+
};
54+
}

docs/app/docs/layout.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
import { DocsLayout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
3+
import { baseOptions } from '@/lib/layout.shared';
4+
import { source } from '@/lib/source';
5+
function docsOptions(): DocsLayoutProps {
6+
return {
7+
...baseOptions(),
8+
tree: source.pageTree,
9+
links: [],
10+
};
11+
}
12+
export default function Layout({ children }: { children: React.ReactNode }) {
13+
return <DocsLayout {...docsOptions()}>{children}</DocsLayout>;
14+
}

0 commit comments

Comments
 (0)