diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index eb106ba..3901cb1 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,10 +1,9 @@ --- name: Bug Report about: Reporting bugs in existing code -title: '' +title: "" labels: bug, good first issue -assignees: '' - +assignees: "" --- ## The Problem diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index 2a85514..7e53c61 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,10 +1,9 @@ --- name: Documentation about: Report issues or additions to Documentation -title: '' +title: "" labels: documentation, good first issue -assignees: '' - +assignees: "" --- ## Description of what to add diff --git a/.github/ISSUE_TEMPLATE/feature-change-request.md b/.github/ISSUE_TEMPLATE/feature-change-request.md index 7cc49cc..dd91022 100644 --- a/.github/ISSUE_TEMPLATE/feature-change-request.md +++ b/.github/ISSUE_TEMPLATE/feature-change-request.md @@ -1,10 +1,9 @@ --- name: Feature-Change Request about: Suggest a change in an existing feature -title: '' +title: "" labels: change, good first issue -assignees: '' - +assignees: "" --- ## What needs to change diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 8864bec..4bc541e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,10 +1,9 @@ --- name: Feature Request about: Suggest a new feature for this project -title: '' +title: "" labels: feature, good first issue -assignees: '' - +assignees: "" --- ## Description of the feature diff --git a/.github/ISSUE_TEMPLATE/refactoring.md b/.github/ISSUE_TEMPLATE/refactoring.md index 7fca9e2..6fec0d5 100644 --- a/.github/ISSUE_TEMPLATE/refactoring.md +++ b/.github/ISSUE_TEMPLATE/refactoring.md @@ -1,10 +1,9 @@ --- name: Refactoring Request about: Suggest refactoring or code quality improvements -title: '' +title: "" labels: refactoring, good first issue -assignees: '' - +assignees: "" --- ## Area to Refactor @@ -20,9 +19,7 @@ assignees: '' - describe the refactoring approach in bullet points - > [!NOTE] > **CONTRIBUTIONS ARE WELCOME!** > If you want to get this issue assigned to you, just comment `assign this issue to me`. > You will be assigned to the issue instantly via GitHub-actions bot. - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e56a4c0..1a23f9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,12 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Validate Locales + run: pnpm validate-locales + + - name: Check Formatting + run: pnpm format:check + - name: Type check run: pnpm exec tsc --noEmit diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..2e6b87e --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +pnpm exec commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..5ee7abd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm exec lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fef4326 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +.next +node_modules +dist +build +coverage +public +pnpm-lock.yaml +*.min.* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ec2b289 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "bracketSpacing": true, + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 74f4bb2..38a14ff 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ce133b..601bf25 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,10 @@ > [!WARNING] > **Important Note on AI-Generated Contributions** -> -> While we appreciate the use of AI as a productivity tool, pull requests consisting of code or documentation generated entirely by AI **without significant human review and testing** are not welcome. -> +> +> While we appreciate the use of AI as a productivity tool, pull requests consisting of code or documentation generated entirely by AI **without significant human review and testing** are not welcome. +> > Every contributor is responsible for the code they submit. If we suspect a contribution is a "blind" AI generation that has not been verified for logic, security, or style, it will be closed without review. - - --- # Contributing to DevImpact @@ -48,16 +46,19 @@ Thank you for your interest in contributing to DevImpact! This guide will help y ### Installation 1. Install dependencies: + ```bash pnpm install ``` 2. Create a `.env` file in the project root (see `.env.example`): + ``` GITHUB_TOKEN=your_github_token_here ``` 3. Start the development server: + ```bash pnpm dev ``` @@ -90,6 +91,7 @@ DevImpact/ ## Making Changes 1. **Sync your fork** with the latest upstream changes: + ```bash git fetch upstream git checkout main @@ -97,6 +99,7 @@ DevImpact/ ``` 2. **Create a feature branch** from `main`: + ```bash git checkout -b feat/your-feature-name ``` @@ -104,11 +107,13 @@ DevImpact/ 3. **Make your changes** and test them locally. 4. **Run the linter** before committing: + ```bash pnpm lint ``` 5. **Commit your changes** with a clear message: + ```bash git commit -m "feat: add your feature description" ``` diff --git a/README.md b/README.md index 7f8932a..1ef09d3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ # πŸš€ DevImpact -

License @@ -36,9 +35,6 @@ CSS Β· Tailwind

