From 2005f67fd0a022b5898a37af3de66a2f0af9d4ec Mon Sep 17 00:00:00 2001 From: Sanjay Viswanathan Date: Thu, 30 Apr 2026 17:33:23 +1000 Subject: [PATCH] Revert "fix: stop exposing client-side tokens" --- .env.example | 13 +- api/github-discussions.js | 166 ---------------- docusaurus.config.ts | 14 +- eslint.config.mjs | 3 - package.json | 4 +- src/lib/statsProvider.tsx | 38 ++-- src/pages/dashboard/index.tsx | 39 ++-- src/services/githubService.ts | 336 ++++++++++++++++++++++++++++----- src/types/githubDiscussions.ts | 33 ---- src/types/global.d.ts | 4 - wiki/Documentation.md | 30 +-- 11 files changed, 371 insertions(+), 309 deletions(-) delete mode 100644 api/github-discussions.js delete mode 100644 src/types/githubDiscussions.ts diff --git a/.env.example b/.env.example index b58783fc..889769b4 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,12 @@ -# GitHub API Configuration -# Used by the server-side /api/github-discussions endpoint. -# Prefer GITHUB_TOKEN for new setups. DOCUSAURUS_GIT_TOKEN is still supported as a legacy fallback. -# Create a Classic PAT at: https://github.com/settings/tokens -# Recommended scopes: public_repo, read:org, read:discussion +# GitHub API Configuration (Optional) +# To avoid rate limits, you can add a GitHub Personal Access Token +# Create one at: https://github.com/settings/tokens +# No special permissions needed for public repositories GITHUB_TOKEN=your_github_token_here -# Legacy fallback for existing deployments. Optional if GITHUB_TOKEN is already set. +# GitHub token used by Docusaurus for dynamic features (discussions, stats, leaderboard) +# This must be set for the discussions section to fetch live data from GitHub +# Create a Classic PAT with read:discussion scope at https://github.com/settings/tokens DOCUSAURUS_GIT_TOKEN=your_github_token_here # Shopify Configuration (for Merch Store) diff --git a/api/github-discussions.js b/api/github-discussions.js deleted file mode 100644 index 1333951b..00000000 --- a/api/github-discussions.js +++ /dev/null @@ -1,166 +0,0 @@ -const ORG_NAME = "recodehive"; -const DISCUSSIONS_REPO = "recode-website"; -const DEFAULT_LIMIT = 20; -const UNAVAILABLE_MESSAGE = - "GitHub Discussions are available only when a server-side GITHUB_TOKEN or DOCUSAURUS_GIT_TOKEN is configured."; - -function getToken() { - return process.env.GITHUB_TOKEN?.trim() || process.env.DOCUSAURUS_GIT_TOKEN?.trim() || ""; -} - -function parseLimit(limitParam) { - const parsed = Number.parseInt(limitParam, 10); - if (!Number.isFinite(parsed) || parsed <= 0) { - return DEFAULT_LIMIT; - } - - return Math.min(parsed, 50); -} - -function mapDiscussion(discussion) { - return { - id: discussion.id, - title: discussion.title || "Untitled discussion", - body: discussion.body || "", - author: { - login: discussion.author?.login || "Unknown", - avatar_url: discussion.author?.avatarUrl || "", - html_url: discussion.author?.url || "", - }, - category: { - name: discussion.category?.name || "General", - emoji: discussion.category?.emoji || "", - }, - created_at: discussion.createdAt, - updated_at: discussion.updatedAt, - comments: discussion.comments?.totalCount || 0, - reactions: { - total_count: discussion.reactions?.totalCount || 0, - }, - html_url: discussion.url, - labels: - discussion.labels?.nodes?.map((label) => ({ - name: label.name, - color: label.color, - })) || [], - }; -} - -async function fetchGitHubDiscussions(token, limit) { - const query = ` - query GetDiscussions($owner: String!, $name: String!, $first: Int!) { - repository(owner: $owner, name: $name) { - discussions(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { - totalCount - nodes { - id - title - body - createdAt - updatedAt - url - author { - login - avatarUrl - url - } - category { - name - emoji - } - comments { - totalCount - } - reactions { - totalCount - } - labels(first: 10) { - nodes { - name - color - } - } - } - } - } - } - `; - - const response = await fetch("https://api.github.com/graphql", { - method: "POST", - headers: { - Accept: "application/vnd.github.v3+json", - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query, - variables: { - owner: ORG_NAME, - name: DISCUSSIONS_REPO, - first: limit, - }, - }), - }); - - if (!response.ok) { - throw new Error(`GitHub discussions request failed: ${response.status}`); - } - - const payload = await response.json(); - - if (payload.errors?.length) { - const message = payload.errors.map((error) => error.message).join(", "); - throw new Error(message || "GitHub discussions GraphQL query failed"); - } - - const discussions = payload.data?.repository?.discussions; - - return { - available: true, - message: null, - totalCount: discussions?.totalCount ?? 0, - discussions: (discussions?.nodes || []).map(mapDiscussion), - fetchedAt: new Date().toISOString(), - }; -} - -async function handler(req, res) { - const token = getToken(); - - if (!token) { - res.setHeader("Cache-Control", "no-store"); - res.status(503).json({ - available: false, - message: UNAVAILABLE_MESSAGE, - totalCount: null, - discussions: [], - fetchedAt: null, - }); - return; - } - - try { - const limit = parseLimit(req.query.limit); - const data = await fetchGitHubDiscussions(token, limit); - res.setHeader( - "Cache-Control", - "public, s-maxage=300, stale-while-revalidate=600", - ); - res.status(200).json(data); - } catch (error) { - res.setHeader("Cache-Control", "no-store"); - res.status(502).json({ - available: false, - message: - error instanceof Error - ? error.message - : "Failed to fetch GitHub discussions.", - totalCount: null, - discussions: [], - fetchedAt: null, - }); - } -} - -module.exports = handler; diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 78a69c27..7d7b5bce 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -19,6 +19,7 @@ const config: Config = { projectName: "recode-website", onBrokenLinks: "throw", + // onBrokenMarkdownLinks moved to markdown.hooks // Google Analytics and Theme Scripts scripts: [ @@ -264,11 +265,10 @@ const config: Config = { markdown: { mermaid: true, - hooks: { - onBrokenMarkdownLinks: "warn", - }, }, + // Migrated legacy setting to markdown.hooks.onBrokenMarkdownLinks + themes: ["@docusaurus/theme-mermaid"], plugins: [ @@ -284,16 +284,22 @@ 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, + process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || + "2503dfbf93132b42e627e7d53b3ba3e9", // EmailJS credentials for Contact Us page (public values only) EMAILJS_PUBLIC_KEY: process.env.EMAILJS_PUBLIC_KEY || "", EMAILJS_SERVICE_ID: process.env.EMAILJS_SERVICE_ID || "", EMAILJS_TEMPLATE_ID: process.env.EMAILJS_TEMPLATE_ID || "", + hooks: { + onBrokenMarkdownLinks: "warn", + }, }, }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 63fe7815..a8b98b3c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,9 +4,6 @@ import tsPlugin from "@typescript-eslint/eslint-plugin"; import reactPlugin from "eslint-plugin-react"; export default [ - { - ignores: ["node_modules/", "build/", ".docusaurus/", "static/", "dist/"], - }, { files: ["**/*.{ts,tsx}"], ignores: ["node_modules/", "build/", ".docusaurus/", "static/", "dist/"], diff --git a/package.json b/package.json index 2e6e4cdc..2a2c0ca3 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css}\"", "format:check": "prettier --check .", "prepare": "husky" diff --git a/src/lib/statsProvider.tsx b/src/lib/statsProvider.tsx index 51d57a00..923c16c5 100644 --- a/src/lib/statsProvider.tsx +++ b/src/lib/statsProvider.tsx @@ -7,7 +7,8 @@ import React, { useState, type ReactNode, } from "react"; -import { githubService } from "../services/githubService"; +import { githubService, type GitHubOrgStats } from "../services/githubService"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // Time filter types export type TimeFilter = "week" | "month" | "year" | "all"; @@ -21,7 +22,7 @@ interface ICommunityStatsContext { githubForksCountText: string; githubReposCount: number; githubReposCountText: string; - githubDiscussionsCount: number | null; + githubDiscussionsCount: number; githubDiscussionsCountText: string; loading: boolean; error: string | null; @@ -159,15 +160,18 @@ 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(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< - number | null - >(null); + const [githubDiscussionsCount, setGithubDiscussionsCount] = useState(0); const [lastUpdated, setLastUpdated] = useState(null); // Time filter state @@ -429,16 +433,24 @@ 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 = { + Authorization: `token ${token}`, Accept: "application/vnd.github.v3+json", }; // Fetch both org stats and repos in parallel - const [orgStats, repos, discussionsCount] = await Promise.all([ + const [orgStats, repos] = await Promise.all([ githubService.fetchOrganizationStats(signal), fetchAllOrgRepos(headers), - githubService.fetchDiscussionsCount(signal), ]); // Set org stats immediately @@ -446,7 +458,7 @@ export function CommunityStatsProvider({ setGithubContributorsCount(orgStats.totalContributors); setGithubForksCount(orgStats.totalForks); setGithubReposCount(orgStats.publicRepositories); - setGithubDiscussionsCount(discussionsCount ?? orgStats.discussionsCount); + setGithubDiscussionsCount(orgStats.discussionsCount); setLastUpdated(new Date(orgStats.lastUpdated)); // Process leaderboard data with concurrent processing @@ -479,13 +491,13 @@ export function CommunityStatsProvider({ setGithubContributorsCount(140); setGithubForksCount(0); setGithubReposCount(20); - setGithubDiscussionsCount(null); + setGithubDiscussionsCount(0); } } finally { setLoading(false); } }, - [fetchAllOrgRepos, processBatch, cache], + [token, fetchAllOrgRepos, processBatch, cache], ); const clearCache = useCallback(() => { @@ -565,11 +577,7 @@ export const useCommunityStatsContext = (): ICommunityStatsContext => { return context; }; -export const convertStatToText = (num: number | null): string => { - if (num === null) { - return "N/A"; - } - +export const convertStatToText = (num: number): string => { const hasIntlSupport = typeof Intl === "object" && Intl && typeof Intl.NumberFormat === "function"; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 8305cdcc..fdb55cc9 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { JSX, useEffect, useState } from "react"; import Layout from "@theme/Layout"; import Head from "@docusaurus/Head"; import BrowserOnly from "@docusaurus/BrowserOnly"; @@ -9,12 +9,12 @@ import { } from "@site/src/lib/statsProvider"; import SlotCounter from "react-slot-counter"; import { useLocation, useHistory } from "@docusaurus/router"; -import DiscussionCard from "@site/src/components/discussions/DiscussionCard"; -import { githubService } from "@site/src/services/githubService"; -import type { +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { + githubService, GitHubDiscussion, - GitHubDiscussionsResponse, -} from "../../types/githubDiscussions"; +} from "@site/src/services/githubService"; +import DiscussionCard from "@site/src/components/discussions/DiscussionCard"; import { Megaphone, Lightbulb, @@ -57,6 +57,9 @@ const categories: Category[] = [ const DashboardContent: React.FC = () => { const location = useLocation(); const history = useHistory(); + const { + siteConfig: { customFields }, + } = useDocusaurusContext(); const [activeTab, setActiveTab] = useState< "home" | "discuss" | "giveaway" | "contributors" >("home"); @@ -72,6 +75,14 @@ const DashboardContent: React.FC = () => { const [discussionsError, setDiscussionsError] = useState(null); const [showDashboardMenu, setShowDashboardMenu] = useState(false); + // Initialize GitHub service with token from Docusaurus config + useEffect(() => { + const token = customFields?.gitToken as string; + if (token) { + githubService.setToken(token); + } + }, [customFields?.gitToken]); + // Close dashboard menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -107,6 +118,7 @@ const DashboardContent: React.FC = () => { } }, [location]); + // Fetch discussions when discuss tab is active useEffect(() => { if (activeTab === "discuss") { fetchDiscussions(); @@ -117,17 +129,10 @@ const DashboardContent: React.FC = () => { try { setDiscussionsLoading(true); setDiscussionsError(null); - - const data = (await githubService.fetchDiscussions( - 20, - )) as GitHubDiscussionsResponse; - - setDiscussions(data.discussions ?? []); - setDiscussionsError( - data.available ? null : data.message || "Failed to load discussions", - ); + const discussionsData = await githubService.fetchDiscussions(20); + setDiscussions(discussionsData); } catch (error) { - setDiscussions([]); + console.error("Failed to fetch discussions:", error); setDiscussionsError( error instanceof Error ? error.message : "Failed to load discussions", ); @@ -273,7 +278,7 @@ const DashboardContent: React.FC = () => { }); }; - const filteredDiscussions = useMemo( + const filteredDiscussions = React.useMemo( () => getFilteredDiscussions(discussions), [discussions, activeDiscussionTab, selectedCategory, searchQuery, sortBy], ); diff --git a/src/services/githubService.ts b/src/services/githubService.ts index ba0680f8..9b9aece7 100644 --- a/src/services/githubService.ts +++ b/src/services/githubService.ts @@ -1,9 +1,8 @@ -import type { - GitHubDiscussionsResponse, -} from "../types/githubDiscussions"; - -// GitHub API service for fetching public organization metrics. -// Discussions are fetched through a server-side proxy so no token is exposed client-side. +// GitHub API service for fetching organization metrics +// Uses localStorage for caching to reduce API calls +// 1) discussions count used org-wide search — replaced with repo-specific GraphQL query (default repo: "Support"). +// 2) anonymous contributors (anon=true) made configurable (default: false). +// Changes are annotated with // === ADDED and // === UPDATED where applicable. export interface GitHubOrgStats { totalStars: number; @@ -11,7 +10,7 @@ export interface GitHubOrgStats { totalRepositories: number; totalContributors: number; publicRepositories: number; - discussionsCount: number | null; + discussionsCount: number; lastUpdated: number; } @@ -34,22 +33,67 @@ export interface GitHubOrganization { following: number; } +export interface GitHubDiscussion { + id: string; + title: string; + body: string; + author: { + login: string; + avatar_url: string; + html_url: string; + }; + category: { + name: string; + emoji: string; + }; + created_at: string; + updated_at: string; + comments: number; + reactions: { + total_count: number; + }; + html_url: string; + labels: Array<{ + name: string; + color: string; + }>; +} + class GitHubService { private readonly ORG_NAME = "recodehive"; 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_API_URL = "/api/github-discussions"; // === ADDED: include anonymous contributors configurable (default false) private includeAnonymousContributors = false; + // === ADDED: stored token for authenticated API calls + private token: string = ""; + + // === ADDED: set the GitHub token (e.g. from Docusaurus customFields.gitToken) + setToken(token: string): void { + if (token && token.trim()) { + this.token = token.trim(); + } + } + // Get headers for GitHub API requests private getHeaders(): Record { - return { + const headers: Record = { Accept: "application/vnd.github.v3+json", "Content-Type": "application/json", }; + + // Use stored token first, then fall back to window.GITHUB_TOKEN + const token = + this.token || + (typeof window !== "undefined" ? (window as any).GITHUB_TOKEN : ""); + if (token) { + headers["Authorization"] = `token ${token}`; + } + + return headers; } // === ADDED: setter to toggle anonymous contributors inclusion @@ -243,6 +287,54 @@ 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). + private async getDiscussionsCount( + signal?: AbortSignal, + repoName: string = "Support", + ): Promise { + try { + // GraphQL query to get discussions totalCount for a repository + const query = ` + query ($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + discussions { totalCount } + } + } + `; + const variables = { owner: this.ORG_NAME, name: repoName }; + + const resp = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + ...this.getHeaders(), + "Content-Type": "application/json", + }, + body: JSON.stringify({ query, variables }), + signal, + }); + + if (!resp.ok) { + console.warn(`GraphQL request for discussions failed: ${resp.status}`); + return 0; + } + + const data = await resp.json(); + if (data.errors) { + console.warn("GraphQL errors while fetching discussions:", data.errors); + return 0; + } + + const count = data?.data?.repository?.discussions?.totalCount || 0; + return Number(count); + } catch (error) { + console.warn("Error fetching discussions count via GraphQL:", error); + return 0; + } + } + // Main method to fetch all organization statistics async fetchOrganizationStats(signal?: AbortSignal): Promise { // Try to get cached data first @@ -252,7 +344,11 @@ class GitHubService { } try { - const repositories = await this.fetchAllRepositories(signal); + // Fetch organization info and repositories in parallel + const [orgInfo, repositories] = await Promise.all([ + this.fetchOrganizationInfo(signal), + this.fetchAllRepositories(signal), + ]); // Filter out archived repositories for active stats const activeRepos = repositories.filter((repo) => !repo.archived); @@ -267,10 +363,12 @@ class GitHubService { 0, ); - const totalContributors = await this.estimateContributors( - activeRepos, - signal, - ); + // Estimate contributors and get discussions count + // === UPDATED: getDiscussionsCount now uses GraphQL for a specific repo (default 'Support') + const [totalContributors, discussionsCount] = await Promise.all([ + this.estimateContributors(activeRepos, signal), + this.getDiscussionsCount(signal), // default repoName: "Support" (change if you prefer another repo) + ]); const stats: GitHubOrgStats = { totalStars, @@ -278,7 +376,7 @@ class GitHubService { totalRepositories: repositories.length, publicRepositories: activeRepos.length, totalContributors, - discussionsCount: null, + discussionsCount, lastUpdated: Date.now(), }; @@ -296,7 +394,7 @@ class GitHubService { totalRepositories: 0, publicRepositories: 0, totalContributors: 0, - discussionsCount: null, + discussionsCount: 0, lastUpdated: Date.now(), }; @@ -324,52 +422,194 @@ 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 { - const response = await fetch( - `${this.DISCUSSIONS_API_URL}?limit=${encodeURIComponent(limit)}`, - { + ): Promise { + const query = ` + query GetDiscussions($owner: String!, $name: String!, $first: Int!) { + repository(owner: $owner, name: $name) { + discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { + nodes { + id + title + body + createdAt + updatedAt + url + author { + login + avatarUrl + url + } + category { + name + emoji + } + comments { + totalCount + } + reactions { + totalCount + } + labels(first: 10) { + nodes { + name + color + } + } + } + } + } + } + `; + + const variables = { + owner: this.ORG_NAME, + name: "recode-website", // Main repository for discussions (unchanged) + first: limit, + }; + + try { + const response = await fetch("https://api.github.com/graphql", { + method: "POST", headers: { - Accept: "application/json", + ...this.getHeaders(), + "Content-Type": "application/json", }, + body: JSON.stringify({ query, variables }), signal, - }, - ); + }); - const contentType = response.headers.get("Content-Type") || ""; - const isJsonResponse = contentType.includes("application/json"); - const fallbackMessage = `Failed to fetch GitHub discussions: ${response.status} ${response.statusText}`; - let payload: GitHubDiscussionsResponse | null = null; + if (!response.ok) { + throw new Error(`GraphQL request failed: ${response.status}`); + } - if (isJsonResponse) { - try { - payload = (await response.json()) as GitHubDiscussionsResponse; - } catch { - throw new Error(`${fallbackMessage} (invalid JSON response)`); + const data = await response.json(); + + if (data.errors) { + console.error("GraphQL errors:", data.errors); + throw new Error("GraphQL query failed"); } - } - if (!response.ok) { - throw new Error(payload?.message || fallbackMessage); - } + const discussions = data.data?.repository?.discussions?.nodes || []; + + return discussions.map( + (discussion: any): GitHubDiscussion => ({ + id: discussion.id, + title: discussion.title, + body: discussion.body || "", + author: { + login: discussion.author?.login || "Unknown", + avatar_url: discussion.author?.avatarUrl || "", + html_url: discussion.author?.url || "", + }, + category: { + name: discussion.category?.name || "General", + emoji: discussion.category?.emoji || "💬", + }, + created_at: discussion.createdAt, + updated_at: discussion.updatedAt, + comments: discussion.comments?.totalCount || 0, + reactions: { + total_count: discussion.reactions?.totalCount || 0, + }, + html_url: discussion.url, + labels: + discussion.labels?.nodes?.map((label: any) => ({ + name: label.name, + color: label.color, + })) || [], + }), + ); + } catch (error) { + console.error("Error fetching discussions:", error); - if (!payload) { - throw new Error(`${fallbackMessage} (non-JSON response)`); + // Return mock data for development/fallback + return this.getMockDiscussions(); } - - return payload; } - async fetchDiscussionsCount(signal?: AbortSignal): Promise { - try { - const payload = await this.fetchDiscussions(1, signal); - return payload.totalCount ?? null; - } catch (error) { - console.warn("Error fetching discussions count from proxy:", error); - return null; - } + // Mock discussions for development/fallback (unchanged) + private getMockDiscussions(): GitHubDiscussion[] { + return [ + { + id: "1", + title: "Welcome to recode hive Discussions!", + body: "This is where we discuss ideas, share knowledge, and help each other grow. Feel free to ask questions, share your projects, or just say hello!", + author: { + login: "recodehive", + avatar_url: "https://avatars.githubusercontent.com/u/your-org-id?v=4", + html_url: "https://github.com/recodehive", + }, + category: { + name: "Announcements", + emoji: "📢", + }, + created_at: new Date(Date.now() - 86400000).toISOString(), + updated_at: new Date(Date.now() - 3600000).toISOString(), + comments: 12, + reactions: { + total_count: 25, + }, + html_url: "https://github.com/recodehive/recode-website/discussions", + labels: [ + { name: "welcome", color: "0075ca" }, + { name: "community", color: "7057ff" }, + ], + }, + { + id: "2", + title: "How to contribute to open source projects?", + body: "I'm new to open source and would love to learn how to make my first contribution. Any tips or resources would be greatly appreciated!", + author: { + login: "newcontributor", + avatar_url: "https://avatars.githubusercontent.com/u/example?v=4", + html_url: "https://github.com/newcontributor", + }, + category: { + name: "Q&A", + emoji: "❓", + }, + created_at: new Date(Date.now() - 172800000).toISOString(), + updated_at: new Date(Date.now() - 7200000).toISOString(), + comments: 8, + reactions: { + total_count: 15, + }, + html_url: "https://github.com/recodehive/recode-website/discussions", + labels: [ + { name: "question", color: "d876e3" }, + { name: "beginner", color: "0e8a16" }, + ], + }, + { + id: "3", + title: "Feature Request: Dark Mode for Documentation", + body: "It would be great to have a dark mode option for the documentation pages. This would be easier on the eyes during late-night coding sessions.", + author: { + login: "darkmode-lover", + avatar_url: "https://avatars.githubusercontent.com/u/example2?v=4", + html_url: "https://github.com/darkmode-lover", + }, + category: { + name: "Ideas", + emoji: "💡", + }, + created_at: new Date(Date.now() - 259200000).toISOString(), + updated_at: new Date(Date.now() - 10800000).toISOString(), + comments: 5, + reactions: { + total_count: 22, + }, + html_url: "https://github.com/recodehive/recode-website/discussions", + labels: [ + { name: "enhancement", color: "a2eeef" }, + { name: "ui/ux", color: "f9d0c4" }, + ], + }, + ]; } } diff --git a/src/types/githubDiscussions.ts b/src/types/githubDiscussions.ts deleted file mode 100644 index 30f3dc8b..00000000 --- a/src/types/githubDiscussions.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface GitHubDiscussion { - id: string; - title: string; - body: string; - author: { - login: string; - avatar_url: string; - html_url: string; - }; - category: { - name: string; - emoji: string; - }; - created_at: string; - updated_at: string; - comments: number; - reactions: { - total_count: number; - }; - html_url: string; - labels: Array<{ - name: string; - color: string; - }>; -} - -export interface GitHubDiscussionsResponse { - available: boolean; - message: string | null; - totalCount: number | null; - discussions: GitHubDiscussion[]; - fetchedAt: string | null; -} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 1eb4696b..58e7830a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -100,10 +100,6 @@ declare module "@site/src/services/github" { } } -declare module "@site/src/services/githubService" { - export const githubService: any; -} - declare module "@site/src/components/ui/button" { export const Button: any; } diff --git a/wiki/Documentation.md b/wiki/Documentation.md index 6b29680c..3a4e5021 100644 --- a/wiki/Documentation.md +++ b/wiki/Documentation.md @@ -221,13 +221,9 @@ const isPRInTimeRange = (mergedAt: string, filter: TimeFilter): boolean => { const prDate = new Date(mergedAt); return prDate >= filterDate; }; -``` - Computed Contributors This is where React's useMemo shines: - -```typescript -const contributors = useMemo(() => { +typescriptconst contributors = useMemo(() => { if (!allContributors.length) return []; const filteredContributors = allContributors @@ -577,25 +573,37 @@ Response Example: } ``` #### Authentication -Authenticated requests should be made from a server-side endpoint or serverless function so the token is never shipped to the browser: +All requests require a GitHub Personal Access Token: ```typescript const headers: Record = { Authorization: `token ${YOUR_GITHUB_TOKEN}`, Accept: "application/vnd.github.v3+json", }; -``` - -For this site, GitHub Discussions are now fetched dynamically through a server-side `/api/github-discussions` endpoint using a server-side `GITHUB_TOKEN`, and only the sanitized discussion data is exposed to the client bundle. #### Getting a Token: Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic) Generate new token -Select scopes: public_repo, read:org, read:discussion +Select scopes: public_repo, read:org Copy the token (you won't see it again!) #### Storing the Token: -Do not store a GitHub token in `docusaurus.config.ts/js` or any other client-bundled config. Keep it in server-side environment variables and call GitHub from a backend endpoint instead. +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 || ""; +``` #### Error Handling **Rate Limit Exceeded (403)**