Skip to content
Open
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
11 changes: 4 additions & 7 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ const config: Config = {

markdown: {
mermaid: true,
hooks: {
onBrokenMarkdownLinks: "warn",
},
},

// Migrated legacy setting to markdown.hooks.onBrokenMarkdownLinks
Expand All @@ -285,18 +288,12 @@ const config: Config = {
],
],

// ✅ Add this customFields object to expose the token to the client-side
customFields: {
gitToken: process.env.DOCUSAURUS_GIT_TOKEN,
// Shopify credentials for merch store
SHOPIFY_STORE_DOMAIN:
process.env.SHOPIFY_STORE_DOMAIN || "junh9v-gw.myshopify.com",
SHOPIFY_STOREFRONT_ACCESS_TOKEN:
process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN ||
"2503dfbf93132b42e627e7d53b3ba3e9",
hooks: {
onBrokenMarkdownLinks: "warn",
},
process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN,
},
};

Expand Down
31 changes: 11 additions & 20 deletions src/lib/statsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import React, {
type ReactNode,
} from "react";
import { githubService, type GitHubOrgStats } from "../services/githubService";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";

// Time filter types
export type TimeFilter = "week" | "month" | "year" | "all";
Expand All @@ -22,7 +21,7 @@ interface ICommunityStatsContext {
githubForksCountText: string;
githubReposCount: number;
githubReposCountText: string;
githubDiscussionsCount: number;
githubDiscussionsCount: number | null;
githubDiscussionsCountText: string;
loading: boolean;
error: string | null;
Expand Down Expand Up @@ -160,18 +159,15 @@ const isPRInTimeRange = (mergedAt: string, filter: TimeFilter): boolean => {
export function CommunityStatsProvider({
children,
}: CommunityStatsProviderProps) {
const {
siteConfig: { customFields },
} = useDocusaurusContext();
const token = customFields?.gitToken || "";

const [loading, setLoading] = useState(false); // Start with false to avoid hourglass
const [error, setError] = useState<string | null>(null);
const [githubStarCount, setGithubStarCount] = useState(984); // Placeholder value - updated to match production
const [githubContributorsCount, setGithubContributorsCount] = useState(467); // Placeholder value - updated to match production
const [githubForksCount, setGithubForksCount] = useState(1107); // Placeholder value - updated to match production
const [githubReposCount, setGithubReposCount] = useState(10); // Placeholder value - updated to match production
const [githubDiscussionsCount, setGithubDiscussionsCount] = useState(0);
const [githubDiscussionsCount, setGithubDiscussionsCount] = useState<
number | null
>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);

// Time filter state
Expand Down Expand Up @@ -433,17 +429,8 @@ export function CommunityStatsProvider({

setError(null);

if (!token) {
setError(
"GitHub token not found. Please set customFields.gitToken in docusaurus.config.js.",
);
setLoading(false);
return;
}

try {
const headers: Record<string, string> = {
Authorization: `token ${token}`,
Accept: "application/vnd.github.v3+json",
};

Expand Down Expand Up @@ -491,13 +478,13 @@ export function CommunityStatsProvider({
setGithubContributorsCount(140);
setGithubForksCount(0);
setGithubReposCount(20);
setGithubDiscussionsCount(0);
setGithubDiscussionsCount(null);
}
} finally {
setLoading(false);
}
},
[token, fetchAllOrgRepos, processBatch, cache],
[fetchAllOrgRepos, processBatch, cache],
);

const clearCache = useCallback(() => {
Expand Down Expand Up @@ -577,7 +564,11 @@ export const useCommunityStatsContext = (): ICommunityStatsContext => {
return context;
};

export const convertStatToText = (num: number): string => {
export const convertStatToText = (num: number | null): string => {
if (num === null) {
return "N/A";
}

const hasIntlSupport =
typeof Intl === "object" && Intl && typeof Intl.NumberFormat === "function";

Expand Down
1 change: 1 addition & 0 deletions src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const DashboardContent: React.FC = () => {
setDiscussions(discussionsData);
} catch (error) {
console.error("Failed to fetch discussions:", error);
setDiscussions([]);
setDiscussionsError(
error instanceof Error ? error.message : "Failed to load discussions",
);
Expand Down
69 changes: 49 additions & 20 deletions src/services/githubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface GitHubOrgStats {
totalRepositories: number;
totalContributors: number;
publicRepositories: number;
discussionsCount: number;
discussionsCount: number | null;
lastUpdated: number;
}

Expand Down Expand Up @@ -64,6 +64,8 @@ class GitHubService {
private readonly CACHE_KEY = "github_org_stats";
private readonly CACHE_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
private readonly BASE_URL = "https://api.github.com";
private readonly DISCUSSIONS_UNAVAILABLE_MESSAGE =
"GitHub Discussions are disabled until a server-side GitHub proxy is configured.";

// === ADDED: include anonymous contributors configurable (default false)
private includeAnonymousContributors = false;
Expand Down Expand Up @@ -96,6 +98,31 @@ class GitHubService {
return headers;
}

private canUseGitHubGraphQL(): boolean {
return typeof window === "undefined";
}

private getGitHubToken(): string | null {
if (typeof window !== "undefined") {
return null;
}

return process.env.GITHUB_TOKEN?.trim() || this.token || null;
}

private getGraphQLHeaders(): Record<string, string> {
const token = this.getGitHubToken();

if (!token) {
throw new Error(this.DISCUSSIONS_UNAVAILABLE_MESSAGE);
}

return {
...this.getHeaders(),
Authorization: `Bearer ${token}`,
};
}

// === ADDED: setter to toggle anonymous contributors inclusion
setIncludeAnonymousContributors(value: boolean) {
this.includeAnonymousContributors = value;
Expand Down Expand Up @@ -287,16 +314,16 @@ class GitHubService {
return totalContributors;
}

// === UPDATED: Get discussions count for a specific repository (default: "Support")
// Reason: previous code used an org-wide issues search which returned issues, not discussions.
// This function uses GraphQL to read repository.discussions.totalCount (repo-specific).
// If you need org-wide discussions count, we should iterate all repos and sum totalCount (heavier).
// GitHub GraphQL requires authentication, so the browser should not call it directly.
private async getDiscussionsCount(
signal?: AbortSignal,
repoName: string = "Support",
): Promise<number> {
): Promise<number | null> {
if (!this.canUseGitHubGraphQL()) {
return null;
}
Comment on lines +317 to +324
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

getDiscussionsCount() returns 0 when GraphQL is unavailable in the browser. That makes downstream UI/reporting treat the value as a real count ("0 discussions") rather than "unavailable", which is misleading. Consider representing this as null/undefined (and updating GitHubOrgStats + UI), or surfacing an explicit "unavailable" flag/message instead of overloading 0.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Abhash-Chakraborty this can be doable


try {
// GraphQL query to get discussions totalCount for a repository
const query = `
query ($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
Expand All @@ -309,7 +336,7 @@ class GitHubService {
const resp = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
...this.getHeaders(),
...this.getGraphQLHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
Expand All @@ -318,20 +345,20 @@ class GitHubService {

if (!resp.ok) {
console.warn(`GraphQL request for discussions failed: ${resp.status}`);
return 0;
return null;
}

const data = await resp.json();
if (data.errors) {
console.warn("GraphQL errors while fetching discussions:", data.errors);
return 0;
return null;
}

const count = data?.data?.repository?.discussions?.totalCount || 0;
return Number(count);
} catch (error) {
console.warn("Error fetching discussions count via GraphQL:", error);
return 0;
return null;
}
}

Expand Down Expand Up @@ -363,11 +390,10 @@ class GitHubService {
0,
);

// Estimate contributors and get discussions count
// === UPDATED: getDiscussionsCount now uses GraphQL for a specific repo (default 'Support')
// Estimate contributors and fetch discussion stats when a server-side context is available.
const [totalContributors, discussionsCount] = await Promise.all([
this.estimateContributors(activeRepos, signal),
this.getDiscussionsCount(signal), // default repoName: "Support" (change if you prefer another repo)
this.getDiscussionsCount(signal),
]);

const stats: GitHubOrgStats = {
Expand All @@ -394,7 +420,7 @@ class GitHubService {
totalRepositories: 0,
publicRepositories: 0,
totalContributors: 0,
discussionsCount: 0,
discussionsCount: null,
lastUpdated: Date.now(),
};

Expand Down Expand Up @@ -422,11 +448,16 @@ class GitHubService {
return { cached: true, age, expiresIn };
}

// Fetch GitHub Discussions using GraphQL API (existing method kept intact)
async fetchDiscussions(
limit: number = 20,
signal?: AbortSignal,
): Promise<GitHubDiscussion[]> {
if (!this.canUseGitHubGraphQL()) {
throw new Error(this.DISCUSSIONS_UNAVAILABLE_MESSAGE);
}

Comment on lines +455 to +458
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

Even when canUseGitHubGraphQL() is true, fetchDiscussions() still calls https://api.github.com/graphql directly without any auth header. If/when this is invoked in a server-side context, it will still fail with 401 unless you route through an authenticated proxy or add server-side auth (e.g., from env). Consider failing fast with a clear configuration error when no server-side token/proxy is available, rather than attempting the call.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

const graphqlHeaders = this.getGraphQLHeaders();

const query = `
query GetDiscussions($owner: String!, $name: String!, $first: Int!) {
repository(owner: $owner, name: $name) {
Expand Down Expand Up @@ -475,7 +506,7 @@ class GitHubService {
const response = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
...this.getHeaders(),
...graphqlHeaders,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
Expand Down Expand Up @@ -525,9 +556,7 @@ class GitHubService {
);
} catch (error) {
console.error("Error fetching discussions:", error);

// Return mock data for development/fallback
return this.getMockDiscussions();
throw error;
}
}

Expand Down
25 changes: 7 additions & 18 deletions wiki/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,13 @@ const isPRInTimeRange = (mergedAt: string, filter: TimeFilter): boolean => {
const prDate = new Date(mergedAt);
return prDate >= filterDate;
};
```

Computed Contributors
This is where React's useMemo shines:
typescriptconst contributors = useMemo(() => {

```typescript
const contributors = useMemo(() => {
if (!allContributors.length) return [];

const filteredContributors = allContributors
Expand Down Expand Up @@ -573,7 +577,7 @@ Response Example:
}
```
#### Authentication
All requests require a GitHub Personal Access Token:
Authenticated requests should be made from a server-side endpoint or serverless function so the token is never shipped to the browser:
```typescript
const headers: Record<string, string> = {
Authorization: `token ${YOUR_GITHUB_TOKEN}`,
Expand All @@ -588,22 +592,7 @@ Select scopes: public_repo, read:org
Copy the token (you won't see it again!)

#### Storing the Token:
In Docusaurus, we store it in docusaurus.config.js:
```javascript
module.exports = {
customFields: {
gitToken: process.env.GITHUB_TOKEN || '',
},
// ...
};
```
Then access it:
```typescript
const {
siteConfig: { customFields },
} = useDocusaurusContext();
const token = customFields?.gitToken || "";
```
Do not store a GitHub token in `docusaurus.config.js` or any other client-bundled config. Keep it in server-side environment variables and call GitHub from a backend endpoint instead.
#### Error Handling
**Rate Limit Exceeded (403)**

Expand Down
Loading