- - - **DevImpact** is an open-source platform that compares software developers based on their real impact in the open-source ecosystem β€” not just raw numbers. It evaluates developers using a smart scoring system that considers: @@ -141,7 +137,6 @@ Final Score = --- - ## πŸ› οΈ Tech Stack ### Frontend @@ -163,7 +158,7 @@ Final Score = ### ⚑ Option A: Run Full Platform via Docker (Fastest) -Run the entire platform (Web App UI + PostgreSQL + Redis + Worker) in 2 simple steps: +Run the entire platform (Web App UI + PostgreSQL + Redis) in 2 simple steps: ```bash # 1. Copy environment template and set GITHUB_TOKEN @@ -172,6 +167,7 @@ cp .env.example .env # 2. Start full platform docker compose -f ops/docker/docker-compose.yml up -d --build ``` + Then open `http://localhost:3000` in your browser! --- @@ -179,21 +175,25 @@ Then open `http://localhost:3000` in your browser! ### πŸ“¦ Option B: Run Locally with Node.js & pnpm 1. **Install dependencies**: + ```bash pnpm install ``` 2. **Configure environment**: + ```bash cp .env.example .env ``` 3. **Start local database & Redis**: + ```bash pnpm db:up && pnpm redis:up ``` 4. **Run development server**: + ```bash pnpm run dev ``` @@ -218,7 +218,6 @@ docker compose -f ops/docker/leaderboard-compose.yml up -d --- - ## 🌍 Localization - Supported languages: English πŸ‡ΊπŸ‡Έ, Arabic πŸ‡ΈπŸ‡¦ diff --git a/SECURITY.md b/SECURITY.md index bc0e133..b91e2b4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,32 +4,38 @@ We actively provide security updates for the following versions of DevImpact: -| Version | Supported | -| ------- | ------------------ | -| Main | βœ… Yes | -| < 1.0.0 | ❌ No | +| Version | Supported | +| ------- | --------- | +| Main | βœ… Yes | +| < 1.0.0 | ❌ No | ## Reporting a Vulnerability We take the security of DevImpact seriously. If you discover a security vulnerability, please do not open a public issue. Instead, follow the steps below: ### How to report + 1. **Email:** Please send a detailed report to osama.f.mabkhot@gmail.com or use GitHub's Private Vulnerability Reporting. 2. **Details:** Include a description of the vulnerability, steps to reproduce, and the potential impact. 3. **Response:** You can expect an acknowledgment within 48 hours. ### Scope + This policy covers the core DevImpact application, its scoring logic, and how it handles the `GITHUB_TOKEN`. It does not cover the GitHub API itself or third-party dependencies (though we appreciate reports regarding how we use them). ## Best Practices for Contributors + To keep this project secure, please keep the following in mind: -* **Environment Variables:** Never commit your `.env` file. It contains your `GITHUB_TOKEN`. -* **Data Sanitization:** Ensure all data fetched from the GitHub GraphQL API is sanitized before being rendered in the UI to prevent XSS. -* **Dependency Updates:** We use automated tools to keep our dependencies up to date. Please ensure your PRs do not introduce insecure or outdated packages. + +- **Environment Variables:** Never commit your `.env` file. It contains your `GITHUB_TOKEN`. +- **Data Sanitization:** Ensure all data fetched from the GitHub GraphQL API is sanitized before being rendered in the UI to prevent XSS. +- **Dependency Updates:** We use automated tools to keep our dependencies up to date. Please ensure your PRs do not introduce insecure or outdated packages. ## Security Controls -* **Code Scanning:** We use GitHub Actions to run automated security scans on every Pull Request. -* **Secret Scanning:** GitHub's secret scanning is enabled to prevent the accidental leak of tokens. + +- **Code Scanning:** We use GitHub Actions to run automated security scans on every Pull Request. +- **Secret Scanning:** GitHub's secret scanning is enabled to prevent the accidental leak of tokens. --- -*Thank you for helping keep DevImpact safe for the open-source community!* + +_Thank you for helping keep DevImpact safe for the open-source community!_ diff --git a/algorithm.md b/algorithm.md index 93891cb..9c39a31 100644 --- a/algorithm.md +++ b/algorithm.md @@ -1,9 +1,7 @@ # DevImpact - - - ### 🧠 Main + ``` compareUsers(user1, user2): @@ -17,6 +15,7 @@ ``` ### 🧠 User Score + ``` calculateUserScore(user): @@ -37,6 +36,7 @@ ``` ### πŸ“¦ Repository Score + ``` calculateRepoScore(repos): @@ -65,8 +65,8 @@ RETURN total ``` - ### πŸ”₯ Pull Request Score + ``` calculatePRScore(prs, username): @@ -119,8 +119,8 @@ RETURN totalScore ``` - ### 🌍 Contribution Score (Activity) + ``` calculateContributionScore(contributions): @@ -134,4 +134,4 @@ issues * 0.3 RETURN score -``` \ No newline at end of file +``` diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts index c2e2168..8aa2db4 100644 --- a/app/api/compare/route.ts +++ b/app/api/compare/route.ts @@ -44,22 +44,15 @@ type ComparedUserResult = { normalizedFinalScore: number; topRepos: ReturnType["topRepos"]; topPullRequests: ReturnType["topPullRequests"]; - topCommunityContributions: ReturnType< - typeof calculateUserScore - >["topCommunityContributions"]; + topCommunityContributions: ReturnType["topCommunityContributions"]; languageScores: ReturnType["languageScores"]; signals: ReturnType["signals"]; explanations: ReturnType["explanations"]; }; -type ClientSafeError = Pick< - SafeApiError, - "code" | "message" | "targetUsernames" ->; +type ClientSafeError = Pick; -function parseSelectedLanguagesFromSearchParams( - searchParams: URLSearchParams, -): string[] { +function parseSelectedLanguagesFromSearchParams(searchParams: URLSearchParams): string[] { const fromRepeated = searchParams.getAll("selectedLanguage"); const fromCsv = searchParams .get("selectedLanguages") @@ -67,10 +60,7 @@ function parseSelectedLanguagesFromSearchParams( .map((language) => language.trim()) .filter(Boolean); - return normalizeSelectedLanguages([ - ...(fromRepeated ?? []), - ...(fromCsv ?? []), - ]); + return normalizeSelectedLanguages([...(fromRepeated ?? []), ...(fromCsv ?? [])]); } function calculateWinner(users: ComparedUserResult[]): { @@ -92,8 +82,7 @@ function calculateWinner(users: ComparedUserResult[]): { const [userA, userB] = users; const overallWinner = userA.finalScore >= userB.finalScore ? userA : userB; - const overallLoser = - overallWinner.username === userA.username ? userB : userA; + const overallLoser = overallWinner.username === userA.username ? userB : userA; const overallDifference = Math.abs(userA.finalScore - userB.finalScore); const overallPercentage = calculatePercentageDifference( overallDifference, @@ -116,18 +105,14 @@ function calculateWinner(users: ComparedUserResult[]): { winner: { username: overallWinner.username, finalScoreDifference: Math.round(overallDifference), - percentageDifference: - overallPercentage === null ? null : Math.round(overallPercentage), + percentageDifference: overallPercentage === null ? null : Math.round(overallPercentage), }, }; if (userA.languageScores && userB.languageScores) { const languageWinner = - userA.languageScores.finalScore >= userB.languageScores.finalScore - ? userA - : userB; - const languageLoser = - languageWinner.username === userA.username ? userB : userA; + userA.languageScores.finalScore >= userB.languageScores.finalScore ? userA : userB; + const languageLoser = languageWinner.username === userA.username ? userB : userA; const winnerLanguageScores = languageWinner.languageScores!; const loserLanguageScores = languageLoser.languageScores!; const languageDifference = Math.abs( @@ -141,8 +126,7 @@ function calculateWinner(users: ComparedUserResult[]): { result.languageWinner = { username: languageWinner.username, finalScoreDifference: Math.round(languageDifference), - percentageDifference: - languagePercentage === null ? null : Math.round(languagePercentage), + percentageDifference: languagePercentage === null ? null : Math.round(languagePercentage), selectedLanguages: winnerLanguageScores.selectedLanguages, }; } @@ -150,10 +134,7 @@ function calculateWinner(users: ComparedUserResult[]): { return result; } -function calculatePercentageDifference( - difference: number, - baseline: number, -): number | null { +function calculatePercentageDifference(difference: number, baseline: number): number | null { if (baseline <= 0) { return difference > 0 ? null : 0; } @@ -179,8 +160,7 @@ function createComparisonInsights( const repoLeader = user1.repoScore >= user2.repoScore ? user1 : user2; const prLeader = user1.prScore >= user2.prScore ? user1 : user2; - const contributionLeader = - user1.contributionScore >= user2.contributionScore ? user1 : user2; + const contributionLeader = user1.contributionScore >= user2.contributionScore ? user1 : user2; const user1Strengths: string[] = []; const user2Strengths: string[] = []; @@ -317,11 +297,7 @@ function resolveLocale(request: Request): Locale { return localeFromCookie; } - return parseAcceptLanguage( - request.headers.get("accept-language"), - ["en", "ar"], - DEFAULT_LOCALE, - ); + return parseAcceptLanguage(request.headers.get("accept-language"), ["en", "ar"], DEFAULT_LOCALE); } async function compareUsers( @@ -361,9 +337,7 @@ async function compareUsers( finalScore: Math.round(score.finalScore), normalizedRepoScore: Math.round(score.normalizedRepoScore), normalizedPRScore: Math.round(score.normalizedPRScore), - normalizedContributionScore: Math.round( - score.normalizedContributionScore, - ), + normalizedContributionScore: Math.round(score.normalizedContributionScore), normalizedFinalScore: Math.round(score.normalizedFinalScore), topRepos: score.topRepos, topPullRequests: score.topPullRequests, @@ -376,10 +350,7 @@ async function compareUsers( // ── Fire-and-forget: detect country & upsert into DB ────────────── const country = detectCountry(data.location); if (country) { - const staleDays = parseInt( - process.env.GITHUB_USER_STALE_DAYS ?? "14", - 10, - ); + const staleDays = parseInt(process.env.GITHUB_USER_STALE_DAYS ?? "14", 10); const db = getDatabaseStore(); db.upsertUser({ @@ -414,9 +385,7 @@ async function compareUsers( return results; } -function toApiErrorStatus( - code: ReturnType["code"], -): number { +function toApiErrorStatus(code: ReturnType["code"]): number { switch (code) { case "RATE_LIMITED": case "TEMPORARY_THROTTLE": @@ -458,8 +427,7 @@ export async function GET(request: Request) { try { const locale = resolveLocale(request); - const selectedLanguages = - parseSelectedLanguagesFromSearchParams(searchParams); + const selectedLanguages = parseSelectedLanguagesFromSearchParams(searchParams); const users = await compareUsers(usernames, selectedLanguages); const winnerData = calculateWinner(users); const insights = createComparisonInsights(users, locale); @@ -473,8 +441,7 @@ export async function GET(request: Request) { const mappedCause = toSafeApiError(error.causeError); if ( mappedCause.code === "GITHUB_NOT_FOUND" || - (error.causeError instanceof Error && - error.causeError.message === "User not found") + (error.causeError instanceof Error && error.causeError.message === "User not found") ) { safeError = { code: "GITHUB_NOT_FOUND", diff --git a/app/globals.css b/app/globals.css index 794cfbc..cb526ea 100644 --- a/app/globals.css +++ b/app/globals.css @@ -55,7 +55,12 @@ body { @apply bg-background text-foreground antialiased; - font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + sans-serif; background-color: hsl(var(--background)); min-height: 100vh; position: relative; @@ -101,14 +106,15 @@ body::after { } .card { - @apply bg-card/90 text-card-foreground shadow-card rounded-2xl border border-border backdrop-blur; - transition: transform 180ms ease, box-shadow 180ms ease; + @apply rounded-2xl border border-border bg-card/90 text-card-foreground shadow-card backdrop-blur; + transition: + transform 180ms ease, + box-shadow 180ms ease; box-shadow: 0 18px 48px rgba(15, 23, 42, 0.12); } - .dark .card { - @apply bg-card/80 border-border; + @apply border-border bg-card/80; box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45); } @@ -116,7 +122,8 @@ html.theme-transition, html.theme-transition *, html.theme-transition *::before, html.theme-transition *::after { - transition-property: background-color, border-color, color, fill, stroke, box-shadow, opacity, backdrop-filter; + transition-property: + background-color, border-color, color, fill, stroke, box-shadow, opacity, backdrop-filter; transition-duration: var(--theme-transition-duration); transition-timing-function: var(--theme-transition-ease); } diff --git a/app/layout.tsx b/app/layout.tsx index 5865918..f459666 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -102,20 +102,13 @@ export default async function RootLayout({ children }: { children: ReactNode }) const cookieLocale = cookieStore.get(LOCALE_COOKIE)?.value; const initialLocale = isSupportedLocale(cookieLocale) ? cookieLocale - : parseAcceptLanguage( - headerStore.get("accept-language"), - supportedLocales, - DEFAULT_LOCALE - ); + : parseAcceptLanguage(headerStore.get("accept-language"), supportedLocales, DEFAULT_LOCALE); const dir = getLocaleDir(initialLocale); return ( -