diff --git a/Dockerfile b/Dockerfile index 915af286..fdb99554 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,14 @@ -FROM node:18-alpine AS builder +FROM node:20-alpine -# Set working directory inside the container WORKDIR /app COPY package*.json ./ -# Install dependencies with legacy peer deps fix RUN npm install --legacy-peer-deps COPY . . -RUN npm run build -# Production Image -FROM node:18-alpine -WORKDIR /app -COPY --from=builder /app /app +# No need to run 'npm run build' for development-mode Docker EXPOSE 3000 -CMD ["npm", "run","serve"] \ No newline at end of file +CMD [ "npm", "run", "dev" ] diff --git a/Documentation.md b/Documentation.md new file mode 100644 index 00000000..a5ff4f16 --- /dev/null +++ b/Documentation.md @@ -0,0 +1,806 @@ +# Building a GitHub Organization Leaderboard: A Deep Dive into Recode Hive's Implementation + +Ever wondered how to build a live leaderboard that tracks contributions across an entire GitHub organization? Let me walk you through how we built ours at Recode Hive. This isn't your typical "fetch and display" tutorial - we're talking about handling rate limits, caching strategies, time-based filtering, and creating a smooth user experience that doesn't make the GitHub API cry. + +## What We're Building + +Our leaderboard does a few things that make it interesting: +- Aggregates merged PRs from **all** repositories in an organization +- Calculates contributor rankings based on a simple point system +- Supports time-based filtering (this week, month, year, or all time) +- Shows detailed PR information for each contributor +- Caches aggressively to avoid hitting API rate limits +- Handles concurrent requests efficiently + +The whole thing is built with React, TypeScript, and integrates with Docusaurus. But the concepts here work for any stack. + +## Architecture Overview + +Here's how the pieces fit together: +┌─────────────────────────────────────────────────────────────┐ +│ User Interface │ +│ (LeaderBoard Component + PRListModal) │ +└────────────────────────┬────────────────────────────────────┘ + │ + │ Consumes Context; + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CommunityStatsProvider │ +│ • Manages state (contributors, stats, filters) │ +│ • Handles caching (5-minute TTL) │ +│ • Orchestrates data fetching │ +└────────────────────────┬────────────────────────────────────┘ + │ + │ Makes API Calls + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ GitHub REST API │ +│ • Organization repos │ +│ • Pull requests per repo │ +│ • User information │ +└─────────────────────────────────────────────────────────────┘ + +## Data Flow: From API to UI + +### Phase 1: Data Collection + +When the app loads, we kick off a multi-stage process: + +**Stage 1: Fetch All Organization Repositories** +```typescript +const fetchAllOrgRepos = async (headers: Record) => { + const repos: any[] = []; + let page = 1; + + while (true) { + const resp = await fetch( + `https://api.github.com/orgs/${GITHUB_ORG}/repos?type=public&per_page=100&page=${page}`, + { headers } + ); + + const data = await resp.json(); + repos.push(...data); + + if (data.length < 100) break; // Last page + page++; + } + + return repos; +}; +``` +This grabs every public repo in the organization. Simple pagination loop. + +**Stage 2: Fetch Merged PRs for Each Repository** +Here's where it gets interesting. We need PRs from potentially dozens of repos, and we can't just fire off 50 requests at once. +```typescript +const fetchMergedPRsForRepo = async (repoName: string, headers: Record) => { + const mergedPRs: PullRequestItem[] = []; + + // Get first page to estimate total + const firstResp = await fetch( + `https://api.github.com/repos/${GITHUB_ORG}/${repoName}/pulls?state=closed&per_page=100&page=1`, + { headers } + ); + + const firstPRs = await firstResp.json(); + const firstPageMerged = firstPRs.filter((pr) => Boolean(pr.merged_at)); + mergedPRs.push(...firstPageMerged); + + if (firstPRs.length < 100) return mergedPRs; // Only one page + + // Fetch remaining pages in parallel (with limit) + const pagePromises: Promise[] = []; + const maxPages = Math.min(MAX_PAGES_PER_REPO, 10); + + for (let i = 2; i <= maxPages; i++) { + pagePromises.push( + fetch( + `https://api.github.com/repos/${GITHUB_ORG}/${repoName}/pulls?state=closed&per_page=100&page=${i}`, + { headers } + ) + .then(async (resp) => { + const prs = await resp.json(); + return prs.filter((pr) => Boolean(pr.merged_at)); + }) + .catch(() => []) + ); + } + + const remainingPages = await Promise.all(pagePromises); + remainingPages.forEach(pagePRs => { + if (pagePRs.length > 0) mergedPRs.push(...pagePRs); + }); + + return mergedPRs; +}; +``` +**Key optimization**: we fetch the first page sequentially to see if there are more pages, then parallelize the remaining requests. This prevents unnecessary API calls for repos with few PRs. + +**Stage 3: Process in Batches** +We don't process all repos at once. Instead, we batch them: +```typescript +const processBatch = async ( + repos: any[], + headers: Record +): Promise<{ contributorMap: Map; totalMergedPRs: number }> => { + const contributorMap = new Map(); + let totalMergedPRs = 0; + + // Process 8 repos at a time + for (let i = 0; i < repos.length; i += MAX_CONCURRENT_REQUESTS) { + const batch = repos.slice(i, i + MAX_CONCURRENT_REQUESTS); + + const promises = batch.map(async (repo) => { + if (repo.archived) return { mergedPRs: [], repoName: repo.name }; + + try { + const mergedPRs = await fetchMergedPRsForRepo(repo.name, headers); + return { mergedPRs, repoName: repo.name }; + } catch (error) { + console.warn(`Skipping repo ${repo.name} due to error:`, error); + return { mergedPRs: [], repoName: repo.name }; + } + }); + + const results = await Promise.all(promises); + + // Aggregate contributor data + results.forEach(({ mergedPRs, repoName }) => { + totalMergedPRs += mergedPRs.length; + + mergedPRs.forEach((pr) => { + const username = pr.user.login; + if (!contributorMap.has(username)) { + contributorMap.set(username, { + username, + avatar: pr.user.avatar_url, + profile: pr.user.html_url, + points: 0, + prs: 0, + allPRDetails: [], + }); + } + + const contributor = contributorMap.get(username)!; + contributor.allPRDetails.push({ + title: pr.title, + url: pr.html_url, + mergedAt: pr.merged_at, + repoName, + number: pr.number, + }); + }); + }); + } + + return { contributorMap, totalMergedPRs }; +}; +``` +Why batches of 8? Trial and error. Too many concurrent requests = rate limits. Too few = slow load times. Eight was the sweet spot. +### Phase 2: State Management & Filtering +Once we have all the data, we store it in a way that makes filtering fast. This is the secret sauce. +#### Data Structure +```typescript +interface FullContributor { + username: string; + avatar: string; + profile: string; + allPRDetails: PRDetails[]; // ALL PRs ever, unfiltered + points: number; // Calculated based on current filter + prs: number; // Calculated based on current filter +} +``` +Notice allPRDetails stores everything. We never refetch when users change the time filter. +- **Time Filtering Logic** + +``` typescript +const getTimeFilterDate = (filter: TimeFilter): Date | null => { + const now = new Date(); + switch (filter) { + case 'week': + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case 'month': { + const lastMonth = new Date(now); + lastMonth.setMonth(now.getMonth() - 1); + return lastMonth; + } + case 'year': + return new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); + case 'all': + default: + return null; + } +}; + +const isPRInTimeRange = (mergedAt: string, filter: TimeFilter): boolean => { + if (filter === 'all') return true; + + const filterDate = getTimeFilterDate(filter); + if (!filterDate) return true; + + const prDate = new Date(mergedAt); + return prDate >= filterDate; +}; +Computed Contributors +This is where React's useMemo shines: +typescriptconst contributors = useMemo(() => { + if (!allContributors.length) return []; + + const filteredContributors = allContributors + .map(contributor => { + const filteredPRs = contributor.allPRDetails.filter(pr => + isPRInTimeRange(pr.mergedAt, currentTimeFilter) + ); + + return { + username: contributor.username, + avatar: contributor.avatar, + profile: contributor.profile, + points: filteredPRs.length * POINTS_PER_PR, + prs: filteredPRs.length, + prDetails: filteredPRs, + }; + }) + .filter(contributor => contributor.prs > 0) + .sort((a, b) => b.points - a.points || b.prs - a.prs); + + return filteredContributors; +}, [allContributors, currentTimeFilter]); +``` +When the user changes the filter, this recalculates instantly. No API calls, no loading spinners. +The Context Provider Pattern +Everything runs through a Context Provider. This gives us a single source of truth and makes the data available anywhere in the component tree. +- **Context Definition** +```typescript +interface ICommunityStatsContext { + // Organization stats + githubStarCount: number; + githubContributorsCount: number; + githubForksCount: number; + githubReposCount: number; + + // Leaderboard data + contributors: Contributor[]; + stats: Stats | null; + + // Time filtering + currentTimeFilter: TimeFilter; + setTimeFilter: (filter: TimeFilter) => void; + getFilteredPRsForContributor: (username: string) => PRDetails[]; + + // Meta + loading: boolean; + error: string | null; + refetch: (signal: AbortSignal) => Promise; + clearCache: () => void; +} +``` +- **Using the Context** +```typescript +export const useCommunityStatsContext = (): ICommunityStatsContext => { + const context = useContext(CommunityStatsContext); + if (context === undefined) { + throw new Error("useCommunityStatsContext must be used within a CommunityStatsProvider"); + } + return context; +}; +``` +Any component can now access the leaderboard data: +```typescript +const { contributors, currentTimeFilter, setTimeFilter } = useCommunityStatsContext(); +### Caching Strategy +GitHub API has rate limits. We cache everything for 5 minutes: +```typescript +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + +const [cache, setCache] = useState<{ + data: { contributors: FullContributor[]; rawStats: { totalPRs: number } } | null; + timestamp: number; +}>({ data: null, timestamp: 0 }); + +// In fetchAllStats +const now = Date.now(); +if (cache.data && (now - cache.timestamp) < CACHE_DURATION) { + setAllContributors(cache.data.contributors); + setLoading(false); + return; +} +``` +Simple but effective. On subsequent visits within 5 minutes, users see data instantly. +### UI Components +- **The Main Leaderboard** +The leaderboard component is pretty straightforward once you have the data: +```typescript +export default function LeaderBoard(): JSX.Element { + const { + contributors, + stats, + loading, + error, + currentTimeFilter, + setTimeFilter + } = useCommunityStatsContext(); + + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + + // Filter by search + const filteredContributors = contributors.filter((contributor) => + contributor.username.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Pagination + const itemsPerPage = 10; + const totalPages = Math.ceil(filteredContributors.length / itemsPerPage); + const currentItems = filteredContributors.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + // Render... +} +``` +#### Time Filter Dropdown +```tsx + +``` +- **Top 3 Performers** +We show the top 3 in a special layout: +```tsx{filteredContributors.length > 2 && ( +
+ + + +
+)} +``` +Notice rank 2, then 1, then 3? Creates a podium effect in the layout. +- **Contributor Rows** +```tsx +{currentItems.map((contributor, index) => ( +
+
+
+ {filteredContributors.indexOf(contributor) + 1} +
+
+
+ {contributor.username} +
+ +
+ handlePRClick(contributor)} + clickable={true} + /> +
+
+ +
+
+))} +``` +- **PR Details Modal** +When users click on a contributor's PR count, we show a modal with all their PRs: +```typescript +const getFilteredPRsForContributor = useCallback((username: string): PRDetails[] => { + const contributor = allContributors.find(c => c.username === username); + if (!contributor) return []; + + return contributor.allPRDetails + .filter(pr => isPRInTimeRange(pr.mergedAt, currentTimeFilter)) + .sort((a, b) => new Date(b.mergedAt).getTime() - new Date(a.mergedAt).getTime()); +}, [allContributors, currentTimeFilter]); +``` +This function is exposed through the context, so the modal can call it: + +```tsx +export default function PRListModal({ contributor, isOpen, onClose }: PRListModalProps) { + const { getFilteredPRsForContributor, currentTimeFilter } = useCommunityStatsContext(); + + const filteredPRs = getFilteredPRsForContributor(contributor.username); + + return ( + + {isOpen && ( + + +
+

{contributor.username}'s Pull Requests

+

+ {filteredPRs.length} merged PR{filteredPRs.length !== 1 ? 's' : ''} • + {filteredPRs.length * 10} points +

+
+ +
+ {filteredPRs.map((pr) => ( +
+

{pr.title}

+
+ {pr.repoName} + #{pr.number} + Merged on {formatDate(pr.mergedAt)} +
+ + + +
+ ))} +
+
+
+ )} +
+ ); +} +``` +### GitHub API Reference +Endpoints We Use +1. List Organization Repositories +GET https://api.github.com/orgs/{org}/repos +Parameters: + +- *type (query)*: Repository type filter. We use public +- *per_page (query)*: Results per page. Max 100 +- *page (query)*: Page number for pagination + +#### Request Example: +```bash +curl -H "Authorization: token YOUR_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/orgs/recodehive/repos?type=public&per_page=100&page=1" +Response Example: +```json[ + { + "id": 123456, + "name": "awesome-project", + "full_name": "recodehive/awesome-project", + "private": false, + "archived": false, + "html_url": "https://github.com/recodehive/awesome-project", + "description": "An awesome project", + "fork": false, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-12-20T15:45:00Z" + } +] + +``` +Rate Limit: 5000 requests per hour (authenticated) + +2. List Pull Requests +GET https://api.github.com/repos/{owner}/{repo}/pulls + +Parameters: + +- *state (query)*: PR state. We use closed to get merged PRs +- *per_page (query)*: Results per page. Max 100 +- *page (query)*: Page number +- *sort (query, optional)*: Sort by created, updated, popularity, long-running +- *direction (query, optional)*: Sort direction asc or desc + +#### Request Example: +```bash +curl -H "Authorization: token YOUR_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/recodehive/awesome-project/pulls?state=closed&per_page=100&page=1" + +```json +[ + { + "id": 789012, + "number": 42, + "state": "closed", + "title": "Add new feature", + "user": { + "login": "johndoe", + "id": 12345, + "avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4", + "html_url": "https://github.com/johndoe" + }, + "body": "This PR adds a new feature...", + "created_at": "2024-11-01T09:00:00Z", + "updated_at": "2024-11-05T14:30:00Z", + "closed_at": "2024-11-05T14:30:00Z", + "merged_at": "2024-11-05T14:30:00Z", + "merge_commit_sha": "abc123def456", + "html_url": "https://github.com/recodehive/awesome-project/pull/42", + "commits": 5, + "additions": 150, + "deletions": 30, + "changed_files": 8 + } +] +``` +Key Fields: + +- *merged_at*: If not null, PR was merged (this is what we check) +- *user*: Contains contributor info (username, avatar, profile URL) +- *number*: PR number for the repo +- *title*: PR title +- *html_url*: Link to the PR + +Rate Limit: Same as above + +3. Get Organization Stats +GET https://api.github.com/orgs/{org} +#### Request Example: +```bash +curl -H "Authorization: token YOUR_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/orgs/recodehive" +Response Example: +```json +{ + "login": "recodehive", + "id": 98765, + "url": "https://api.github.com/orgs/recodehive", + "repos_url": "https://api.github.com/orgs/recodehive/repos", + "avatar_url": "https://avatars.githubusercontent.com/u/98765?v=4", + "description": "Community-driven open source projects", + "name": "Recode Hive", + "company": null, + "blog": "https://recodehive.com", + "location": "Worldwide", + "email": null, + "public_repos": 25, + "public_gists": 0, + "followers": 150, + "following": 0, + "html_url": "https://github.com/recodehive", + "created_at": "2023-06-15T08:00:00Z", + "updated_at": "2024-12-01T12:00:00Z", + "type": "Organization" +} +``` +#### Authentication +All requests require a GitHub Personal Access Token: +```typescript +const headers: Record = { + Authorization: `token ${YOUR_GITHUB_TOKEN}`, + Accept: "application/vnd.github.v3+json", +}; + +#### Getting a Token: + +Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic) +Generate new token +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 || ""; +``` +#### Error Handling +**Rate Limit Exceeded (403)** + +```json +{ + "message": "API rate limit exceeded for user ID 12345.", + "documentation_url": "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting" +} +``` +#### How we handle it: +```typescript +try { + const resp = await fetch(url, { headers }); + if (!resp.ok) { + if (resp.status === 403) { + throw new Error("Rate limit exceeded. Please try again later."); + } + throw new Error(`API error: ${resp.status} ${resp.statusText}`); + } + return await resp.json(); +} catch (error) { + console.error("Error fetching data:", error); + // Fallback to cached data or show error message +} +``` +**Not Found (404)** +Repository doesn't exist or is private: +```json +{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/reference/repos#get-a-repository" +} +``` +**Unauthorized (401)** +Invalid or expired token: +```json +{ + "message": "Bad credentials", + "documentation_url": "https://docs.github.com/rest" +} +``` +### Performance Optimizations +1. Concurrent Request Batching +Processing 50 repos sequentially takes forever. Processing them all at once hits rate limits. Solution: batches. +```typescript +const MAX_CONCURRENT_REQUESTS = 8; + +for (let i = 0; i < repos.length; i += MAX_CONCURRENT_REQUESTS) { + const batch = repos.slice(i, i + MAX_CONCURRENT_REQUESTS); + const promises = batch.map(repo => fetchMergedPRsForRepo(repo.name, headers)); + await Promise.all(promises); +} +``` +2. Early Pagination Termination +If a repo has 15 PRs, don't fetch 10 pages: +```typescript +const firstResp = await fetch(`${url}?page=1&per_page=100`, { headers }); +const firstPRs = await firstResp.json(); + +if (firstPRs.length < 100) { + return firstPRs.filter(pr => pr.merged_at); // Only one page +} + + +// Otherwise, fetch more pages +``` +3. Memoization +Filter calculations are expensive when you have 200+ contributors: +```typescript +const contributors = useMemo(() => { + return allContributors + .map(c => ({ + ...c, + prs: c.allPRDetails.filter(pr => isPRInTimeRange(pr.mergedAt, filter)).length + })) + .filter(c => c.prs > 0) + .sort((a, b) => b.prs - a.prs); +}, [allContributors, currentTimeFilter]); +``` +Only recalculates when allContributors or currentTimeFilter changes. +4. Abort Controllers +Clean up pending requests when component unmounts: +```typescript +useEffect(() => { + const abortController = new AbortController(); + fetchAllStats(abortController.signal); + + return () => { + abortController.abort(); // Cancel pending requests + }; +}, [fetchAllStats]); +``` +### Best Practices +#### Security Considerations +#### Never Expose Tokens Client-Side: +Our implementation runs in the browser, but the token is injected at build time and never exposed in the bundle: +In production, this gets baked into the static build. The token never appears in client-side code. +##### Alternative: Backend Proxy +For more sensitive applications, proxy GitHub API calls through your backend: +Client → Your API → GitHub API +This way, tokens stay on the server. +#### Token Scopes +#### Use the minimum required scopes: +- public_repo (read access to public repos) +- read:org (read org data) +- repo (full access - not needed) +- admin:org (admin access - definitely not needed) + +### Scalability Considerations +#### Caching Strategy +5-minute cache works for us, but adjust based on your needs: +```typescript +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + +// For very large orgs (100+ repos), increase to 15 minutes: +const CACHE_DURATION = 15 * 60 * 1000; + + +// For real-time requirements, decrease to 1 minute: +const CACHE_DURATION = 1 * 60 * 1000; +``` +### Deployment Checklist +Before going live: + + - Token Security: Ensure GitHub token is in environment variables, not hardcoded + - Rate Limits: Monitor API usage and adjust cache duration if needed + - Error Handling: All API calls wrapped in try-catch with user-friendly messages + - Loading States: Skeleton loaders in place for all async operations + - Mobile Responsive: Test on various screen sizes + - Accessibility: Keyboard navigation, screen reader support, ARIA labels + - Performance: Check bundle size, lazy load components if needed + - Analytics: Track usage patterns (filter changes, PR modal opens, etc.) + - Monitoring: Set up alerts for API failures or performance degradation + - Documentation: Update README with setup instructions + +#### Complete Flow +Here's what the complete flow looks like in action: +```mermaid +flowchart TD + A[User visits leaderboard] --> B[CommunityStatsProvider initializes] + B --> C[Check cache] + C -->|Cache empty - first visit| D[Fetch org repos - 1 API call] + C -->|Cache hit| J[Render leaderboard from cache] + + D --> E[Process repos in batches of 8] + E --> F[Fetch closed PRs for each repo] + F --> G[Filter merged PRs only] + G --> H[Build contributor map] + H --> I[Calculate points and rankings] + I --> K[Cache results with timestamp] + K --> J[Render leaderboard] + +``` + +2. User changes filter to "This Month": + +```mermaid +flowchart TD + A["setTimeFilter('month') called"] --> B["useMemo recalculates contributors"] + B --> C["Filters allPRDetails by date"] + C --> D["Recalculates points for each contributor"] + D --> E["Re-sorts contributors"] + E --> F["UI updates instantly (no API calls)"] + +``` +3. User clicks PR badge: + +```mermaid +flowchart TD + A["handlePRClick sets selected contributor"] --> B["Modal opens with animation"] + B --> C["getFilteredPRsForContributor called"] + C --> D["Returns PRs matching current time filter"] + D --> E["Modal displays list with details"] +``` + +4. User comes back 10 minutes later: + +```mermaid +flowchart TD + A["CommunityStatsProvider checks cache"] --> B["Cache expired (10min > 5min TTL)"] + B --> C["Fetches fresh data"] + C --> D["Updates cache"] + D --> E["Shows new leaderboard"] +``` + +### Wrapping Up +Building a leaderboard like this taught us a few things: + +- **Cache aggressively** - The GitHub API is fast, but not hitting it is faster +- **Batch intelligently** - Too much concurrency = rate limits, too little = slow +- **Store everything** - Fetch once, filter client-side for instant updates +- **Handle failures gracefully** - One bad repo shouldn't break the whole leaderboard +- **Optimize for perception** - Show something immediately, even if incomplete + +The architecture here scales pretty well. We handle 10+ repos with 200+ contributors and the initial load takes about 8-10 seconds. After that, everything is instant because filtering happens in-memory. +If you're building something similar, start simple. Fetch all the data, display it, then add features. We started with just a list of contributors and added time filters, PR details, and search later. +The complete code is in our repo at [recodehive](https://github.com/recodehive/recode-website) check it out, steal liberally, and let us know if you build something cool with it. + +Got questions? Open an issue on our repo or ping us on Discord. +Happy coding! diff --git a/README.md b/README.md index 33632d9e..dc0f7a81 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,12 @@ flowchart LR 1. **Clone the repository:** ```bash - git clone https://github.com/your-username/recodehive-website.git + git clone https://github.com/your-username/recode-website.git ``` 2. **Navigate to the project directory:** ```bash - cd recodehive-website + cd recode-website ``` 3. **Prerequesites** @@ -80,6 +80,25 @@ docker run -p 3000:3000 recodehive-app This command will start a development server and open the application in your default web browser. +## ⚡ Local Development with Docker Compose & Hot Reload + +For an even smoother experience, contributors can leverage **Docker Compose with hot reloading**. +This lets you see code changes instantly at [http://localhost:3000](http://localhost:3000) without rebuilding or restarting containers. + +### 🏃 Quick Start + +```bash +git clone https://github.com/your-username/recodehive-website.git +cd recodehive-website +docker-compose up +``` + +### 🚢 Production Deployment +```bash +npm run build +npm run serve +``` + **If you'd like to contribute to CodeHarborHub, please follow these guidelines:** - **Fork** the repository and clone it locally. @@ -112,7 +131,7 @@ recode-website/ | ├── Google-Student-Ambassador/ | ├── ... ├── src/ 🔹Source Code -| └── compenents/ +| └── components/ | ├── css/ | └── custom.css | ├── data/ @@ -126,7 +145,8 @@ recode-website/ | ├── theme/ | └── utils/ ├── static/ 🔹 Public Assets -| ├── icons, img +| ├── icons +| ├── img | ├── .nojekyll | └── *.png ├── .gitignore diff --git a/blog/google-backlinks/index.md b/blog/google-backlinks/index.md index 333aeec3..c5b7f9b3 100644 --- a/blog/google-backlinks/index.md +++ b/blog/google-backlinks/index.md @@ -8,10 +8,10 @@ date: 2025-05-30 --- - +  Google’s algorithms have been evolving for many years. Still, backlinks have always been a factor that marketers have looked at and are continually altering to boost a website’s search engine rankings. You’ve certainly heard a lot about backlinks from all of these digital marketers, but you don’t really understand what they are. -  + --- diff --git a/docker-compose.yml b/docker-compose.yml index 3b09ec5c..6a9e77b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ version: "3.9" - services: recodehive: build: @@ -8,8 +7,8 @@ services: - "3000:3000" volumes: - .:/app - - /app/node_modules + - /app/node_modules # Prevents node_modules being overwritten by the mount working_dir: /app - command: npm run start + command: npm run dev # THIS is the crucial change for hot-reload! environment: - NODE_ENV=development diff --git a/docs/python/python-errors-and-exceptions.md b/docs/python/python-errors-and-exceptions.md new file mode 100644 index 00000000..7e9bfbfa --- /dev/null +++ b/docs/python/python-errors-and-exceptions.md @@ -0,0 +1,789 @@ +--- +id: python-errors-and-exceptions +title: Exception Handling in Python +sidebar_label: Exception Handling +sidebar_position: 12 +tags: + [ + Python, + Exceptions, + Error Handling, + try, + except, + finally, + else, + raise, + Python Syntax, + Introduction of python, + ] +--- + +# Exception Handling in Python + +**Exception handling** in Python is a mechanism to gracefully manage errors that occur during program execution. Instead of letting your program crash, you can catch and handle errors, provide meaningful feedback, and ensure proper cleanup of resources. + +Exceptions are objects that represent errors, and Python provides a robust system to catch, handle, and raise them. + +--- + +## Why Exception Handling? + +Without exception handling, errors cause programs to crash: + +```python +# This will crash the program +number = int("abc") # ValueError: invalid literal for int() +``` + +With exception handling, you can manage errors gracefully: + +```python +try: + number = int("abc") +except ValueError: + print("Invalid input! Please enter a number.") + number = 0 +``` + +--- + +## The `try...except` Block + +The basic syntax for handling exceptions: + +```python +try: + # Code that might raise an exception + result = 10 / 0 +except ZeroDivisionError: + # Code to handle the exception + print("Cannot divide by zero!") + +# Output: Cannot divide by zero! +``` + +**Syntax:** + +```python +try: + # Risky code + pass +except ExceptionType: + # Handle the exception + pass +``` + +--- + +## Catching Specific Exceptions + +### Single Exception + +Catch a specific exception type: + +```python +try: + age = int(input("Enter your age: ")) + print(f"You are {age} years old") +except ValueError: + print("Please enter a valid number!") +``` + +### Common Built-in Exceptions + +```python +# ValueError - Invalid value +try: + number = int("hello") +except ValueError: + print("That's not a valid number!") + +# ZeroDivisionError - Division by zero +try: + result = 100 / 0 +except ZeroDivisionError: + print("Cannot divide by zero!") + +# FileNotFoundError - File doesn't exist +try: + with open("nonexistent.txt", "r") as file: + content = file.read() +except FileNotFoundError: + print("File not found!") + +# IndexError - Invalid index +try: + my_list = [1, 2, 3] + print(my_list[10]) +except IndexError: + print("Index out of range!") + +# KeyError - Invalid dictionary key +try: + my_dict = {"name": "Alice"} + print(my_dict["age"]) +except KeyError: + print("Key not found in dictionary!") + +# TypeError - Wrong type operation +try: + result = "hello" + 5 +except TypeError: + print("Cannot add string and integer!") +``` + +--- + +## Multiple Exception Handlers + +Handle different exceptions separately: + +```python +def divide_numbers(a, b): + try: + result = int(a) / int(b) + return result + except ValueError: + print("Error: Please provide valid numbers") + except ZeroDivisionError: + print("Error: Cannot divide by zero") + except Exception as e: + print(f"Unexpected error: {e}") + +divide_numbers("10", "2") # Output: 5.0 +divide_numbers("abc", "2") # Output: Error: Please provide valid numbers +divide_numbers("10", "0") # Output: Error: Cannot divide by zero +``` + +### Catching Multiple Exceptions Together + +Use a tuple to catch multiple exceptions with the same handler: + +```python +try: + value = int(input("Enter a number: ")) + result = 100 / value +except (ValueError, ZeroDivisionError): + print("Invalid input or division by zero!") + +# With exception details +try: + value = int(input("Enter a number: ")) + result = 100 / value +except (ValueError, ZeroDivisionError) as e: + print(f"Error occurred: {e}") +``` + +--- + +## The `else` Clause + +The `else` block executes **only if no exception occurs** in the `try` block: + +```python +try: + number = int(input("Enter a number: ")) +except ValueError: + print("Invalid input!") +else: + print(f"Success! You entered: {number}") + print("No errors occurred") +``` + +### Practical Example with `else` + +```python +def read_file(filename): + try: + file = open(filename, "r") + except FileNotFoundError: + print(f"Error: {filename} not found") + else: + content = file.read() + print(f"File content:\n{content}") + file.close() + print("File read successfully!") + +read_file("example.txt") +``` + +--- + +## The `finally` Clause + +The `finally` block **always executes**, whether an exception occurs or not. It's used for cleanup actions: + +```python +try: + file = open("data.txt", "r") + content = file.read() + print(content) +except FileNotFoundError: + print("File not found!") +finally: + print("Cleanup: Closing resources") + # This always runs +``` + +### Resource Cleanup Example + +```python +def process_file(filename): + file = None + try: + file = open(filename, "r") + data = file.read() + result = int(data) # Might raise ValueError + return result + except FileNotFoundError: + print("File not found!") + return None + except ValueError: + print("File contains invalid data!") + return None + finally: + if file: + file.close() + print("File closed") + +process_file("numbers.txt") +``` + +--- + +## Complete `try...except...else...finally` Structure + +All clauses together: + +```python +def divide_and_log(a, b): + try: + result = a / b + except ZeroDivisionError: + print("Error: Division by zero") + return None + except TypeError: + print("Error: Invalid types for division") + return None + else: + print(f"Division successful: {a} / {b} = {result}") + return result + finally: + print("Operation completed") + +print(divide_and_log(10, 2)) +# Output: +# Division successful: 10 / 2 = 5.0 +# Operation completed +# 5.0 + +print(divide_and_log(10, 0)) +# Output: +# Error: Division by zero +# Operation completed +# None +``` + +--- + +## Accessing Exception Information + +Use the `as` keyword to access exception details: + +```python +try: + number = int("abc") +except ValueError as e: + print(f"Exception type: {type(e).__name__}") + print(f"Exception message: {e}") + print(f"Exception args: {e.args}") + +# Output: +# Exception type: ValueError +# Exception message: invalid literal for int() with base 10: 'abc' +# Exception args: ("invalid literal for int() with base 10: 'abc'",) +``` + +--- + +## Raising Exceptions + +Use the `raise` keyword to throw exceptions: + +### Basic Raise + +```python +def check_age(age): + if age < 0: + raise ValueError("Age cannot be negative!") + if age < 18: + raise ValueError("Must be 18 or older") + print(f"Age {age} is valid") + +try: + check_age(-5) +except ValueError as e: + print(f"Error: {e}") +# Output: Error: Age cannot be negative! +``` + +### Re-raising Exceptions + +```python +def process_data(data): + try: + result = int(data) + return result + except ValueError as e: + print("Logging error...") + raise # Re-raise the same exception + +try: + process_data("invalid") +except ValueError: + print("Caught re-raised exception") + +# Output: +# Logging error... +# Caught re-raised exception +``` + +### Raising with Custom Messages + +```python +def withdraw(balance, amount): + if amount > balance: + raise ValueError(f"Insufficient funds! Balance: {balance}, Requested: {amount}") + return balance - amount + +try: + new_balance = withdraw(100, 150) +except ValueError as e: + print(e) +# Output: Insufficient funds! Balance: 100, Requested: 150 +``` + +--- + +## Custom Exceptions + +Create your own exception classes: + +```python +class InvalidEmailError(Exception): + """Custom exception for invalid email addresses""" + pass + +class AgeRestrictionError(Exception): + """Custom exception for age restrictions""" + def __init__(self, age, minimum_age): + self.age = age + self.minimum_age = minimum_age + super().__init__(f"Age {age} is below minimum required age {minimum_age}") + +# Using custom exceptions +def validate_email(email): + if "@" not in email: + raise InvalidEmailError(f"Invalid email format: {email}") + print("Email is valid!") + +def check_eligibility(age): + minimum_age = 18 + if age < minimum_age: + raise AgeRestrictionError(age, minimum_age) + print("Eligible!") + +# Test custom exceptions +try: + validate_email("invalidemail.com") +except InvalidEmailError as e: + print(f"Error: {e}") +# Output: Error: Invalid email format: invalidemail.com + +try: + check_eligibility(15) +except AgeRestrictionError as e: + print(f"Error: {e}") + print(f"You are {e.minimum_age - e.age} years too young") +# Output: +# Error: Age 15 is below minimum required age 18 +# You are 3 years too young +``` + +--- + +## Exception Hierarchy + +Python exceptions follow a hierarchy. Catching a parent exception catches all child exceptions: + +```python +try: + # Some code + pass +except Exception as e: + # Catches most exceptions (but not KeyboardInterrupt, SystemExit) + print(f"Caught: {e}") +``` + +**Common Exception Hierarchy:** +``` +BaseException +├── SystemExit +├── KeyboardInterrupt +├── Exception + ├── ArithmeticError + │ ├── ZeroDivisionError + │ ├── FloatingPointError + │ └── OverflowError + ├── LookupError + │ ├── IndexError + │ └── KeyError + ├── ValueError + ├── TypeError + ├── NameError + └── ... (many more) +``` + +### Catching Parent Exceptions + +```python +try: + my_list = [1, 2, 3] + print(my_list[10]) # IndexError +except LookupError as e: # Parent of IndexError + print(f"Lookup error occurred: {e}") +# Output: Lookup error occurred: list index out of range +``` + +--- + +## Practical Examples + +### Example 1: Safe User Input + +```python +def get_integer_input(prompt): + """Safely get integer input from user.""" + while True: + try: + value = int(input(prompt)) + return value + except ValueError: + print("Invalid input! Please enter a whole number.") + except KeyboardInterrupt: + print("\nInput cancelled by user") + return None + +age = get_integer_input("Enter your age: ") +if age: + print(f"You are {age} years old") +``` + +### Example 2: File Processing with Error Handling + +```python +def process_data_file(filename): + """Process a data file with comprehensive error handling.""" + try: + with open(filename, "r") as file: + lines = file.readlines() + + numbers = [] + for line_num, line in enumerate(lines, 1): + try: + number = float(line.strip()) + numbers.append(number) + except ValueError: + print(f"Warning: Invalid number on line {line_num}: '{line.strip()}'") + continue + + if not numbers: + raise ValueError("No valid numbers found in file") + + average = sum(numbers) / len(numbers) + return average + + except FileNotFoundError: + print(f"Error: File '{filename}' not found") + return None + except PermissionError: + print(f"Error: No permission to read '{filename}'") + return None + except ValueError as e: + print(f"Error: {e}") + return None + else: + print(f"Successfully processed {len(numbers)} numbers") + finally: + print("File processing completed") + +result = process_data_file("data.txt") +if result: + print(f"Average: {result:.2f}") +``` + +### Example 3: API Request Handler + +```python +class APIError(Exception): + """Custom exception for API errors""" + pass + +class AuthenticationError(APIError): + """Custom exception for authentication failures""" + pass + +def make_api_request(endpoint, auth_token=None): + """Simulate an API request with error handling.""" + try: + if not auth_token: + raise AuthenticationError("No authentication token provided") + + if not endpoint.startswith("/api/"): + raise ValueError(f"Invalid endpoint format: {endpoint}") + + # Simulate API call + if endpoint == "/api/users": + return {"status": "success", "data": ["user1", "user2"]} + else: + raise APIError(f"Endpoint not found: {endpoint}") + + except AuthenticationError as e: + print(f"Authentication failed: {e}") + return {"status": "error", "message": str(e)} + except APIError as e: + print(f"API error: {e}") + return {"status": "error", "message": str(e)} + except ValueError as e: + print(f"Validation error: {e}") + return {"status": "error", "message": str(e)} + except Exception as e: + print(f"Unexpected error: {e}") + return {"status": "error", "message": "Internal server error"} + finally: + print(f"Request to {endpoint} completed") + +# Test the API handler +response = make_api_request("/api/users", "secret_token") +print(response) +``` + +### Example 4: Calculator with Exception Handling + +```python +def safe_calculator(operation, num1, num2): + """Perform calculations with comprehensive error handling.""" + try: + # Convert inputs to float + a = float(num1) + b = float(num2) + + if operation == "+": + result = a + b + elif operation == "-": + result = a - b + elif operation == "*": + result = a * b + elif operation == "/": + if b == 0: + raise ZeroDivisionError("Cannot divide by zero") + result = a / b + elif operation == "**": + if a == 0 and b < 0: + raise ValueError("Cannot raise 0 to a negative power") + result = a ** b + else: + raise ValueError(f"Unknown operation: {operation}") + + except ValueError as e: + return f"Error: {e}" + except ZeroDivisionError as e: + return f"Error: {e}" + except OverflowError: + return "Error: Result is too large to calculate" + except Exception as e: + return f"Unexpected error: {e}" + else: + return f"{num1} {operation} {num2} = {result}" + finally: + print("Calculation attempt completed") + +# Test cases +print(safe_calculator("+", "10", "5")) # 10 + 5 = 15.0 +print(safe_calculator("/", "10", "0")) # Error: Cannot divide by zero +print(safe_calculator("^", "10", "5")) # Error: Unknown operation: ^ +print(safe_calculator("**", "2", "1000")) # Error: Result is too large +``` + +--- + +## Best Practices + +### 1. Be Specific with Exceptions + +```python +# Good - Catch specific exceptions +try: + value = int(input("Enter number: ")) +except ValueError: + print("Invalid number") + +# Avoid - Too broad +try: + value = int(input("Enter number: ")) +except Exception: + print("Something went wrong") +``` + +### 2. Don't Silence Exceptions + +```python +# Bad - Silences all errors +try: + risky_operation() +except: + pass + +# Good - Handle specifically or log +try: + risky_operation() +except SpecificError as e: + logger.error(f"Operation failed: {e}") + # Take appropriate action +``` + +### 3. Use `finally` for Cleanup + +```python +# Good - Ensures cleanup happens +file = None +try: + file = open("data.txt", "r") + process(file) +except Exception as e: + print(f"Error: {e}") +finally: + if file: + file.close() + +# Better - Use context managers +try: + with open("data.txt", "r") as file: + process(file) +except Exception as e: + print(f"Error: {e}") +``` + +### 4. Provide Helpful Error Messages + +```python +# Good +def set_age(age): + if age < 0: + raise ValueError(f"Age cannot be negative. Received: {age}") + if age > 150: + raise ValueError(f"Age seems invalid. Received: {age}") + +# Less helpful +def set_age(age): + if age < 0 or age > 150: + raise ValueError("Invalid age") +``` + +### 5. Don't Use Exceptions for Flow Control + +```python +# Bad - Using exceptions for normal flow +try: + return my_dict[key] +except KeyError: + return default_value + +# Good - Use proper checks +return my_dict.get(key, default_value) +``` + +--- + +## Common Patterns + +### Pattern 1: Retry Logic + +```python +def retry_operation(func, max_attempts=3): + """Retry an operation multiple times on failure.""" + for attempt in range(1, max_attempts + 1): + try: + result = func() + return result + except Exception as e: + print(f"Attempt {attempt} failed: {e}") + if attempt == max_attempts: + print("All attempts failed") + raise + print("Retrying...") + +# Usage +def unreliable_operation(): + import random + if random.random() < 0.7: + raise ConnectionError("Network error") + return "Success!" + +try: + result = retry_operation(unreliable_operation) + print(result) +except ConnectionError: + print("Operation failed after all retries") +``` + +### Pattern 2: Context Manager with Exception Handling + +```python +class DatabaseConnection: + """Custom context manager with exception handling.""" + + def __init__(self, db_name): + self.db_name = db_name + self.connection = None + + def __enter__(self): + print(f"Opening connection to {self.db_name}") + self.connection = f"Connected to {self.db_name}" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print(f"Closing connection to {self.db_name}") + if exc_type is not None: + print(f"Exception occurred: {exc_type.__name__}: {exc_val}") + # Return False to propagate the exception + return False + return True + +# Usage +try: + with DatabaseConnection("users_db") as db: + print("Performing database operations...") + # Simulate an error + raise ValueError("Invalid query") +except ValueError as e: + print(f"Caught: {e}") +``` + +--- + +## Summary + +| Concept | Purpose | Example | +| --------------- | ------------------------------------ | ------------------------------------------ | +| `try` | Code that might raise exceptions | `try: risky_code()` | +| `except` | Handle specific exceptions | `except ValueError: handle_error()` | +| `else` | Runs if no exception occurred | `else: print("Success!")` | +| `finally` | Always runs (cleanup) | `finally: cleanup()` | +| `raise` | Throw an exception | `raise ValueError("Invalid")` | +| `as` | Capture exception details | `except ValueError as e:` | +| Custom | Create custom exception classes | `class MyError(Exception): pass` | +| Multiple except | Handle different exceptions | `except (ValueError, TypeError):` | +| Exception chain | Parent exception catches children | `except LookupError:` (catches IndexError) | + +Exception handling is essential for writing robust Python programs. It allows you to gracefully manage errors, provide meaningful feedback to users, and ensure proper resource cleanup. Master these concepts to build reliable and maintainable applications! \ No newline at end of file diff --git a/docs/python/python-functions.md b/docs/python/python-functions.md new file mode 100644 index 00000000..a15ccd8b --- /dev/null +++ b/docs/python/python-functions.md @@ -0,0 +1,464 @@ +--- +id: python-functions +title: Functions in Python +sidebar_label: Functions in Python +sidebar_position: 11 +tags: + [ + Python, + Functions, + def, + return, + arguments, + parameters, + args, + kwargs, + Python Syntax, + Introduction of python, + ] +--- + +# Functions in Python + +A **function** in Python is a block of reusable code that performs a specific task. Functions help organize code, avoid repetition, and make programs more modular and maintainable. + +Functions are defined using the `def` keyword and can accept inputs (parameters) and return outputs. + +--- + +## Defining a Function + +Use the `def` keyword followed by the function name and parentheses: + +```python +def greet(): + print("Hello, World!") + +# Call the function +greet() # Output: Hello, World! +``` + +**Syntax:** + +```python +def function_name(parameters): + """Optional docstring""" + # Function body + return value # Optional +``` + +--- + +## Function with Parameters + +Functions can accept inputs called **parameters** or **arguments**: + +```python +def greet_person(name): + print(f"Hello, {name}!") + +greet_person("Alice") # Output: Hello, Alice! +greet_person("Bob") # Output: Hello, Bob! +``` + +### Multiple Parameters + +```python +def add_numbers(a, b): + result = a + b + print(f"{a} + {b} = {result}") + +add_numbers(5, 3) # Output: 5 + 3 = 8 +``` + +--- + +## The `return` Statement + +Functions can return values using the `return` keyword: + +```python +def multiply(x, y): + return x * y + +result = multiply(4, 5) +print(result) # Output: 20 +``` + +### Multiple Return Values + +Python functions can return multiple values as a tuple: + +```python +def get_name_age(): + name = "John" + age = 25 + return name, age + +person_name, person_age = get_name_age() +print(f"Name: {person_name}, Age: {person_age}") +# Output: Name: John, Age: 25 +``` + +### Functions Without Return + +If no `return` statement is used, the function returns `None`: + +```python +def say_hello(): + print("Hello!") + +result = say_hello() # Output: Hello! +print(result) # Output: None +``` + +--- + +## Default Arguments + +You can provide default values for parameters: + +```python +def greet_with_title(name, title="Mr."): + print(f"Hello, {title} {name}!") + +greet_with_title("Smith") # Output: Hello, Mr. Smith! +greet_with_title("Johnson", "Dr.") # Output: Hello, Dr. Johnson! +``` + +### Multiple Default Arguments + +```python +def create_profile(name, age=18, country="USA"): + print(f"Name: {name}, Age: {age}, Country: {country}") + +create_profile("Alice") # Name: Alice, Age: 18, Country: USA +create_profile("Bob", 25) # Name: Bob, Age: 25, Country: USA +create_profile("Charlie", 30, "Canada") # Name: Charlie, Age: 30, Country: Canada +``` + +--- + +## Keyword Arguments + +You can pass arguments by specifying the parameter name: + +```python +def book_info(title, author, year): + print(f"'{title}' by {author} ({year})") + +# Positional arguments +book_info("1984", "George Orwell", 1949) + +# Keyword arguments +book_info(author="Jane Austen", title="Pride and Prejudice", year=1813) + +# Mixed (positional first, then keyword) +book_info("Hamlet", author="Shakespeare", year=1600) +``` + +--- + +## Variable-Length Arguments: `*args` + +Use `*args` to accept any number of positional arguments: + +```python +def sum_all(*numbers): + total = 0 + for num in numbers: + total += num + return total + +print(sum_all(1, 2, 3)) # Output: 6 +print(sum_all(1, 2, 3, 4, 5)) # Output: 15 +print(sum_all(10)) # Output: 10 +``` + +### Combining Regular Parameters with \*args + +```python +def introduce(name, *hobbies): + print(f"Hi, I'm {name}!") + if hobbies: + print("My hobbies are:", ", ".join(hobbies)) + +introduce("Alice") # Hi, I'm Alice! +introduce("Bob", "reading", "swimming") # Hi, I'm Bob! + # My hobbies are: reading, swimming +``` + +--- + +## Variable-Length Keyword Arguments: `**kwargs` + +Use `**kwargs` to accept any number of keyword arguments: + +```python +def display_info(**info): + for key, value in info.items(): + print(f"{key}: {value}") + +display_info(name="John", age=25, city="New York") +# Output: +# name: John +# age: 25 +# city: New York +``` + +### Combining \*args and \*\*kwargs + +```python +def flexible_function(*args, **kwargs): + print("Positional arguments:", args) + print("Keyword arguments:", kwargs) + +flexible_function(1, 2, 3, name="Alice", age=30) +# Output: +# Positional arguments: (1, 2, 3) +# Keyword arguments: {'name': 'Alice', 'age': 30} +``` + +--- + +## Function Parameter Order + +When combining different types of parameters, follow this order: + +```python +def complete_function(required, default="value", *args, **kwargs): + print(f"Required: {required}") + print(f"Default: {default}") + print(f"Args: {args}") + print(f"Kwargs: {kwargs}") + +complete_function("must_have", "custom", 1, 2, 3, extra="info") +# Output: +# Required: must_have +# Default: custom +# Args: (1, 2, 3) +# Kwargs: {'extra': 'info'} +``` + +--- + +## Scope and Local vs Global Variables + +### Local Scope + +Variables defined inside a function are **local**: + +```python +def my_function(): + local_var = "I'm local" + print(local_var) + +my_function() # Output: I'm local +# print(local_var) # Error: local_var is not defined +``` + +### Global Scope + +Variables defined outside functions are **global**: + +```python +global_var = "I'm global" + +def access_global(): + print(global_var) # Can read global variable + +access_global() # Output: I'm global +``` + +### Modifying Global Variables + +Use the `global` keyword to modify global variables inside functions: + +```python +counter = 0 + +def increment(): + global counter + counter += 1 + +increment() +print(counter) # Output: 1 +``` + +--- + +## Docstrings + +Document your functions using docstrings: + +```python +def calculate_area(length, width): + """ + Calculate the area of a rectangle. + + Args: + length (float): The length of the rectangle + width (float): The width of the rectangle + + Returns: + float: The area of the rectangle + """ + return length * width + +# Access docstring +print(calculate_area.__doc__) +``` + +--- + +## Lambda Functions + +**Lambda functions** are small, anonymous functions defined using the `lambda` keyword: + +```python +# Regular function +def square(x): + return x ** 2 + +# Lambda equivalent +square_lambda = lambda x: x ** 2 + +print(square(5)) # Output: 25 +print(square_lambda(5)) # Output: 25 +``` + +### Lambda with Multiple Arguments + +```python +# Lambda with multiple arguments +add = lambda x, y: x + y +print(add(3, 7)) # Output: 10 + +# Lambda with default arguments +greet = lambda name="World": f"Hello, {name}!" +print(greet()) # Output: Hello, World! +print(greet("Alice")) # Output: Hello, Alice! +``` + +--- + +## Practical Examples + +### Example 1: Temperature Converter + +```python +def celsius_to_fahrenheit(celsius, precision=2): + """Convert Celsius to Fahrenheit with specified precision.""" + fahrenheit = (celsius * 9/5) + 32 + return round(fahrenheit, precision) + +print(celsius_to_fahrenheit(25)) # Output: 77.0 +print(celsius_to_fahrenheit(0, 1)) # Output: 32.0 +``` + +### Example 2: Shopping Cart Calculator + +```python +def calculate_total(*prices, tax_rate=0.08, discount=0): + """Calculate total price with tax and discount.""" + subtotal = sum(prices) + discounted = subtotal - (subtotal * discount) + total = discounted + (discounted * tax_rate) + return round(total, 2) + +# Usage examples +print(calculate_total(10.99, 25.50, 8.75)) +# Output: 48.87 + +print(calculate_total(10.99, 25.50, tax_rate=0.10, discount=0.15)) +# Output: 34.19 +``` + +### Example 3: User Registration System + +```python +def register_user(username, email, **additional_info): + """Register a new user with optional additional information.""" + user = { + "username": username, + "email": email, + "status": "active" + } + + # Add any additional information + user.update(additional_info) + + print(f"User {username} registered successfully!") + return user + +# Usage +new_user = register_user( + "alice_dev", + "alice@example.com", + age=28, + location="New York", + skills=["Python", "JavaScript"] +) + +print(new_user) +``` + +--- + +## Best Practices + +### 1. Use Descriptive Names + +```python +# Good +def calculate_monthly_payment(principal, rate, months): + return (principal * rate) / (1 - (1 + rate) ** -months) + +# Avoid +def calc(p, r, m): + return (p * r) / (1 - (1 + r) ** -m) +``` + +### 2. Keep Functions Small and Focused + +```python +# Good - Single responsibility +def validate_email(email): + return "@" in email and "." in email + +def send_welcome_email(email): + if validate_email(email): + print(f"Welcome email sent to {email}") + +# Better than one large function doing everything +``` + +### 3. Use Type Hints (Python 3.5+) + +```python +def add_numbers(a: int, b: int) -> int: + """Add two integers and return the result.""" + return a + b + +def greet_user(name: str, times: int = 1) -> None: + """Greet a user a specified number of times.""" + for _ in range(times): + print(f"Hello, {name}!") +``` + +--- + +## Summary + +| Concept | Description | Example | +| ------------ | ----------------------------- | ---------------------------- | +| `def` | Define a function | `def my_func():` | +| `return` | Return a value | `return result` | +| Parameters | Function inputs | `def func(a, b):` | +| Default args | Parameters with defaults | `def func(a, b=10):` | +| `*args` | Variable positional arguments | `def func(*args):` | +| `**kwargs` | Variable keyword arguments | `def func(**kwargs):` | +| Lambda | Anonymous function | `lambda x: x * 2` | +| Docstring | Function documentation | `"""Function description"""` | + +Functions are fundamental building blocks in Python that make code reusable, organized, and maintainable. Master these concepts to write clean and efficient Python programs! diff --git a/docs/python/python-oops.md b/docs/python/python-oops.md new file mode 100644 index 00000000..d34108ad --- /dev/null +++ b/docs/python/python-oops.md @@ -0,0 +1,185 @@ +--- +id: python-oops +title: Object-Oriented Programming +sidebar_label: OOPs in Python +sidebar_position: 11 +tags: + [ + Python, + List in Python, + Introduction of python, + Python Syntax, + Variables, + Operators, + Type Casting, + String, + Tuple in Python + Array in Python + Functions in Python + Recursion in Python + ] +--- + +# Object-Oriented Programming (OOPs) in Python + +Python is an **object-oriented programming language**, which means it supports concepts like **classes, objects, inheritance, polymorphism, encapsulation, and abstraction**. +OOP allows developers to structure programs so that properties and behaviors are bundled into objects, making code **modular, reusable, and easier to maintain**. + +--- + +## 🔹 What is OOP? + +- **Object-Oriented Programming (OOP)** is a programming paradigm based on the concept of **objects**. +- Each object can hold **data (attributes)** and **functions (methods)** that operate on that data. + + **Benefits of OOP in Python:** +- Reusability of code +- Encapsulation of data +- Modularity and abstraction +- Easier debugging and maintenance + +--- + +## 🔹 Classes and Objects + +A **class** is a blueprint for creating objects. +An **object** is an instance of a class. + +```python +# Defining a class +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + + def greet(self): + return f"Hello, my name is {self.name} and I am {self.age} years old." + +# Creating objects +p1 = Person("Alice", 25) +p2 = Person("Bob", 30) + +print(p1.greet()) # Output: Hello, my name is Alice and I am 25 years old. +print(p2.greet()) # Output: Hello, my name is Bob and I am 30 years old. +```` + + + +## 🔹 Attributes and Methods + +* **Attributes** → Variables inside a class. +* **Methods** → Functions defined inside a class. + +````python +class Car: + wheels = 4 # Class attribute + + def __init__(self, brand, model): + self.brand = brand # Instance attribute + self.model = model + + def info(self): # Instance method + return f"{self.brand} {self.model} has {Car.wheels} wheels." + +c1 = Car("Tesla", "Model S") +print(c1.info()) # Tesla Model S has 4 wheels. +```` + +## 🔹 Encapsulation + +Encapsulation means **restricting access** to certain variables/methods. +In Python, we use: + +* `_protected_var` → convention for protected members +* `__private_var` → name mangling for private members + +````python +class BankAccount: + def __init__(self, balance): + self.__balance = balance # Private variable + + def deposit(self, amount): + self.__balance += amount + + def get_balance(self): + return self.__balance + +acc = BankAccount(1000) +acc.deposit(500) +print(acc.get_balance()) # 1500 +```` + +--- + +## 🔹 Inheritance + +Inheritance allows a class (child) to acquire properties of another class (parent). + +````python +# Base class +class Animal: + def speak(self): + return "This is an animal." + +# Derived class +class Dog(Animal): + def speak(self): + return "Woof! Woof!" + +class Cat(Animal): + def speak(self): + return "Meow!" + +d = Dog() +c = Cat() +print(d.speak()) # Woof! Woof! +print(c.speak()) # Meow! +```` + + +## 🔹 Polymorphism + +Polymorphism allows the same method name to perform different tasks depending on the object. + +````python +for animal in [Dog(), Cat()]: + print(animal.speak()) + +# Output: +# Woof! Woof! +# Meow! +```` + + +## 🔹 Abstraction + +Abstraction means **hiding implementation details** and showing only the necessary functionality. +In Python, we use the `abc` module to create abstract classes. + +````python +from abc import ABC, abstractmethod + +class Shape(ABC): + @abstractmethod + def area(self): + pass + +class Circle(Shape): + def __init__(self, radius): + self.radius = radius + + def area(self): + return 3.14 * self.radius * self.radius + +c = Circle(5) +print(c.area()) # 78.5 +```` + +## 🔹 Summary + +* **Class & Object** → Blueprint and instance +* **Attributes & Methods** → Data and behavior inside a class +* **Encapsulation** → Restricting access +* **Inheritance** → Reusing parent class features +* **Polymorphism** → Same function name, different behavior +* **Abstraction** → Hiding unnecessary details \ No newline at end of file diff --git a/docs/python/python-recursion.md b/docs/python/python-recursion.md new file mode 100644 index 00000000..5fbcbf7f --- /dev/null +++ b/docs/python/python-recursion.md @@ -0,0 +1,172 @@ +--- +id: python-recursion +title: Recursion in Python +sidebar_label: Recursion in Python +sidebar_position: 12 +tags: + [ + Python, + List in Python, + Introduction of python, + Python Syntax, + Variables, + Operators, + Type Casting, + String, + Tuple in Python + ] + +--- + +# Recursion in Python + +**Recursion** is a programming technique where a function calls itself directly or indirectly to solve a problem. +It is often used to break down complex problems into smaller, simpler sub-problems. + +--- + +## Basic Structure of Recursion + +Every recursive function has two main parts: + +1. **Base Case** → Stops the recursion (prevents infinite calls). +2. **Recursive Case** → Function calls itself with modified input. + +**Example:** +```python +def countdown(n): + if n == 0: # Base case + print("Time's up!") + else: # Recursive case + print(n) + countdown(n-1) + +countdown(5) +``` + +```python +Output: +# 5 +# 4 +# 3 +# 2 +# 1 +# Time's up! +```` + +## Factorial using Recursion + +Factorial of `n` → `n! = n × (n-1) × (n-2) × ... × 1` + +```python +def factorial(n): + if n == 0 or n == 1: # Base case + return 1 + else: # Recursive case + return n * factorial(n-1) + +print(factorial(5)) # Output: 120 +``` + +## Fibonacci Series using Recursion + +Fibonacci sequence → 0, 1, 1, 2, 3, 5, 8, ... + +```python +def fibonacci(n): + if n <= 1: # Base case + return n + else: # Recursive case + return fibonacci(n-1) + fibonacci(n-2) + +for i in range(7): + print(fibonacci(i), end=" ") +# Output: 0 1 1 2 3 5 8 +``` + + +## Recursion vs Iteration + +* **Iteration (loops):** Uses `for` or `while` loops. +* **Recursion:** Function calls itself. + +**Example: Sum of first n numbers** + +Recursive: + +```python +def sum_recursive(n): + if n == 0: + return 0 + return n + sum_recursive(n-1) + +print(sum_recursive(5)) # Output: 15 +``` + +Iterative: + +```python +def sum_iterative(n): + total = 0 + for i in range(1, n+1): + total += i + return total + +print(sum_iterative(5)) # Output: 15 +``` + + +## Advantages of Recursion + + Makes code **shorter and cleaner** + Useful for problems naturally defined recursively (factorial, Fibonacci, tree traversal, divide and conquer algorithms) + + +## Disadvantages of Recursion + + **Slower execution** than iteration (due to repeated function calls) + **Memory usage is high** (function calls are stored in the call stack) + Risk of **stack overflow error** if base case is missing + + +## Tail Recursion in Python + +Tail recursion is when the **recursive call is the last statement** in the function. +Unlike some languages, Python **does not optimize tail recursion**, so deep recursion may cause errors. + +```python +def tail_sum(n, accumulator=0): + if n == 0: + return accumulator + return tail_sum(n-1, accumulator+n) + +print(tail_sum(5)) # Output: 15 +``` + + +## Practical Example: Binary Search (Recursive) + +```python +def binary_search(arr, target, low, high): + if low > high: + return -1 # Not found + + mid = (low + high) // 2 + + if arr[mid] == target: + return mid + elif arr[mid] < target: + return binary_search(arr, target, mid+1, high) + else: + return binary_search(arr, target, low, mid-1) + +nums = [1, 3, 5, 7, 9, 11] +print(binary_search(nums, 7, 0, len(nums)-1)) # Output: 3 +``` + +## Conclusion + +* Recursion is a function calling itself to solve smaller sub-problems. +* Every recursive function must have a **base case** to avoid infinite calls. +* Useful for problems like factorial, Fibonacci, searching, sorting, and tree/graph traversal. +* While recursion makes code elegant, it may be slower and consume more memory than iteration. \ No newline at end of file diff --git a/docs/python/setup-environment.md b/docs/python/setup-environment.md index cd4122e8..c9e8ff53 100644 --- a/docs/python/setup-environment.md +++ b/docs/python/setup-environment.md @@ -2,7 +2,7 @@ id: setup-environment title: Setting up your development environment sidebar_label: Setting up environment -sidebar_position: 12 +sidebar_position: 13 tags: [ html, diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 7e5f734f..bc7932ab 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -20,7 +20,6 @@ const config: Config = { projectName: "recode-website", onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", // Google Analytics and Theme Scripts scripts: [ @@ -203,10 +202,11 @@ const config: Config = { }, ], }, - { - type: "search", - position: "right", - }, + // Search disabled until Algolia is properly configured + // { + // type: "search", + // position: "right", + // }, { type: "html", position: "right", @@ -223,25 +223,29 @@ const config: Config = { theme: prismThemes.github, darkTheme: prismThemes.dracula, }, - algolia: { - appId: "YOUR_APP_ID", - apiKey: "YOUR_SEARCH_API_KEY", - indexName: "YOUR_INDEX_NAME", - contextualSearch: true, - externalUrlRegex: "external\\.com|domain\\.com", - replaceSearchResultPathname: { - from: "/docs/", - to: "/", - }, - searchParameters: {}, - searchPagePath: "search", - insights: false, - }, + // Disable Algolia search until properly configured + // algolia: { + // appId: "YOUR_APP_ID", + // apiKey: "YOUR_SEARCH_API_KEY", + // indexName: "YOUR_INDEX_NAME", + // contextualSearch: true, + // externalUrlRegex: "external\\.com|domain\\.com", + // replaceSearchResultPathname: { + // from: "/docs/", + // to: "/", + // }, + // searchParameters: {}, + // searchPagePath: "search", + // insights: false, + // }, } satisfies Preset.ThemeConfig, markdown: { mermaid: true, }, + + // Keep legacy setting until fully migrated to v4 + onBrokenMarkdownLinks: "warn", themes: ["@docusaurus/theme-mermaid"], diff --git a/package.json b/package.json index b42bb8b0..ac0ba67e 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", + "dev": "docusaurus start", "start": "docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", @@ -15,14 +16,14 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "^3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/plugin-google-analytics": "^3.8.1", - "@docusaurus/plugin-ideal-image": "3.8.1", - "@docusaurus/preset-classic": "^3.8.1", - "@docusaurus/theme-classic": "^3.8.1", - "@docusaurus/theme-mermaid": "3.8.1", - "@docusaurus/theme-search-algolia": "3.8.1", + "@docusaurus/core": "^3.9.1", + "@docusaurus/plugin-content-docs": "3.9.1", + "@docusaurus/plugin-google-analytics": "^3.9.1", + "@docusaurus/plugin-ideal-image": "3.9.1", + "@docusaurus/preset-classic": "^3.9.1", + "@docusaurus/theme-classic": "^3.9.1", + "@docusaurus/theme-mermaid": "3.9.1", + "@docusaurus/theme-search-algolia": "3.9.1", "@floating-ui/react": "^0.27.8", "@giscus/react": "^3.1.0", "@mdx-js/react": "^3.0.0", @@ -57,9 +58,9 @@ "vanilla-tilt": "^1.8.1" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^3.8.1", - "@docusaurus/tsconfig": "^3.8.1", - "@docusaurus/types": "^3.8.1", + "@docusaurus/module-type-aliases": "^3.9.1", + "@docusaurus/tsconfig": "^3.9.1", + "@docusaurus/types": "^3.9.1", "@tailwindcss/postcss": "^4.1.4", "@types/canvas-confetti": "^1.9.0", "@types/react": "^19.1.9", diff --git a/sidebars.ts b/sidebars.ts index 6ca96213..5fe49db8 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -90,6 +90,8 @@ const sidebars: SidebarsConfig = { 'python/python-array', 'python/python-conditional-statements', 'python/python-loops', + 'python/python-functions', + 'python/python-errors-and-exceptions', ], }, { diff --git a/src/components/Community/index.tsx b/src/components/Community/index.tsx index b989b5b0..9d34126f 100644 --- a/src/components/Community/index.tsx +++ b/src/components/Community/index.tsx @@ -33,7 +33,7 @@ export const LandingCommunity: FC = ({ className }) => { { stat: githubReposCount, statText: githubReposCountText, - description: "Live public projects on RecodHive, demonstrating the power of open-source collaboration.", + description: "Live public projects on Recode Hive, demonstrating the power of open-source collaboration.", href: "https://github.com/orgs/recodehive/repositories?q=visibility%3Apublic+archived%3Afalse", label: "Public Repositories" }, @@ -129,7 +129,7 @@ export const LandingCommunity: FC = ({ className }) => { handleCardClick("https://github.com/recodehive"); } }} - title="Click to visit RecodHive GitHub Organization" + title="Click to visit Recode Hive GitHub Organization" > = ({ className }) => { loading="lazy" />
- Our developers are the core of RecodHive community. We take pride in + Our developers are the core of Recode Hive community. We take pride in our{" "} GitHub organization @@ -147,7 +147,7 @@ export const LandingCommunity: FC = ({ className }) => { contributors and maintainers {" "} - powering RecodHive's growth. + powering Recode Hive's growth.
Click to explore our GitHub diff --git a/src/components/FloatingContributors/FloatingContributors.css b/src/components/FloatingContributors/FloatingContributors.css index 755b56bb..f5fbbdaf 100644 --- a/src/components/FloatingContributors/FloatingContributors.css +++ b/src/components/FloatingContributors/FloatingContributors.css @@ -30,7 +30,7 @@ -webkit-backdrop-filter: blur(20px); border: none; border-radius: 20px; - padding: 18px; + padding: 20px; box-shadow: 0 15px 35px rgba(108, 74, 232, 0.15), 0 5px 15px rgba(0, 0, 0, 0.05); @@ -40,12 +40,12 @@ color: #1a202c; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-width: 330px; - width: 330px; /* Set fixed width */ - height: 290px; /* Reduced fixed height */ + width: 330px; + min-height: 290px; /* Increased min-height for better content fit */ transition: all 0.4s ease; display: flex; flex-direction: column; - justify-content: space-between; /* Distribute space evenly */ + gap: 12px; } /* New activity feed styles */ @@ -210,13 +210,14 @@ /* Header embedded version - larger size */ .floating-contributors-container.header-embedded .floating-contributors-card { min-width: 450px; - width: 450px; /* Set fixed width */ - height: 370px; /* Updated height as requested */ - padding: 28px; + width: 450px; + min-height: 370px; /* content fit */ + padding: 24px; border-radius: 24px; box-shadow: 0 15px 35px rgba(108, 74, 232, 0.12), 0 5px 15px rgba(0, 0, 0, 0.03); + gap: 16px; } @@ -378,11 +379,10 @@ /* Header */ .floating-contributors-header { - margin-bottom: 10px; padding-right: 30px; display: flex; flex-direction: column; - gap: 2px; + gap: 4px; } .floating-contributors-title { @@ -424,18 +424,17 @@ .floating-contributors-activity { display: flex; align-items: center; - gap: 10px; - padding: 10px 14px; + gap: 12px; + padding: 12px 16px; background: rgba(255, 255, 255, 0.4); border-radius: 14px; - margin-bottom: 10px; border: none; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; position: relative; overflow: hidden; - height: 60px; /* Slightly reduced height */ - box-sizing: border-box; /* Include padding in height calculation */ + min-height: 64px; + box-sizing: border-box; box-shadow: 0 2px 8px rgba(108, 74, 232, 0.08); } @@ -540,17 +539,18 @@ .activity-details { flex: 1; min-width: 0; - overflow: hidden; /* Hide overflow */ - max-width: calc(100% - 60px); /* Account for avatar + padding */ + overflow: hidden; + max-width: calc(100% - 60px); display: flex; flex-direction: column; + gap: 2px; } .activity-user { display: flex; align-items: center; - gap: 6px; - margin-bottom: 3px; + gap: 8px; + flex-wrap: wrap; } .activity-username { @@ -605,13 +605,12 @@ .activity-message { font-size: 13px; - margin: 2px 0; color: rgba(60, 60, 60, 0.8); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; - line-height: 1.3; + line-height: 1.4; } [data-theme='light'] .activity-message { @@ -620,22 +619,21 @@ /* Contributors grid */ .floating-contributors-grid { - margin-bottom: 0px; - flex: 0 0 auto; /* Don't grow, don't shrink, use auto height */ + flex: 0 0 auto; display: flex; flex-direction: column; - overflow: hidden; /* Hide overflow */ + overflow: visible; + gap: 8px; } .contributors-grid-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #333; - padding: 3%; + padding: 8px 12px; background: rgba(205, 205, 205, 0.562); border-radius: 12px; box-shadow: none; @@ -663,19 +661,23 @@ .contributors-avatars { display: flex; flex-wrap: wrap; - gap: 10px; + gap: 8px; align-items: center; - max-height: 50px; /* Set fixed height */ - padding: 10px; /* Add padding around all avatars */ + padding: 12px; background: rgba(255, 255, 255, 0.3); border-radius: 12px; - margin-top: 8px; box-shadow: inset 0 2px 6px rgba(108, 74, 232, 0.05); + overflow: visible; } .contributor-avatar-wrapper { position: relative; cursor: pointer; + z-index: 1; +} + +.contributor-avatar-wrapper:hover { + z-index: 10; } .contributor-avatar { @@ -730,22 +732,22 @@ .contributor-tooltip { position: absolute; - bottom: 100%; + bottom: calc(100% + 8px); left: 50%; - transform: translateX(-50%) translateY(4px); - background: rgba(0, 0, 0, 0.9); + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.92); color: white; - padding: 8px 12px; - border-radius: 8px; + padding: 6px 10px; + border-radius: 6px; font-size: 11px; white-space: nowrap; opacity: 0; visibility: hidden; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - margin-bottom: 8px; - z-index: 20; + transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease; + z-index: 100; pointer-events: none; backdrop-filter: blur(8px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .contributor-tooltip::after { @@ -754,14 +756,14 @@ top: 100%; left: 50%; transform: translateX(-50%); - border: 4px solid transparent; - border-top-color: rgba(0, 0, 0, 0.9); + border: 5px solid transparent; + border-top-color: rgba(0, 0, 0, 0.92); } .contributor-avatar-wrapper:hover .contributor-tooltip { opacity: 1; visibility: visible; - transform: translateX(-50%) translateY(-8px); + transform: translateX(-50%) translateY(-4px); } .tooltip-name, @@ -784,13 +786,20 @@ display: flex; align-items: center; justify-content: center; - font-size: 11px; + font-size: 8px; font-weight: 600; color: rgba(255, 255, 255, 0.7); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; } +.contributors-more span { + font-size: 8px; + line-height: 1; + display: inline-block; + text-align: center; +} + .contributors-more:hover { background: rgba(102, 126, 234, 0.2); border-color: rgba(102, 126, 234, 0.5); @@ -812,9 +821,7 @@ /* Footer */ .floating-contributors-footer { - padding-top: 8px; - margin-top: auto; /* Push to bottom of flex container */ - margin-bottom: 0; /* Remove bottom spacing */ + margin-top: auto; } [data-theme='light'] .floating-contributors-footer { @@ -827,7 +834,7 @@ justify-content: center; gap: 8px; width: 100%; - padding: 10px 0; + padding: 12px 16px; background: linear-gradient(90deg, #4f46e5 0%, #6366f1 100%); color: white; text-decoration: none; @@ -838,7 +845,7 @@ position: relative; overflow: hidden; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.25); - height: 42px; /* Fixed height */ + min-height: 44px; border: none; } @@ -954,8 +961,9 @@ } .floating-contributors-card { - padding: 16px; + padding: 18px; border-radius: 16px; + gap: 10px; } .floating-contributors-title { @@ -986,6 +994,13 @@ font-size: 10px; } +.contributors-more span { + font-size: 6.5px; + line-height: 1; + display: inline-block; + text-align: center; +} + .contributors-cta { padding: 10px 14px; font-size: 13px; @@ -1012,12 +1027,14 @@ } .floating-contributors-card { - padding: 14px; + padding: 16px; border-radius: 14px; + gap: 8px; } .floating-contributors-activity { - padding: 25px; + padding: 10px 12px; + min-height: 56px; } .activity-details { @@ -1042,6 +1059,13 @@ height: 24px; font-size: 9px; } + + .contributors-more span { + font-size: 6.5px; + line-height: 1; + display: inline-block; + text-align: center; + } } /* Extra small screens */ @@ -1095,7 +1119,7 @@ .status-dot { animation: none !important; } - + .floating-contributors-card { transform: none !important; } @@ -1107,11 +1131,11 @@ border-width: 2px; border-color: rgba(255, 255, 255, 0.8); } - + .activity-avatar { border-width: 3px; } - + .contributor-avatar { border-width: 3px; } @@ -1216,4 +1240,4 @@ .contributor-avatar, .activity-avatar { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} \ No newline at end of file +} diff --git a/src/components/FloatingContributors/index.tsx b/src/components/FloatingContributors/index.tsx index df362f80..e6f4855d 100644 --- a/src/components/FloatingContributors/index.tsx +++ b/src/components/FloatingContributors/index.tsx @@ -5,25 +5,25 @@ import './FloatingContributors.css'; // Format relative time (e.g., "2 hours ago") const formatTimeAgo = (date: Date): string => { const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); - + let interval = Math.floor(seconds / 31536000); if (interval > 1) return `${interval} years ago`; - + interval = Math.floor(seconds / 2592000); if (interval > 1) return `${interval} months ago`; - + interval = Math.floor(seconds / 86400); if (interval > 1) return `${interval} days ago`; if (interval === 1) return `1 day ago`; - + interval = Math.floor(seconds / 3600); if (interval > 1) return `${interval} hours ago`; if (interval === 1) return `1 hour ago`; - + interval = Math.floor(seconds / 60); if (interval > 1) return `${interval} minutes ago`; if (interval === 1) return `1 minute ago`; - + return `just now`; }; @@ -115,7 +115,7 @@ const FloatingContributors: React.FC = ({ headerEmbed }, { login: 'recodehive-team', - avatar_url: 'https://avatars.githubusercontent.com/u/150000000?v=4', + avatar_url: 'https://avatars.githubusercontent.com/u/150000000?v=4', html_url: 'https://github.com/recodehive', }, { @@ -130,11 +130,11 @@ const FloatingContributors: React.FC = ({ headerEmbed }, { login: 'coder', - avatar_url: 'https://avatars.githubusercontent.com/u/6154722?v=4', + avatar_url: 'https://avatars.githubusercontent.com/u/6154722?v=4', html_url: 'https://github.com/coder', }, ]; - + const actions: ContributorActivity['action'][] = ['pushed', 'created', 'merged', 'opened', 'commented']; const timeOffsets = [5, 10, 30, 60, 120, 240, 480]; // minutes const messages = [ @@ -146,11 +146,11 @@ const FloatingContributors: React.FC = ({ headerEmbed 'Updated dependencies', 'Fixed typo in README' ]; - + return fallbackContributors.map((contributor, index) => { const now = new Date(); const timestamp = new Date(now.getTime() - (timeOffsets[index % timeOffsets.length] * 60 * 1000)); - + return { id: `fallback-${index}`, contributor: { @@ -165,21 +165,21 @@ const FloatingContributors: React.FC = ({ headerEmbed }; }); }, []); - + // Fetch live data from GitHub const fetchLiveData = useCallback(async () => { try { // Use specific cache key for this repository's events const CACHE_KEY = 'recodehive_website_events'; const CACHE_DURATION = 2 * 60 * 1000; // 2 minutes - short for "live" data - + // Check if we have recent data already const now = Date.now(); if (lastFetched && now - lastFetched < 30000) { // Don't fetch more than once every 30 seconds return; } - + // Check for cached events let events: GitHubEvent[] = []; if (typeof window !== 'undefined') { @@ -195,20 +195,20 @@ const FloatingContributors: React.FC = ({ headerEmbed console.warn('Error retrieving cached events', e); } } - + // If no valid cache, fetch fresh data if (events.length === 0) { setLoading(true); - + // Fetch repository events from GitHub API const eventsResponse = await fetch('https://api.github.com/repos/recodehive/recode-website/events?per_page=30'); - + if (!eventsResponse.ok) { throw new Error(`GitHub API error: ${eventsResponse.status}`); } - + events = await eventsResponse.json(); - + // Save to cache if (typeof window !== 'undefined' && Array.isArray(events)) { try { @@ -221,7 +221,7 @@ const FloatingContributors: React.FC = ({ headerEmbed } } } - + // Process events into activities if (Array.isArray(events) && events.length > 0) { // Convert GitHub events to our activity format @@ -229,7 +229,7 @@ const FloatingContributors: React.FC = ({ headerEmbed // Map GitHub event types to our action types let action: ContributorActivity['action'] = 'other'; let message: string | undefined; - + switch (event.type) { case 'PushEvent': action = 'pushed'; @@ -252,9 +252,9 @@ const FloatingContributors: React.FC = ({ headerEmbed default: action = 'other'; } - + const timestamp = new Date(event.created_at); - + return { id: event.id, contributor: { @@ -268,21 +268,21 @@ const FloatingContributors: React.FC = ({ headerEmbed timeAgo: formatTimeAgo(timestamp), }; }); - + // Update only if we have events if (newActivities.length > 0) { setActivities(newActivities); - + // Extract contributors from these events const contributorsMap = new Map(); - + // Also fetch contributors directly for contribution counts try { const contributorsResponse = await fetch('https://api.github.com/repos/recodehive/recode-website/contributors?per_page=100'); - + if (contributorsResponse.ok) { const contributorsData = await contributorsResponse.json(); - + if (Array.isArray(contributorsData)) { contributorsData.forEach(contributor => { if (contributor.login && contributor.type === 'User') { @@ -299,7 +299,7 @@ const FloatingContributors: React.FC = ({ headerEmbed } } catch (error) { console.warn('Error fetching contributors:', error); - + // If we couldn't get contributors data, at least use actors from events events.forEach(event => { const login = event.actor.login; @@ -314,17 +314,17 @@ const FloatingContributors: React.FC = ({ headerEmbed } }); } - + // Update contributors if we found any if (contributorsMap.size > 0) { setContributors(Array.from(contributorsMap.values())); } } } - + setLastFetched(now); setLoading(false); - + // Set up next refresh if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); @@ -332,15 +332,15 @@ const FloatingContributors: React.FC = ({ headerEmbed refreshTimerRef.current = setTimeout(() => { fetchLiveData(); }, 60000); // Refresh every minute - + } catch (error) { console.warn('Error fetching GitHub events:', error); - + // Use fallback data if we have no activities yet if (activities.length === 0) { const fallbackActivities = createFallbackActivities(); setActivities(fallbackActivities); - + // Create fallback contributors const contributorsMap = new Map(); fallbackActivities.forEach(activity => { @@ -355,22 +355,22 @@ const FloatingContributors: React.FC = ({ headerEmbed }); } }); - + setContributors(Array.from(contributorsMap.values())); } - + setLoading(false); } }, [activities.length, createFallbackActivities, lastFetched]); - + // Initialize component and start data fetching useEffect(() => { // Set loading state setLoading(true); - + // Fetch data immediately fetchLiveData(); - + // Clean up on unmount return () => { if (refreshTimerRef.current) { @@ -378,22 +378,22 @@ const FloatingContributors: React.FC = ({ headerEmbed } }; }, [fetchLiveData]); - + // Cycle through activities useEffect(() => { if (activities.length <= 1) return; - + const interval = setInterval(() => { setCurrentActivityIndex((prev) => (prev + 1) % activities.length); }, 4000); - + return () => clearInterval(interval); }, [activities.length]); - + // Get GitHub URL for event const getGitHubEventUrl = (activity: ContributorActivity): string => { const repoUrl = 'https://github.com/recodehive/recode-website'; - + switch (activity.action) { case 'pushed': return `${repoUrl}/commits`; @@ -409,7 +409,7 @@ const FloatingContributors: React.FC = ({ headerEmbed return repoUrl; } }; - + // Get icon for action type const getActionIcon = (action: ContributorActivity['action']): string => { switch (action) { @@ -422,7 +422,7 @@ const FloatingContributors: React.FC = ({ headerEmbed default: return '💻'; } }; - + // Get text for action type const getActionText = (action: ContributorActivity['action']): string => { switch (action) { @@ -435,15 +435,15 @@ const FloatingContributors: React.FC = ({ headerEmbed default: return 'ACTIVE'; } }; - + // Don't render anything while initial loading if (loading && activities.length === 0) { return null; } - + // Get current activity to display const currentActivity = activities[currentActivityIndex]; - + return ( {isVisible && ( @@ -519,7 +519,7 @@ const FloatingContributors: React.FC = ({ headerEmbed
- +
@{currentActivity.contributor.login} @@ -543,7 +543,7 @@ const FloatingContributors: React.FC = ({ headerEmbed Recent Contributors {contributors.length}
- +
{contributors .sort((a, b) => b.contributions - a.contributions) // Sort contributors by contributions in descending order @@ -554,7 +554,7 @@ const FloatingContributors: React.FC = ({ headerEmbed className="contributor-avatar-wrapper" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} - transition={{ + transition={{ delay: index * 0.05, type: "spring", stiffness: 300, @@ -562,9 +562,9 @@ const FloatingContributors: React.FC = ({ headerEmbed }} whileHover={{ scale: 1.1, zIndex: 5 }} > - = ({ headerEmbed ))} - - {contributors.length > 12 && ( + + {contributors.length > 5 && (
- +{contributors.length - 12} more + +{contributors.length - 5}
)}
diff --git a/src/components/dashboard/LeaderBoard/PRListModal.tsx b/src/components/dashboard/LeaderBoard/PRListModal.tsx index 04e1ca10..9ee724ee 100644 --- a/src/components/dashboard/LeaderBoard/PRListModal.tsx +++ b/src/components/dashboard/LeaderBoard/PRListModal.tsx @@ -11,6 +11,7 @@ interface PRDetails { mergedAt: string; repoName: string; number: number; + points: number; // Now includes the points field } interface Contributor { @@ -40,6 +41,9 @@ export default function PRListModal({ contributor, isOpen, onClose }: PRListModa // Get filtered PRs instead of using contributor.prDetails const filteredPRs = getFilteredPRsForContributor(contributor.username); + // Calculate total points from filtered PRs + const totalPoints = filteredPRs.reduce((sum, pr) => sum + pr.points, 0); + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { @@ -72,6 +76,14 @@ export default function PRListModal({ contributor, isOpen, onClose }: PRListModa } }; + // Helper function to get badge color based on points + const getPointsBadgeColor = (points: number) => { + if (points >= 50) return '#10b981'; // Green for Level 3 + if (points >= 30) return '#f59e0b'; // Orange for Level 2 + if (points >= 10) return '#3b82f6'; // Blue for Level 1 + return '#6b7280'; // Gray for no points + }; + return ( {isOpen && ( @@ -107,8 +119,8 @@ export default function PRListModal({ contributor, isOpen, onClose }: PRListModa {contributor.username}'s Pull Requests

- {/*Show filtered count and add filter info */} - {filteredPRs.length} merged PR{filteredPRs.length !== 1 ? 's' : ''} • {filteredPRs.length * 10} points + {/* Show filtered count with actual total points */} + {filteredPRs.length} merged PR{filteredPRs.length !== 1 ? 's' : ''} • {totalPoints} point{totalPoints !== 1 ? 's' : ''} {currentTimeFilter !== 'all' && ( ({getFilterDisplayText(currentTimeFilter)}) @@ -128,7 +140,7 @@ export default function PRListModal({ contributor, isOpen, onClose }: PRListModa {/* Modal Body */}

- {/*Use filteredPRs instead of contributor.prDetails */} + {/* Use filteredPRs instead of contributor.prDetails */} {filteredPRs && filteredPRs.length > 0 ? (
{filteredPRs.map((pr, index) => ( @@ -144,6 +156,23 @@ export default function PRListModal({ contributor, isOpen, onClose }: PRListModa {pr.title}
+ {/* Points badge */} + {pr.points > 0 && ( + + +{pr.points} pts + + )} @@ -335,35 +335,38 @@ export function CommunityStatsProvider({ children }: CommunityStatsProviderProps // Process results from this batch results.forEach(({ mergedPRs, repoName }) => { - totalMergedPRs += mergedPRs.length; - mergedPRs.forEach((pr) => { - const username = pr.user.login; - if (!contributorMap.has(username)) { - contributorMap.set(username, { - username, - avatar: pr.user.avatar_url, - profile: pr.user.html_url, - points: 0, // Will be calculated later based on filter - prs: 0, // Will be calculated later based on filter - allPRDetails: [], // Store all PRs here - }); - } - const contributor = contributorMap.get(username)!; - // Calculate points for this PR based on labels const prPoints = calculatePointsForPR(pr.labels); - // Add detailed PR information to the full list - if (pr.title && pr.html_url && pr.merged_at && pr.number) { - contributor.allPRDetails.push({ - title: pr.title, - url: pr.html_url, - mergedAt: pr.merged_at, - repoName, - number: pr.number, - points: prPoints, - }); + // ONLY store PRs that have points (i.e., have "recode" label and a level label) + if (prPoints > 0) { + totalMergedPRs++; + + const username = pr.user.login; + if (!contributorMap.has(username)) { + contributorMap.set(username, { + username, + avatar: pr.user.avatar_url, + profile: pr.user.html_url, + points: 0, // Will be calculated later based on filter + prs: 0, // Will be calculated later based on filter + allPRDetails: [], // Store only valid PRs here + }); + } + const contributor = contributorMap.get(username)!; + + // Add detailed PR information only if it has all required fields + if (pr.title && pr.html_url && pr.merged_at && pr.number) { + contributor.allPRDetails.push({ + title: pr.title, + url: pr.html_url, + mergedAt: pr.merged_at, + repoName, + number: pr.number, + points: prPoints, + }); + } } }); }); diff --git a/src/pages/careers/index.tsx b/src/pages/careers/index.tsx index 78191b50..3260f109 100644 --- a/src/pages/careers/index.tsx +++ b/src/pages/careers/index.tsx @@ -215,13 +215,11 @@ function CareersContent() { variants={fadeIn} > View Open Positions Learn About Our Culture diff --git a/src/pages/dashboard/dashboard.css b/src/pages/dashboard/dashboard.css index 866f0e74..6a2480e5 100644 --- a/src/pages/dashboard/dashboard.css +++ b/src/pages/dashboard/dashboard.css @@ -128,7 +128,7 @@ .menu-item.active { background: var(--ifm-color-primary-lightest); - color: var(--ifm-color-primary); + color: #fdfffe; border-right: 3px solid var(--ifm-color-primary); } diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx index 5706a7f9..eac0efe5 100644 --- a/src/theme/Navbar/Content/index.tsx +++ b/src/theme/Navbar/Content/index.tsx @@ -6,10 +6,10 @@ import { } from '@docusaurus/theme-common/internal'; import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem'; import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; -import SearchBar from '@theme/SearchBar'; +// import SearchBar from '@theme/SearchBar'; import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; import NavbarLogo from '@theme/Navbar/Logo'; -import NavbarSearch from '@theme/Navbar/Search'; +// import NavbarSearch from '@theme/Navbar/Search'; @@ -79,11 +79,12 @@ export default function NavbarContent(): ReactNode { <> - {!searchBarItem && ( + {/* Search component disabled */} + {/* {!searchBarItem && ( - )} + )} */} } />