Skip to content

Feat/v3 -> optimizations & new routes#84

Merged
ketankauntia merged 13 commits into
masterfrom
feat/v3
Jan 25, 2026
Merged

Feat/v3 -> optimizations & new routes#84
ketankauntia merged 13 commits into
masterfrom
feat/v3

Conversation

@ketankauntia
Copy link
Copy Markdown
Owner

@ketankauntia ketankauntia commented Jan 24, 2026

Summary

Testing

Checklist

  • One logical change per PR
  • PR description is complete
  • No refactors without prior discussion
  • Follows existing project structure

Summary by CodeRabbit

  • New Features

    • Projects: year index and per-year pages with metrics, charts, and an expandable client-side project list.
    • Yearly: new yearly overview pages with stats, charts, orgs, projects, and mentors/contributors tables.
    • Tech Stack: static per-tech pages, searchable gallery, and interactive tech detail views.
    • Organizations: improved filters/sidebar with preloaded tech stats and hybrid static/API loading.
    • Homepage: trending organizations carousel and header renamed to "GSoC Guide".
  • Chores

    • Added static data generation scripts to produce JSON used by the new pages.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
gsoc-orgs Ready Ready Preview, Comment Jan 25, 2026 6:15pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 24, 2026

Important

Review skipped

Too many files!

136 files out of 286 files are above the max files limit of 150.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Expected 'de' | 'de-DE' | 'de-AT' | 'de-CH' | 'en' | 'en-US' | 'en-AU' | 'en-GB' | 'en-CA' | 'en-NZ' | 'en-ZA' | 'es' | 'es-AR' | 'fr' | 'fr-CA' | 'fr-CH' | 'fr-BE' | 'nl' | 'nl-BE' | 'pt-AO' | 'pt' | 'pt-BR' | 'pt-MZ' | 'pt-PT' | 'ar' | 'ast-ES' | 'ast' | 'be-BY' | 'be' | 'br-FR' | 'br' | 'ca-ES' | 'ca' | 'ca-ES-valencia' | 'ca-ES-balear' | 'da-DK' | 'da' | 'de-DE-x-simple-language' | 'el-GR' | 'el' | 'eo' | 'fa' | 'ga-IE' | 'ga' | 'gl-ES' | 'gl' | 'it' | 'ja-JP' | 'ja' | 'km-KH' | 'km' | 'ko-KR' | 'ko' | 'pl-PL' | 'pl' | 'ro-RO' | 'ro' | 'ru-RU' | 'ru' | 'sk-SK' | 'sk' | 'sl-SI' | 'sl' | 'sv' | 'ta-IN' | 'ta' | 'tl-PH' | 'tl' | 'tr' | 'uk-UA' | 'uk' | 'zh-CN' | 'zh' | 'crh-UA' | 'crh' | 'cs-CZ' | 'cs' | 'nb' | 'no' | 'nl-NL' | 'de-DE-x-simple-language-DE' | 'es-ES' | 'it-IT' | 'fa-IR' | 'sv-SE' | 'de-LU' | 'fr-FR' | 'bg-BG' | 'bg' | 'he-IL' | 'he' | 'hi-IN' | 'hi' | 'vi-VN' | 'vi' | 'th-TH' | 'th' | 'bn-BD' | 'bn', received object at "language"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Converts many dynamic pages to static SSG using precomputed JSON, adds data types and generation scripts, moves interactive UI into focused client wrappers, updates organizations filtering to accept preloaded tech data, and removes some Husky hooks and in-component fetches.

Changes

Cohort / File(s) Summary
Config & Git hooks
\.gitignore, .husky/commit-msg, .husky/pre-commit
Added ignore patterns for new-api-details and samples; removed commitlint invocation from commit-msg and deleted the pre-commit hook.
Type definitions & loaders
lib/homepage-types.ts, lib/projects-page-types.ts, lib/tech-stack-page-types.ts, lib/yearly-page-types.ts, lib/organizations-page-types.ts
New/expanded TS interfaces and async loader helpers to import precomputed JSON for homepage, projects, tech-stack, yearly, and organizations.
Data generation scripts
scripts/*generate-*.js, scripts/generate-projects-data.ts, scripts/fetch-external-projects.ts, scripts/populate-year-data.ts
New scripts to extract DB/external data, normalize/aggregate metrics and charts, and write JSON artifacts into new-api-details/.
Projects pages + client
app/projects/page.tsx, app/projects/[year]/page.tsx, app/projects/[year]/client-components.tsx
Added static projects index and per-year pages; new client components for expandable, searchable, paginated project lists.
Yearly pages + client components
app/yearly/page.tsx, app/yearly/[slug]/page.tsx, app/yearly/[slug]/client-components.tsx, app/[slug]/year-charts.tsx
New yearly index and per-year pages, charts, metrics, and client components (expandable lists, mentors table); added SimpleSelectionChart and minor chart tweaks.
Tech-stack refactor
app/tech-stack/page.tsx, app/tech-stack/[stack]/page.tsx, app/tech-stack/tech-stack-client-wrapper.tsx, app/tech-stack/[stack]/tech-stack-detail-client.tsx, app/tech-stack/[stack]/tech-stack-client.tsx (deleted)
Moved to SSG index and per-tech pages; removed large legacy client, added TechStackClientWrapper and TechStackDetailClient that consume static data.
Organizations & filters
app/organizations/page.tsx, app/organizations/organizations-client.tsx, app/organizations/filters-sidebar.tsx, app/organizations/[slug]/page.tsx
Introduced hybrid JSON-first loading with API fallback, added initialTechs / availableTechs prop flow, conditional API calls, and UI rework in OrganizationsClient.
Homepage & trending orgs
components/header.tsx, components/trending-orgs.tsx, components/trending-orgs-client.tsx, lib/homepage-types.ts
Header nav updated (Projects, Yearly); TrendingOrgs converted to server component that loads precomputed homepage JSON and renders a client carousel wrapper.
Package scripts
package.json
Added npm scripts generate:organizations and generate:tech-stack.

Sequence Diagram(s)

sequenceDiagram
    participant Script as "Data Scripts (generate-*)"
    participant DB as Database
    participant FS as "File System (new-api-details/*.json)"
    participant Build as "Build / SSG"
    participant Page as "Server Page Renderer"
    participant Browser as "Browser (Client components)"

    Script->>DB: Query orgs, projects, techs, external APIs
    DB-->>Script: Return raw rows
    Script->>Script: Aggregate, normalize, compute charts
    Script->>FS: Write precomputed JSON files
    Build->>FS: Import precomputed JSON during build
    Build->>Page: Generate static pages (revalidate=false)
    Page->>Browser: Serve pre-rendered HTML
    Browser->>Browser: Client wrappers operate on embedded/loaded data
Loading
sequenceDiagram
    participant Page as "/organizations page (server)"
    participant Loader as "loadOrganizationsIndexData / loadTechStackIndexData"
    participant OrgClient as "OrganizationsClient (client)"
    participant Filter as "FiltersSidebar (client)"
    participant User as "User (browser)"

    Page->>Loader: Load organizations + tech-stack index
    Loader-->>Page: Provide index + initialTechs
    Page->>OrgClient: Render OrgClient with initialData + initialTechs
    OrgClient->>Filter: Pass availableTechs prop
    User->>Filter: Select filters / search
    Filter->>OrgClient: Emit onFilterChange
    OrgClient->>OrgClient: Decide in-memory vs API, update results
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I nibbled through data, baked each byte,

Wrote JSON crumbs to make builds light,
Pages pre-warmed for quicker play,
Client hops lively, charts still sway,
Hooray for static—hop and bite! 🥕

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is completely empty with only an unfilled template; no summary, testing notes, or meaningful checklist completion is provided. Complete the description by filling in the Summary section with key changes, the Testing section with validation steps, and check off relevant checklist items with justification.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and overly broad, using generic terms like 'optimizations' and 'new routes' without specifying what was optimized or which routes were added. Replace with a more specific title that describes the primary change, e.g., 'Add static data generation and year/projects overview pages' or 'Refactor organizations page with static JSON support'.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/tech-stack/page.tsx (1)

1-2: Remove unused imports flagged by CI.

These are currently unused and already reported by the pipeline.

🛠️ Suggested fix
-import Link from "next/link";
-import { Search, TrendingUp, BarChart3, Users, Rocket, ArrowUpDown } from "lucide-react";
+import { TrendingUp, BarChart3, Users, Rocket } from "lucide-react";
🤖 Fix all issues with AI agents
In @.gitignore:
- Around line 49-53: The app fails to load data because /new-api-details/ is
gitignored but produced by generation scripts which aren't run during build;
update the build pipeline to run the generation scripts
(scripts/generate-homepage-data.js, scripts/generate-tech-stack-data.js,
scripts/generate-projects-data.ts, scripts/populate-year-data.ts) before or as
part of pnpm build (e.g., add them to a prebuild/build lifecycle in
package.json) so lib/homepage-types.ts, lib/projects-page-types.ts,
lib/tech-stack-page-types.ts and lib/yearly-page-types.ts have their imports
available at build/runtime; alternatively, add explicit deployment docs/README
instructions stating these scripts must be executed prior to deployment if you
prefer not to change package.json.

In `@app/projects/`[year]/client-components.tsx:
- Line 5: Remove the unused ExternalLink import from the import list (currently
importing Search, ChevronDown, ChevronUp, ExternalLink) so only used icons
remain; update the import to exclude ExternalLink to resolve the lint warning
and keep the import line consistent with actual usage in the file.

In `@app/projects/`[year]/page.tsx:
- Around line 247-264: The map callback over
charts.project_difficulty_distribution.data currently declares an unused second
parameter `index`, causing a lint warning; remove the `index` parameter from the
arrow function signature or rename it to `_index` to indicate intentional
non-use in the `.map()` call (the change should be made where
charts.project_difficulty_distribution.data.map((diff, index) => { ... }) is
defined).

In `@app/projects/page.tsx`:
- Around line 79-82: The hard-coded "2025" should be replaced by a derived
latest year computed from the existing years array: create a latestYear variable
(e.g., const latestYear = Math.max(...years) or years[years.length-1] if years
is already sorted) and use latestYear for the Stat value prop (where
value="2025") and in the CTA block referenced around lines 137-141 so both
places reuse the computed latestYear instead of a hard-coded literal.

In `@app/tech-stack/`[stack]/tech-stack-detail-client.tsx:
- Around line 322-395: You duplicated a local OrganizationCard instead of
reusing the shared component; replace this local OrganizationCard with the
reusable OrganizationCard from components/organization-card.tsx and either (A)
map the TechOrgSnapshot to the Organization prop shape (filling
description/technologies with empty/default values) and pass
showYears/showTechStack as needed, or (B) extend the shared OrganizationCard API
to accept showCategory and showStatus props (add those booleans to its props and
render the category/active badges when true) and then import and use that shared
OrganizationCard here, passing org (or the mapped object) plus the appropriate
flags. Ensure you remove the duplicated function and update usages to reference
the shared OrganizationCard component name.

In `@app/tech-stack/tech-stack-client-wrapper.tsx`:
- Around line 28-57: The useMemo block computing filteredTechs currently calls
setVisibleCount(24), which causes state updates during render and can trigger
infinite re-renders; remove the setVisibleCount call from the filteredTechs
useMemo and instead add a new useEffect that runs when searchQuery or sortBy
(and optionally techs) change and calls setVisibleCount(24); keep the
filteredTechs useMemo pure (dependent on [techs, searchQuery, sortBy]) and
implement the reset logic in a separate useEffect to avoid render-loop issues.

In `@app/yearly/`[slug]/page.tsx:
- Around line 380-386: The current mapping passed to MentorsContributorsTable
builds one row per project causing duplicate org rows; replace that map with a
grouping by org_slug over data.projects (e.g., reduce into a map keyed by
p.org_slug), for each org merge all project mentors and contributors into
arrays, deduplicate names (use a Set) and resolve org_name via
organizations.find(o => o.slug === org_slug)?.name || org_slug, then pass the
resulting array of unique org-level objects with properties org_name, org_slug,
mentors, contributors to MentorsContributorsTable.
- Around line 32-34: Replace the blanket export const revalidate = false with
conditional caching based on the route slug: extract the year from the slug
param in your page handler (slug), and if the year equals "2025" export/use a
30+ day ISR value (e.g., 2592000 seconds) and call revalidateTag('year-2025') to
enable tag-based invalidation; for all other years (2016-2024) keep revalidate =
false to cache aggressively. Ensure the symbols to change are revalidate,
revalidateTag and the page handler that reads slug so the export reflects the
conditional logic.

In `@components/trending-orgs-client.tsx`:
- Around line 57-58: The Tailwind class on the overlay div is invalid: update
the className on the div using the overlay (the element with className starting
"bg-linear-to-r from-background via-white/0 to-background z-10 absolute...") to
replace "bg-linear-to-r" with the correct "bg-gradient-to-r" so the gradient
utility is applied (leave the other utility classes unchanged).

In `@scripts/fetch-external-projects.ts`:
- Around line 24-27: In fetchExternalProjects, the local variable currentPage is
declared with let but never reassigned, causing the build failure; either change
it to const (const currentPage = 1) or remove the unused currentPage declaration
entirely in the fetchExternalProjects function to resolve the pipeline error,
ensuring ExternalProject[], allProjects, and totalPages remain unchanged.

In `@scripts/generate-homepage-data.js`:
- Around line 11-13: Replace the CommonJS requires with ESM imports: import
PrismaClient from '@prisma/client', fs and path via ESM, and compute __dirname
using fileURLToPath(import.meta.url); specifically update the top of
scripts/generate-homepage-data.js to import { PrismaClient } and the built-ins,
add import { fileURLToPath } from 'url', and derive __dirname =
path.dirname(fileURLToPath(import.meta.url)); alternatively, if you must keep
CommonJS, add an ESLint disable comment to allow require()s — but prefer the ESM
conversion so PrismaClient and filesystem/path usage align with import.meta.url.
- Around line 91-98: The call to prisma.$disconnect() inside
generateHomepageData() won't run if the function throws early; move the
disconnect responsibility out of the function and ensure it always runs by
invoking prisma.$disconnect() in a finally block around the top-level call to
generateHomepageData() (or call prisma.$disconnect() from the catch then
rethrow/exit), and remove the in-function line `await prisma.$disconnect();` so
disconnect is only handled once; reference generateHomepageData() and
prisma.$disconnect() when making the change.

In `@scripts/generate-projects-data.ts`:
- Around line 165-173: The percentage calculation for topTechPercentage can
divide by zero when yearlyData.metrics.total_organizations is 0; update the
logic in the block that computes topTechPercentage (used to build topTechData
from yearlyData.charts.top_languages and yearlyData.metrics.total_organizations)
to defensively check that total_organizations is a positive number before
dividing and otherwise set the percentage to 0 (or a safe default), ensuring you
also handle non-numeric values; keep topTechData construction unchanged except
it should rely on this safe topTechPercentage.

In `@scripts/generate-tech-stack-data.js`:
- Around line 11-13: The file uses CommonJS require() (PrismaClient, fs, path)
which violates `@typescript-eslint/no-require-imports`; fix by either (A)
converting the script to TypeScript: rename to generate-tech-stack-data.ts,
replace require() with ES imports for PrismaClient, fs and path, and update any
run docs/commands to use npx ts-node scripts/generate-tech-stack-data.ts, or (B)
explicitly mark it as CommonJS by renaming to generate-tech-stack-data.cjs and
update any references; choose one approach and make the corresponding rename and
import-style changes for the PrismaClient usage and filesystem/path calls.

In `@scripts/populate-year-data.ts`:
- Around line 102-103: There is a duplicated comment line "// 3. Process
Projects" repeated twice; remove the redundant duplicate so only a single "// 3.
Process Projects" remains (or replace both with an appropriate single-line
comment if you intend to renumber), ensuring surrounding code spacing/formatting
stays consistent.
🧹 Nitpick comments (22)
scripts/fetch-external-projects.ts (1)

52-72: Consider adding a fetch timeout to prevent indefinite hangs.

Network requests without timeouts can cause the script to hang indefinitely if the external API is slow or unresponsive.

Proposed enhancement with AbortController timeout
 for (let i = 2; i <= totalPages; i++) {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000);
     const p = fetch(`https://www.gsocorganizationsguide.com/api/v1/projects?year=${year}&page=${i}`)
+      .then(res => {
+        clearTimeout(timeoutId);
+        return res;
+      })
       .then(async res => {
         if (res.ok) {
             const data = await res.json() as ApiResponse;
components/trending-orgs-client.tsx (1)

62-82: Use Next.js Link component for client-side navigation.

Using a plain <a> tag instead of Next.js Link loses the benefits of client-side navigation, prefetching, and faster page transitions.

Proposed fix

Add the import at the top:

import Link from "next/link";

Then replace the anchor tag:

-                  <a href={`/organizations/${org.slug}`}>
+                  <Link href={`/organizations/${org.slug}`} prefetch={true}>
                     <div className="flex rounded-md aspect-square bg-muted items-center justify-center p-2 hover:bg-accent transition-colors overflow-hidden">
                       ...
                     </div>
-                  </a>
+                  </Link>
app/projects/[year]/client-components.tsx (1)

67-88: Prefer the shared SearchBar/filter components for consistency.

This new search UI duplicates functionality that should be shared across the app. Consider swapping in the existing SearchBar (and related filter patterns) to align UX and reduce maintenance. Based on learnings, please reuse the shared components.

app/yearly/page.tsx (2)

19-22: Add canonical URL to metadata for SEO consistency.

Per coding guidelines, ensure canonical URLs are consistent across the application. The metadata is missing the alternates.canonical field.

Suggested fix
+import { getFullUrl } from "@/lib/constants";
+
 export const metadata = {
   title: "GSoC Yearly Stats & Trends",
   description: "Explore Google Summer of Code statistics, trends, and insights by year. Historical data from 2016 to 2025.",
+  alternates: {
+    canonical: getFullUrl("/yearly"),
+  },
 };

140-160: Consider extracting StatCard to a shared component.

This StatCard component appears to be duplicated in app/yearly/[slug]/page.tsx. Consider extracting it to a shared component file to reduce duplication across yearly pages.

app/tech-stack/[stack]/tech-stack-detail-client.tsx (1)

78-79: Avoid hardcoding hex colors; use design tokens.

The TEAL_COLORS array uses hardcoded hex values. Per coding guidelines, use only existing color tokens and design variables. Consider using CSS variables from your design system or moving these to a shared constants file.

app/organizations/page.tsx (1)

14-14: Consider increasing the revalidation period.

Per coding guidelines, current year data should use long ISR (30 days+). A 1-hour revalidation may lead to unnecessary regenerations during traffic spikes (GSoC announcements). Consider increasing to at least 86400 (1 day) or longer.

-export const revalidate = 3600; // 1 hour
+export const revalidate = 86400; // 1 day - organizations data doesn't change frequently

Based on learnings, prefer long-lived caches over frequent regeneration for performance and scale.

app/organizations/organizations-client.tsx (1)

312-323: Remove unused callbacks toggleDifficulty and isDifficultySelected.

These callbacks are defined but never used, as indicated by the pipeline warnings. They appear to be leftover code from the disabled difficulty filter buttons.

Suggested fix
-  // Helper to toggle a difficulty in the array
-  const toggleDifficulty = useCallback((difficulty: string) => {
-    const newDifficulties = filters.difficulties.includes(difficulty)
-      ? filters.difficulties.filter(d => d !== difficulty)
-      : [...filters.difficulties, difficulty]
-    handleFilterChange({ ...filters, difficulties: newDifficulties })
-  }, [filters, handleFilterChange])
-
-  // Check if a difficulty is selected
-  const isDifficultySelected = useCallback((difficulty: string) => {
-    return filters.difficulties.includes(difficulty)
-  }, [filters.difficulties])
scripts/generate-projects-data.ts (2)

50-100: Consider importing ProjectYearPageData from the shared types module.

The ProjectYearPageData interface defined here duplicates the one in lib/projects-page-types.ts (lines 74-87). This creates a maintenance burden and risks drift between the script output and what the page expects.

♻️ Suggested approach
-interface ProjectYearPageData {
-  year: number;
-  slug: string;
-  // ... rest of interface
-}
+import type { ProjectYearPageData } from '../lib/projects-page-types';

If the script needs to run standalone without TypeScript compilation context, consider extracting the shared interface to a common location that both can reference, or use a JSON schema for validation.


219-220: published_at changes on every script run.

Using new Date().toISOString() means every regeneration produces a different timestamp, causing unnecessary diffs in version control and potentially invalidating caches even when data hasn't changed.

♻️ Consider using a deterministic timestamp
-      published_at: new Date().toISOString(),
+      published_at: `${year}-01-01T00:00:00.000Z`, // Stable timestamp based on year

Alternatively, only update published_at when actual data changes by comparing with the existing file.

app/[slug]/year-charts.tsx (3)

200-205: Unused year parameter in OrganizationsProjectsChart.

The year?: string prop was added but is never used in the component body. If this is intentional for future use, consider adding a TODO comment; otherwise, remove it to avoid confusion.

♻️ Either use the prop or remove it
 export function OrganizationsProjectsChart({ 
-  data 
+  data,
+  year, // TODO: Use for chart title/description in future
 }: { 
   data: OrgProjectData[]; 
   year?: string;
 })

Or if not needed:

 export function OrganizationsProjectsChart({ 
   data 
 }: { 
   data: OrgProjectData[]; 
-  year?: string;
 })

306-314: Inconsistent color handling compared to other charts.

SimpleSelectionChart uses a hardcoded colors array while other charts in this file use getChartBarColor(index) from @/lib/theme. This breaks consistency and makes theme changes harder to maintain. As per coding guidelines, follow existing project design patterns strictly.

♻️ Use the shared color utility
             {topItems.map((_, index) => {
-              const colors = [
-                "#0d9488", "#14b8a6", "#2dd4bf", "#5eead4", "#99f6e4",
-                "#0d9488", "#14b8a6", "#2dd4bf", "#5eead4", "#99f6e4",
-              ];
               return (
-                <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
+                <Cell key={`cell-${index}`} fill={getChartBarColor(index)} />
               );
             })}

292-300: Inconsistent Tooltip usage.

SimpleSelectionChart uses Tooltip from recharts directly, while other charts in this file use ChartTooltip from @/components/ui/chart. This creates visual inconsistency across charts. Consider using ChartTooltip for uniformity.

♻️ Use ChartTooltip for consistency
-          <Tooltip
+          <ChartTooltip
             contentStyle={{
               backgroundColor: "white",
               border: "1px solid `#e5e7eb`",
               borderRadius: "8px",
               fontSize: "12px",
             }}
             formatter={(value: number) => [value.toLocaleString(), "Selections"]}
           />
scripts/generate-tech-stack-data.js (1)

213-232: Growth calculation edge case for new technologies.

When count2020 is 0 and count2025 is exactly 5, the growth is set to 0 due to the > 5 threshold (line 221). Consider using >= 5 if technologies with exactly 5 organizations in 2025 should be considered significant.

♻️ Adjust threshold if needed
             const growth = count2020 > 0
                 ? ((count2025 - count2020) / count2020) * 100
-                : count2025 > 5 ? 500 : 0; // Only show high growth for new significant techs
+                : count2025 >= 5 ? 500 : 0; // Only show high growth for new significant techs
app/projects/[year]/page.tsx (2)

357-372: Hardcoded year bounds may drift out of sync.

The year navigation buttons hardcode 2016 and 2025 as boundaries, but getAvailableProjectYears() is already imported and defines these years. Using the function ensures consistency if the year range changes.

♻️ Use the shared year bounds
+const availableYears = getAvailableProjectYears();
+const minYear = Math.min(...availableYears);
+const maxYear = Math.max(...availableYears);
+
 // Then in JSX:
-              {year > 2016 && (
+              {year > minYear && (
                 <Button asChild size="lg" variant="ghost">
                   <Link href={`/projects/${year - 1}`}>
                     <Calendar className="w-4 h-4 mr-2" />
                     GSoC {year - 1}
                   </Link>
                 </Button>
               )}
-              {year < 2025 && (
+              {year < maxYear && (

27-30: Consider using an alias for cross-route imports.

The import from "../../[slug]/year-charts" crosses route segment boundaries, which can be fragile. Consider moving shared chart components to a common location like @/components/charts.

-import {
-  LanguagesBarChart,
-  OrganizationsProjectsChart,
-} from "../../[slug]/year-charts";
+import {
+  LanguagesBarChart,
+  OrganizationsProjectsChart,
+} from "@/components/charts/year-charts";

This would require relocating the chart components to a shared location.

scripts/populate-year-data.ts (2)

117-124: Hardcoding tech_stack: ["unknown"] discards potential data.

Every project is marked with tech_stack: ["unknown"], even though the organization may have technologies data that could be associated. If project-level tech stack is truly unavailable, consider inheriting from the organization's technologies or fetching from external data.

♻️ Consider inheriting from organization
+      // Try to get tech stack from organization if project doesn't have it
+      const orgData = organizations.find(o => o.slug === p.org_slug);
+      const techStack = p.tech_stack?.length > 0 
+        ? p.tech_stack 
+        : (orgData?.technologies?.length > 0 ? orgData.technologies : ["unknown"]);
+
       return {
         id: p.project_id || p.id,
         title: p.project_title,
         org_slug: p.org_slug,
         contributor: contributor,
         mentors: mentors,
-        tech_stack: ["unknown"]
+        tech_stack: techStack
       };

136-148: O(N²) complexity in participant/mentor counting.

Using processedProjects.find() inside a projects.forEach() loop creates O(N²) complexity. Since processedProjects is derived from projects in the same order, you can iterate directly over processedProjects instead.

♻️ Simplify to O(N)
-  projects.forEach(p => {
-      // Use processedProjects to count because they have enriched data
-      // Find the processed version
-      const processed = processedProjects.find(pp => pp.id === (p.project_id || p.id));
-      if (processed) {
-        if (processed.contributor && processed.contributor !== "Unknown") allParticipants.add(processed.contributor);
-        if (processed.mentors && Array.isArray(processed.mentors)) {
-            processed.mentors.forEach((m) => {
-                if (m !== "Unknown") allMentors.add(m);
-            });
-        }
-      }
-  });
+  processedProjects.forEach(processed => {
+      if (processed.contributor && processed.contributor !== "Unknown") {
+        allParticipants.add(processed.contributor);
+      }
+      if (processed.mentors && Array.isArray(processed.mentors)) {
+        processed.mentors.forEach((m) => {
+          if (m !== "Unknown") allMentors.add(m);
+        });
+      }
+  });
app/yearly/[slug]/client-components.tsx (2)

15-85: Reuse OrganizationCard for org listings.

This custom card duplicates an org listing UI; please switch to the shared OrganizationCard for consistency and DRY. Based on learnings, reuse the existing OrganizationCard for organization listings.


186-197: Use the shared SearchBar for filtering.

The inline input bypasses the shared search UI; prefer the existing SearchBar/filter components to keep styling and accessibility consistent. Based on learnings, reuse the existing SearchBar and filter components.

app/tech-stack/page.tsx (1)

49-133: Prefer semantic color tokens over teal utility classes.

Several icons use text-teal-600; if that isn’t an approved design token, please swap to semantic tokens (e.g., text-primary/text-accent) to stay consistent with the design system. As per coding guidelines, use existing color tokens and design variables.

app/yearly/[slug]/page.tsx (1)

192-223: Replace hardcoded hex colors with design tokens.

The pie chart uses raw hex values; please switch to theme tokens/CSS variables to respect theming and design standards. As per coding guidelines, use existing color tokens and design variables.

Comment thread .gitignore
Comment on lines +49 to +53

# others
/sample-gsoc-guide/
/new-api-details/
/things-to-do/ No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check if the file mentioned in the review exists
if [ -f "lib/homepage-types.ts" ]; then
  echo "=== Checking lib/homepage-types.ts ==="
  grep -n "new-api-details" lib/homepage-types.ts || echo "Not found in lib/homepage-types.ts"
  grep -n "loadHomepageData" lib/homepage-types.ts || echo "loadHomepageData not found"
else
  echo "lib/homepage-types.ts does not exist"
fi

# Check if new-api-details directory exists in repo
echo ""
echo "=== Checking for new-api-details in repository ==="
find . -type d -name "new-api-details" 2>/dev/null | head -20

# Check package.json for build/generate scripts
echo ""
echo "=== Checking package.json for relevant scripts ==="
if [ -f "package.json" ]; then
  grep -A 50 '"scripts"' package.json | head -60
fi

# Check for any .gitignore file to understand current state
echo ""
echo "=== Checking .gitignore around lines 49-53 ==="
if [ -f ".gitignore" ]; then
  sed -n '45,55p' .gitignore
fi

Repository: ketankauntia/gsoc-orgs

Length of output: 1904


🏁 Script executed:

#!/bin/bash
# Search for scripts that might generate the data
echo "=== Looking for scripts directory ==="
ls -la scripts/ 2>/dev/null || echo "No scripts directory found"

# Search for any references to homepage.json generation
echo ""
echo "=== Searching for homepage.json references ==="
rg "homepage.json" --type=ts --type=js -B 2 -A 2

# Check if there's a build output directory
echo ""
echo "=== Looking for common build/output directories ==="
ls -la | grep -E "^d.*\.(next|dist|build|out)$"

Repository: ketankauntia/gsoc-orgs

Length of output: 1704


🏁 Script executed:

#!/bin/bash
# Check for API data generation in source files
echo "=== Searching for new-api-details references in codebase ==="
rg "new-api-details" --type=ts --type=js --type=json -B 2 -A 2

# Check for any config files that might indicate build strategy
echo ""
echo "=== Checking for build configuration files ==="
ls -la | grep -E "\.(next|webpack|vite|build)"

Repository: ketankauntia/gsoc-orgs

Length of output: 4060


🏁 Script executed:

#!/bin/bash
# Check for README or deployment documentation
echo "=== Checking for README or deployment docs ==="
fd -e md | grep -i -E "(readme|deploy|setup|guide)" | head -10

# Look for README specifically
if [ -f "README.md" ]; then
  echo ""
  echo "=== Searching README.md for build/generate instructions ==="
  grep -n -i -E "(generate|build|deploy|script|setup)" README.md | head -20
fi

# Check if there's a .github/workflows for deployment hints
echo ""
echo "=== Checking for CI/CD workflow files ==="
fd -e yml -e yaml ".github/workflows" 2>/dev/null | head -10
if [ -d ".github/workflows" ]; then
  ls -la .github/workflows/
fi

Repository: ketankauntia/gsoc-orgs

Length of output: 2218


🏁 Script executed:

#!/bin/bash
# Check the deployment documentation
echo "=== Checking deployment-fix.md ==="
cat md-docs/3-deployment-fix.md

echo ""
echo "=== Checking vercel-deployment-steps.md ==="
cat md-docs/6-vercel-deployment-steps.md

echo ""
echo "=== Checking CI workflow ==="
cat .github/workflows/ci.yml

Repository: ketankauntia/gsoc-orgs

Length of output: 7080


Ensure data generation scripts are integrated into the build/deploy pipeline.

The /new-api-details/ directory is gitignored but imported by multiple pages (lib/homepage-types.ts, lib/projects-page-types.ts, lib/tech-stack-page-types.ts, lib/yearly-page-types.ts). Generation scripts exist in the scripts/ directory (generate-homepage-data.js, generate-tech-stack-data.js, generate-projects-data.ts, populate-year-data.ts), but the build process (pnpm build) does not call them. Add these scripts to the build pipeline or document when they must be run during deployment, otherwise the app will fail to load data at runtime.

🤖 Prompt for AI Agents
In @.gitignore around lines 49 - 53, The app fails to load data because
/new-api-details/ is gitignored but produced by generation scripts which aren't
run during build; update the build pipeline to run the generation scripts
(scripts/generate-homepage-data.js, scripts/generate-tech-stack-data.js,
scripts/generate-projects-data.ts, scripts/populate-year-data.ts) before or as
part of pnpm build (e.g., add them to a prebuild/build lifecycle in
package.json) so lib/homepage-types.ts, lib/projects-page-types.ts,
lib/tech-stack-page-types.ts and lib/yearly-page-types.ts have their imports
available at build/runtime; alternatively, add explicit deployment docs/README
instructions stating these scripts must be executed prior to deployment if you
prefer not to change package.json.

Comment thread app/projects/[year]/client-components.tsx Outdated
Comment thread app/projects/[year]/page.tsx
Comment thread app/projects/page.tsx
Comment on lines +79 to +82
icon={<TrendingUp className="w-5 h-5 text-primary" />}
label="Latest Year"
value="2025"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Compute the latest year from data instead of hard-coding 2025.

This will go stale as soon as a new year is added. Derive latestYear from years and reuse it in the stat and CTA.

🧾 Proposed fix
-  const totalProjects = yearData.reduce((sum, y) => sum + y.total_projects, 0);
-  const totalOrgAppearances = yearData.reduce((sum, y) => sum + y.total_organizations, 0);
+  const totalProjects = yearData.reduce((sum, y) => sum + y.total_projects, 0);
+  const totalOrgAppearances = yearData.reduce((sum, y) => sum + y.total_organizations, 0);
+  const latestYear = Math.max(...years);
...
-                value="2025"
+                value={latestYear}
...
-                <Link href="/projects/2025">
+                <Link href={`/projects/${latestYear}`}>
                   <Calendar className="w-4 h-4 mr-2" />
-                  View 2025 Projects
+                  View {latestYear} Projects
                 </Link>

Also applies to: 137-141

🤖 Prompt for AI Agents
In `@app/projects/page.tsx` around lines 79 - 82, The hard-coded "2025" should be
replaced by a derived latest year computed from the existing years array: create
a latestYear variable (e.g., const latestYear = Math.max(...years) or
years[years.length-1] if years is already sorted) and use latestYear for the
Stat value prop (where value="2025") and in the CTA block referenced around
lines 137-141 so both places reuse the computed latestYear instead of a
hard-coded literal.

Comment on lines +322 to +395
/**
* Organization Card Component
*/
function OrganizationCard({ org }: { org: TechOrgSnapshot }) {
return (
<Link href={`/organizations/${org.slug}`} prefetch={true}>
<CardWrapper hover className="h-full flex flex-col">
{/* Header with Logo */}
<div className="flex items-start gap-4 mb-3">
<div className="w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden shrink-0">
{org.logo_url ? (
<Image
src={org.logo_url}
alt={`${org.name} logo`}
width={48}
height={48}
className="w-full h-full object-contain"
unoptimized={true}
loading="lazy"
/>
) : (
<span className="text-lg font-semibold text-muted-foreground">
{org.name.charAt(0)}
</span>
)}
</div>
<div className="min-w-0 flex-1">
<Heading variant="small" className="line-clamp-1 mb-0.5">
{org.name}
</Heading>
<Text variant="small" className="text-muted-foreground">
{org.total_projects} projects
</Text>
</div>
</div>

{/* Years Section */}
{org.active_years && org.active_years.length > 0 && (
<div className="mb-3">
<div className="flex flex-wrap items-center gap-1.5">
{org.active_years
.sort((a, b) => b - a)
.slice(0, 5)
.map((year) => (
<Badge
key={year}
variant="outline"
className="text-xs bg-teal-50 text-teal-700 border-teal-200"
>
{year}
</Badge>
))}
{org.active_years.length > 5 && (
<Badge variant="outline" className="text-xs">
+{org.active_years.length - 5}
</Badge>
)}
</div>
</div>
)}

{/* Footer */}
<div className="flex items-center justify-between pt-3 border-t mt-auto">
<Badge variant="secondary" className="text-xs">
{org.category}
</Badge>
<Badge variant={org.is_currently_active ? "default" : "outline"} className="text-xs">
{org.is_currently_active ? "Active" : "Inactive"}
</Badge>
</div>
</CardWrapper>
</Link>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the existing OrganizationCard component
rg -n "export.*OrganizationCard" --type ts --type tsx -B 2 -A 20 | head -100

Repository: ketankauntia/gsoc-orgs

Length of output: 93


🏁 Script executed:

# Search for OrganizationCard imports and definitions
rg -n "OrganizationCard" --type ts --type tsx | grep -E "(export|import|function|const)" | head -30

Repository: ketankauntia/gsoc-orgs

Length of output: 93


🏁 Script executed:

# Locate component files in the components directory
find . -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "OrganizationCard" | head -20

Repository: ketankauntia/gsoc-orgs

Length of output: 242


🏁 Script executed:

# Read the existing OrganizationCard component
cat -n components/organization-card.tsx

Repository: ketankauntia/gsoc-orgs

Length of output: 4823


🏁 Script executed:

# Check TechOrgSnapshot type definition in tech-stack-detail-client.tsx
rg -n "TechOrgSnapshot" app/tech-stack/[stack]/tech-stack-detail-client.tsx -B 5 -A 15 | head -80

Repository: ketankauntia/gsoc-orgs

Length of output: 1610


🏁 Script executed:

# Check how OrganizationCard is used in other files to understand expected props
rg -n "OrganizationCard" app/organizations/organizations-client.tsx -B 2 -A 2

Repository: ketankauntia/gsoc-orgs

Length of output: 689


🏁 Script executed:

# Check TechOrgSnapshot type definition
rg -n "type TechOrgSnapshot|interface TechOrgSnapshot" -A 20 lib/

Repository: ketankauntia/gsoc-orgs

Length of output: 1355


🏁 Script executed:

# Check Organization type definition for comparison
rg -n "type Organization|interface Organization|export.*Organization" lib/api -A 15 | head -80

Repository: ketankauntia/gsoc-orgs

Length of output: 113


🏁 Script executed:

# Find Organization type definition
rg -n "export.*type Organization|export.*interface Organization" --type ts -A 15 | head -100

Repository: ketankauntia/gsoc-orgs

Length of output: 4149


🏁 Script executed:

# Check what properties Organization has
rg -n "interface Organization|type Organization" -A 20 | head -120

Repository: ketankauntia/gsoc-orgs

Length of output: 6756


Reuse the existing OrganizationCard component instead of defining a new one.

The shared OrganizationCard component in components/organization-card.tsx accepts both showYears and showTechStack props, making it flexible for different display scenarios. This internal implementation duplicates that component with a different data shape (TechOrgSnapshot vs Organization).

Refactor by either:

  1. Extending OrganizationCard with additional props (e.g., showCategory, showStatus) to support the category and active status badges used here
  2. Transforming TechOrgSnapshot to match Organization shape where possible (providing description and technologies data, or using appropriate null/empty defaults)
🤖 Prompt for AI Agents
In `@app/tech-stack/`[stack]/tech-stack-detail-client.tsx around lines 322 - 395,
You duplicated a local OrganizationCard instead of reusing the shared component;
replace this local OrganizationCard with the reusable OrganizationCard from
components/organization-card.tsx and either (A) map the TechOrgSnapshot to the
Organization prop shape (filling description/technologies with empty/default
values) and pass showYears/showTechStack as needed, or (B) extend the shared
OrganizationCard API to accept showCategory and showStatus props (add those
booleans to its props and render the category/active badges when true) and then
import and use that shared OrganizationCard here, passing org (or the mapped
object) plus the appropriate flags. Ensure you remove the duplicated function
and update usages to reference the shared OrganizationCard component name.

Comment on lines +11 to +13
const { PrismaClient } = require('@prisma/client');
const fs = require('fs');
const path = require('path');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix linting errors: Convert to ESM imports or configure ESLint exception.

The pipeline is failing due to require() style imports being forbidden. Since this is a standalone Node.js script, you have two options:

Option 1: Rename to .mjs and use ESM imports (recommended)

import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Option 2: Add ESLint disable comment

+/* eslint-disable `@typescript-eslint/no-require-imports` */
 const { PrismaClient } = require('@prisma/client');
 const fs = require('fs');
 const path = require('path');
🧰 Tools
🪛 ESLint

[error] 11-11: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 12-12: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 13-13: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🪛 GitHub Check: build-and-lint

[failure] 13-13:
A require() style import is forbidden


[failure] 12-12:
A require() style import is forbidden


[failure] 11-11:
A require() style import is forbidden

🤖 Prompt for AI Agents
In `@scripts/generate-homepage-data.js` around lines 11 - 13, Replace the CommonJS
requires with ESM imports: import PrismaClient from '@prisma/client', fs and
path via ESM, and compute __dirname using fileURLToPath(import.meta.url);
specifically update the top of scripts/generate-homepage-data.js to import {
PrismaClient } and the built-ins, add import { fileURLToPath } from 'url', and
derive __dirname = path.dirname(fileURLToPath(import.meta.url)); alternatively,
if you must keep CommonJS, add an ESLint disable comment to allow require()s —
but prefer the ESM conversion so PrismaClient and filesystem/path usage align
with import.meta.url.

Comment on lines +91 to +98
await prisma.$disconnect();
}

// Run
generateHomepageData().catch((error) => {
console.error('[ERROR]', error);
process.exit(1);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ensure Prisma disconnects on error.

If generateHomepageData() throws before line 91, prisma.$disconnect() won't be called. Consider using a finally block or moving disconnect into the catch handler.

Suggested fix
-generateHomepageData().catch((error) => {
+generateHomepageData()
+  .catch((error) => {
     console.error('[ERROR]', error);
     process.exit(1);
-});
+  })
+  .finally(() => {
+    prisma.$disconnect();
+  });

And remove line 91 (await prisma.$disconnect();) from inside the function.

🤖 Prompt for AI Agents
In `@scripts/generate-homepage-data.js` around lines 91 - 98, The call to
prisma.$disconnect() inside generateHomepageData() won't run if the function
throws early; move the disconnect responsibility out of the function and ensure
it always runs by invoking prisma.$disconnect() in a finally block around the
top-level call to generateHomepageData() (or call prisma.$disconnect() from the
catch then rethrow/exit), and remove the in-function line `await
prisma.$disconnect();` so disconnect is only handled once; reference
generateHomepageData() and prisma.$disconnect() when making the change.

Comment on lines +165 to +173
// Top tech insight
const topTech = yearlyData.charts.top_languages[0];
const topTechPercentage = topTech
? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
: 0;
const topTechData = topTech ? {
name: topTech.label,
percentage: topTechPercentage
} : null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential division by zero when calculating topTechPercentage.

If yearlyData.metrics.total_organizations is 0, this calculation will produce Infinity or NaN. While unlikely given the data flow, defensive handling would be safer.

🐛 Proposed fix
     const topTechPercentage = topTech 
-      ? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
+      ? (yearlyData.metrics.total_organizations > 0 
+          ? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
+          : 0)
       : 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Top tech insight
const topTech = yearlyData.charts.top_languages[0];
const topTechPercentage = topTech
? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
: 0;
const topTechData = topTech ? {
name: topTech.label,
percentage: topTechPercentage
} : null;
// Top tech insight
const topTech = yearlyData.charts.top_languages[0];
const topTechPercentage = topTech
? (yearlyData.metrics.total_organizations > 0
? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
: 0)
: 0;
const topTechData = topTech ? {
name: topTech.label,
percentage: topTechPercentage
} : null;
🤖 Prompt for AI Agents
In `@scripts/generate-projects-data.ts` around lines 165 - 173, The percentage
calculation for topTechPercentage can divide by zero when
yearlyData.metrics.total_organizations is 0; update the logic in the block that
computes topTechPercentage (used to build topTechData from
yearlyData.charts.top_languages and yearlyData.metrics.total_organizations) to
defensively check that total_organizations is a positive number before dividing
and otherwise set the percentage to 0 (or a safe default), ensuring you also
handle non-numeric values; keep topTechData construction unchanged except it
should rely on this safe topTechPercentage.

Comment on lines +11 to +13
const { PrismaClient } = require('@prisma/client');
const fs = require('fs');
const path = require('path');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pipeline failure: require() style imports are forbidden.

The ESLint rule @typescript-eslint/no-require-imports is failing the build. Either convert this file to TypeScript (.ts) with ES module imports, or rename to .cjs if CommonJS is intentional for this script.

🐛 Option 1: Convert to TypeScript with ES imports

Rename file to generate-tech-stack-data.ts and update imports:

-const { PrismaClient } = require('@prisma/client');
-const fs = require('fs');
-const path = require('path');
+import { PrismaClient } from '@prisma/client';
+import * as fs from 'fs';
+import * as path from 'path';

Update the run command in documentation to:

npx ts-node scripts/generate-tech-stack-data.ts
🐛 Option 2: Use .cjs extension for CommonJS

Rename the file to generate-tech-stack-data.cjs and update any references to it. This tells ESLint to treat it as a CommonJS module.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { PrismaClient } = require('@prisma/client');
const fs = require('fs');
const path = require('path');
import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
import * as path from 'path';
🧰 Tools
🪛 ESLint

[error] 11-11: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 12-12: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 13-13: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🪛 GitHub Check: build-and-lint

[failure] 13-13:
A require() style import is forbidden


[failure] 12-12:
A require() style import is forbidden


[failure] 11-11:
A require() style import is forbidden

🤖 Prompt for AI Agents
In `@scripts/generate-tech-stack-data.js` around lines 11 - 13, The file uses
CommonJS require() (PrismaClient, fs, path) which violates
`@typescript-eslint/no-require-imports`; fix by either (A) converting the script
to TypeScript: rename to generate-tech-stack-data.ts, replace require() with ES
imports for PrismaClient, fs and path, and update any run docs/commands to use
npx ts-node scripts/generate-tech-stack-data.ts, or (B) explicitly mark it as
CommonJS by renaming to generate-tech-stack-data.cjs and update any references;
choose one approach and make the corresponding rename and import-style changes
for the PrismaClient usage and filesystem/path calls.

Comment on lines +102 to +103
// 3. Process Projects
// 3. Process Projects
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate comment line.

The comment // 3. Process Projects appears twice on consecutive lines.

🐛 Remove duplicate
-  // 3. Process Projects
   // 3. Process Projects
   const processedProjects = projects.map(p => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 3. Process Projects
// 3. Process Projects
// 3. Process Projects
const processedProjects = projects.map(p => {
🤖 Prompt for AI Agents
In `@scripts/populate-year-data.ts` around lines 102 - 103, There is a duplicated
comment line "// 3. Process Projects" repeated twice; remove the redundant
duplicate so only a single "// 3. Process Projects" remains (or replace both
with an appropriate single-line comment if you intend to renumber), ensuring
surrounding code spacing/formatting stays consistent.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/organizations/organizations-client.tsx (1)

209-268: Client API decision logic is inconsistent with the server.

Server uses a filter‑count threshold (>2) to decide API use, while the client only checks “all four filters” or a narrower combo. This can skip API fetches on pagination or filter changes, leaving stale/partial data. Align client logic with the server (ideally via a shared helper).

🛠️ Example alignment (shared predicate)
+  const needsAPI = useMemo(() => {
+    const filterCount = [
+      filters.years.length > 0,
+      filters.categories.length > 0,
+      filters.techs.length > 0,
+      filters.topics.length > 0,
+      filters.firstTimeOnly,
+    ].filter(Boolean).length;
+
+    return (
+      filters.search.trim().length > 0 ||
+      filters.yearsLogic === 'AND' ||
+      filters.categoriesLogic === 'AND' ||
+      filters.techsLogic === 'AND' ||
+      filters.topicsLogic === 'AND' ||
+      filterCount > 2
+    );
+  }, [filters]);
@@
-      const hasDynamicFilters = filters.search || 
-        filters.yearsLogic === 'AND' || 
-        filters.categoriesLogic === 'AND' ||
-        filters.techsLogic === 'AND' ||
-        filters.topicsLogic === 'AND' ||
-        (filters.years.length > 0 && filters.categories.length > 0 && filters.techs.length > 0)
+      const hasDynamicFilters = needsAPI
@@
-    const needsAPI = 
-      filters.search.trim().length > 0 ||
-      filters.yearsLogic === 'AND' ||
-      filters.categoriesLogic === 'AND' ||
-      filters.techsLogic === 'AND' ||
-      filters.topicsLogic === 'AND' ||
-      (filters.years.length > 0 && filters.categories.length > 0 && filters.techs.length > 0 && filters.topics.length > 0)
+    // Reuse shared predicate
+    if (!needsAPI) return
app/organizations/[slug]/page.tsx (1)

64-79: generateMetadata may trigger an uncached API fetch.

generateMetadata (line 88) calls getOrganization, which falls back to an uncached API call when JSON is unavailable. This violates the requirement that metadata on SEO pages must use cached/static data only. Ensure metadata uses only static JSON (or returns a fallback) and move API fetches to the page render path.

🛠️ Possible fix (skip API in metadata)
-async function getOrganization(slug: string): Promise<OrganizationWithStats | null> {
+async function getOrganization(
+  slug: string,
+  options?: { allowApi?: boolean }
+): Promise<OrganizationWithStats | null> {
   // Try static JSON first
   const jsonData = await loadOrganizationData(slug);
   if (jsonData) {
     return jsonData as OrganizationWithStats;
   }
 
-  // Fallback to API if JSON not available
+  if (options?.allowApi === false) {
+    return null;
+  }
+  // Fallback to API if JSON not available
   try {
     return await apiFetchServer<OrganizationWithStats>(`/api/organizations/${slug}`);
   } catch (error: unknown) {
     if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
       return null;
     }
     throw error;
   }
 }

And update generateMetadata:

-  const org = await getOrganization(slug);
+  const org = await getOrganization(slug, { allowApi: false });
🤖 Fix all issues with AI agents
In `@app/organizations/page.tsx`:
- Around line 88-139: shouldUseAPI currently ignores legacy query names
"category" and "tech" which can cause the function to choose static JSON and
return unfiltered results; update the params type to include optional category?:
string and tech?: string and add checks for params.category and params.tech
(trimmed length > 0) into the filterCount boolean array (and any other places
counting filters) so legacy query keys are treated the same as categories/techs
when deciding to use the API in function shouldUseAPI.
- Around line 144-219: In getOrganizations, the static JSON filtering branch
drops the legacy params.category; fix by mapping params.category into the
categories argument passed to filterOrganizations (i.e. when building the object
for filterOrganizations, set categories to params.categories ?
params.categories.split(',') : params.category ? [params.category] : undefined)
so old links using ?category=... are honored (keep the existing tech -> techs
fallback logic intact).
- Line 22: The ISR duration is too short: replace the hardcoded revalidate =
3600 with the long cache constant and wire it into the page; set revalidate to
CacheDurations.LONG (2592000) and ensure the page uses the existing
revalidateTag invalidation flow (e.g., call or import revalidateTag where the
page triggers tag-based invalidation) so organizations page uses tag-based
invalidation with a 30-day ISR. Locate the export const revalidate and update it
to reference CacheDurations.LONG and keep the revalidateTag integration
consistent with other pages using that infrastructure.

In `@scripts/generate-organizations-data.js`:
- Around line 99-115: The loop writing organization files currently uses
org.slug directly when building orgFile with path.join(OUTPUT_DIR,
`${org.slug}.json`) which allows path traversal or invalid filenames; before
constructing the path (inside the for (const org of organizations) loop)
validate and sanitize org.slug (e.g., enforce a whitelist of [a-z0-9-_], replace
or strip disallowed chars, disallow empty strings, reject or normalize segments
like "." or "..", and ensure it is not an absolute path) and then use the
sanitized value when calling path.join and fs.writeFileSync; also consider using
path.basename or throwing an error for invalid slugs to guarantee files stay
inside OUTPUT_DIR.
- Around line 12-14: ESLint flags the use of CommonJS require for PrismaClient,
fs, and path in the script; fix by either converting those lines to ESM imports
(e.g., import { PrismaClient } from '@prisma/client'; import fs from 'fs';
import path from 'path'; and ensure package.json "type":"module") or add a
file-level lint override to allow requires (add /* eslint-disable
`@typescript-eslint/no-require-imports` */ at the top of the script). Update the
lines that declare PrismaClient, fs, and path accordingly so CI linting passes.
🧹 Nitpick comments (1)
package.json (1)

13-15: Ensure generated data runs in CI/build.

The new generators are defined but not invoked by build/validate. If static JSON is expected at deploy time, wire these into the build pipeline (or CI) to avoid missing/stale artifacts.

* - API for search and complex filter combinations
*/
export const revalidate = 3600; // 1 hour - search pages need more frequent updates
export const revalidate = 3600; // 1 hour
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view its content
cd /tmp && find . -name "page.tsx" -path "*/organizations/*" 2>/dev/null | head -20

Repository: ketankauntia/gsoc-orgs

Length of output: 48


🏁 Script executed:

# List the directory structure to understand the app layout
git ls-files | grep -E "^app/.*page\.tsx$" | head -20

Repository: ketankauntia/gsoc-orgs

Length of output: 536


🏁 Script executed:

# Check the .cursorrules file if it exists
git ls-files | grep -i cursorrules

Repository: ketankauntia/gsoc-orgs

Length of output: 78


🏁 Script executed:

# Search for revalidate patterns in the codebase
rg "revalidate\s*=" --type ts --type tsx | head -30

Repository: ketankauntia/gsoc-orgs

Length of output: 93


🏁 Script executed:

# Search for revalidateTag patterns
rg "revalidateTag" --type ts --type tsx

Repository: ketankauntia/gsoc-orgs

Length of output: 93


🏁 Script executed:

# Read .cursorrules to understand caching guidance
cat .cursorrules

Repository: ketankauntia/gsoc-orgs

Length of output: 10354


🏁 Script executed:

# Read the organizations page to understand current implementation
cat -n app/organizations/page.tsx

Repository: ketankauntia/gsoc-orgs

Length of output: 11555


🏁 Script executed:

# Search for revalidate patterns without problematic file type flags
rg "revalidate" | head -30

Repository: ketankauntia/gsoc-orgs

Length of output: 2370


🏁 Script executed:

# Search for revalidateTag patterns
rg "revalidateTag"

Repository: ketankauntia/gsoc-orgs

Length of output: 1091


Increase ISR window for this SEO page to 30 days.

The current revalidate = 3600 (1 hour) is significantly shorter than the repo's caching strategy. Per .cursorrules, organizations (current year data) should use a long ISR window (30 days+) with tag-based invalidation. The codebase already defines CacheDurations.LONG (2592000 seconds / 30 days) and has revalidateTag infrastructure in place for cache invalidation.

🛠️ Suggested adjustment
-export const revalidate = 3600; // 1 hour
+export const revalidate = 2592000; // 30 days
🤖 Prompt for AI Agents
In `@app/organizations/page.tsx` at line 22, The ISR duration is too short:
replace the hardcoded revalidate = 3600 with the long cache constant and wire it
into the page; set revalidate to CacheDurations.LONG (2592000) and ensure the
page uses the existing revalidateTag invalidation flow (e.g., call or import
revalidateTag where the page triggers tag-based invalidation) so organizations
page uses tag-based invalidation with a 30-day ISR. Locate the export const
revalidate and update it to reference CacheDurations.LONG and keep the
revalidateTag integration consistent with other pages using that infrastructure.

Comment thread app/organizations/page.tsx
Comment on lines 144 to +219
async function getOrganizations(params: {
page?: number;
limit?: number;
q?: string;
category?: string;
tech?: string;
years?: string;
categories?: string;
techs?: string;
topics?: string;
firstTimeOnly?: string;
yearsLogic?: string;
categoriesLogic?: string;
techsLogic?: string;
topicsLogic?: string;
}): Promise<PaginatedResponse<Organization>> {
const queryParams = new URLSearchParams();
if (params.page) queryParams.set("page", params.page.toString());
if (params.limit) queryParams.set("limit", params.limit.toString());
if (params.q) queryParams.set("q", params.q);
if (params.category) queryParams.set("category", params.category);
if (params.tech) queryParams.set("tech", params.tech);

const query = queryParams.toString();
return apiFetchServer<PaginatedResponse<Organization>>(
`/api/organizations${query ? `?${query}` : ""}`
);
// Use API for search or complex filters
const useAPI = shouldUseAPI(params);

if (useAPI) {
if (process.env.NODE_ENV === 'development') {
console.log('[ORGS] Using API - complex filters/search detected');
}
const queryParams = new URLSearchParams();
if (params.page) queryParams.set("page", params.page.toString());
if (params.limit) queryParams.set("limit", params.limit.toString());
if (params.q) queryParams.set("q", params.q);
if (params.category) queryParams.set("category", params.category);
if (params.tech) queryParams.set("tech", params.tech);
if (params.years) queryParams.set("years", params.years);
if (params.categories) queryParams.set("categories", params.categories);
if (params.techs) queryParams.set("techs", params.techs);
if (params.topics) queryParams.set("topics", params.topics);
if (params.firstTimeOnly) queryParams.set("firstTimeOnly", params.firstTimeOnly);
if (params.yearsLogic) queryParams.set("yearsLogic", params.yearsLogic);
if (params.categoriesLogic) queryParams.set("categoriesLogic", params.categoriesLogic);
if (params.techsLogic) queryParams.set("techsLogic", params.techsLogic);
if (params.topicsLogic) queryParams.set("topicsLogic", params.topicsLogic);

const query = queryParams.toString();
return apiFetchServer<PaginatedResponse<Organization>>(
`/api/organizations${query ? `?${query}` : ""}`
);
}

// Use static JSON for simple filters or no filters
if (process.env.NODE_ENV === 'development') {
console.log('[ORGS] Using static JSON');
}
const indexData = await loadOrganizationsIndexData();
if (!indexData) {
if (process.env.NODE_ENV === 'development') {
console.warn('[ORGS] JSON not available, falling back to API');
}
// Fallback to API if JSON not available
const queryParams = new URLSearchParams();
if (params.page) queryParams.set("page", params.page.toString());
if (params.limit) queryParams.set("limit", params.limit.toString());
const query = queryParams.toString();
return apiFetchServer<PaginatedResponse<Organization>>(
`/api/organizations${query ? `?${query}` : ""}`
);
}

// Filter organizations in memory
let filtered = indexData.organizations;

// Apply filters
if (params.years || params.categories || params.techs || params.topics || params.firstTimeOnly) {
filtered = filterOrganizations(indexData.organizations, {
years: params.years ? params.years.split(',').map(y => parseInt(y)).filter(n => !isNaN(n)) : undefined,
categories: params.categories ? params.categories.split(',') : undefined,
techs: params.techs ? params.techs.split(',') : params.tech ? [params.tech] : undefined,
topics: params.topics ? params.topics.split(',') : undefined,
firstTimeOnly: params.firstTimeOnly === 'true',
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Static filtering drops the legacy category param.

When the static path is used, params.category is never applied, so older links will return incorrect results. Map it into categories (same for tech is already handled).

🛠️ Suggested fix
-      categories: params.categories ? params.categories.split(',') : undefined,
+      categories: params.categories
+        ? params.categories.split(',')
+        : params.category
+          ? [params.category]
+          : undefined,
🤖 Prompt for AI Agents
In `@app/organizations/page.tsx` around lines 144 - 219, In getOrganizations, the
static JSON filtering branch drops the legacy params.category; fix by mapping
params.category into the categories argument passed to filterOrganizations (i.e.
when building the object for filterOrganizations, set categories to
params.categories ? params.categories.split(',') : params.category ?
[params.category] : undefined) so old links using ?category=... are honored
(keep the existing tech -> techs fallback logic intact).

Comment thread scripts/generate-organizations-data.js
Comment on lines +99 to +115
for (const org of organizations) {
try {
const orgData = {
...org,
meta: {
version: 1,
generated_at: new Date().toISOString(),
},
};

const orgFile = path.join(OUTPUT_DIR, `${org.slug}.json`);
fs.writeFileSync(orgFile, JSON.stringify(orgData, null, 2));
successCount++;
} catch (error) {
console.error(`[ERROR] Failed to write ${org.slug}.json:`, error.message);
errorCount++;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sanitize org.slug before using it as a filename.

A malicious or malformed slug (e.g., containing / or ..) could write outside the intended directory. Normalize or restrict characters before building the path.

🔒 Suggested hardening
-            const orgFile = path.join(OUTPUT_DIR, `${org.slug}.json`);
+            const safeSlug = String(org.slug).replace(/[^a-z0-9-_]/gi, '_');
+            const orgFile = path.join(OUTPUT_DIR, `${safeSlug}.json`);
🤖 Prompt for AI Agents
In `@scripts/generate-organizations-data.js` around lines 99 - 115, The loop
writing organization files currently uses org.slug directly when building
orgFile with path.join(OUTPUT_DIR, `${org.slug}.json`) which allows path
traversal or invalid filenames; before constructing the path (inside the for
(const org of organizations) loop) validate and sanitize org.slug (e.g., enforce
a whitelist of [a-z0-9-_], replace or strip disallowed chars, disallow empty
strings, reject or normalize segments like "." or "..", and ensure it is not an
absolute path) and then use the sanitized value when calling path.join and
fs.writeFileSync; also consider using path.basename or throwing an error for
invalid slugs to guarantee files stay inside OUTPUT_DIR.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/tech-stack/page.tsx (1)

1-2: Remove unused imports to fix lint warnings.

CI reports Link, Search, and ArrowUpDown as unused imports.

🧹 Proposed fix
-import Link from "next/link";
-import { Search, TrendingUp, BarChart3, Users, Rocket, ArrowUpDown } from "lucide-react";
+import { TrendingUp, BarChart3, Users, Rocket } from "lucide-react";
app/organizations/organizations-client.tsx (1)

21-28: Static-filter navigation can leave stale results.

data is initialized from props once and only updated via fetchOrganizations. When needsAPI is false, the effect returns early, so navigating to a static-filter URL won’t update the list.

🛠️ Suggested fix
 export function OrganizationsClient({ initialData, initialPage, initialTechs }: OrganizationsClientProps) {
   const router = useRouter()
   const searchParams = useSearchParams()
   const [data, setData] = useState<PaginatedResponse<Organization>>(initialData)
   const [isLoading, setIsLoading] = useState(false)
   const [currentPage, setCurrentPage] = useState(initialPage)
@@
   const lastFetchParams = useRef<string>('')
   const lastUrlString = useRef<string>('')
+
+  useEffect(() => {
+    setData(initialData)
+    setCurrentPage(initialPage)
+  }, [initialData, initialPage])

Also applies to: 233-258

🤖 Fix all issues with AI agents
In `@app/`[slug]/year-charts.tsx:
- Around line 262-314: Replace the hardcoded hex palette in SimpleSelectionChart
with the shared chart color helper: remove the local colors array in the Bar ->
{topItems.map(...)} block and instead import and call the project’s chart color
helper (e.g., CHART_COLORS or getChartColors) at the top of the module, then use
that returned array in the Cell fill expression (Cell key={`cell-${index}`}
fill={sharedColors[index % sharedColors.length]}). Ensure you update the import
and variable name references so SimpleSelectionChart uses the central color
token provider rather than hardcoded values.

In `@app/organizations/organizations-client.tsx`:
- Around line 364-370: Remove the Git conflict markers and keep the resolved
aside block that uses the design token background and supplies the new
availableTechs prop: replace the conflicted chunk with a single aside element
using className="hidden lg:block w-[280px] shrink-0 bg-background fixed top-20
lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto
custom-scrollbar" and pass FiltersSidebar the props
onFilterChange={handleFilterChange}, filters={filters},
availableTechs={initialTechs}; ensure no <<<<<<<, =======, or >>>>>>> lines
remain and the component compiles.

In `@app/organizations/page.tsx`:
- Around line 8-12: The import list in app/organizations/page.tsx includes an
unused symbol indexDataToPaginatedResponse which triggers lint warnings; remove
indexDataToPaginatedResponse from the import statement that currently imports
loadOrganizationsIndexData, indexDataToPaginatedResponse, filterOrganizations so
the file only imports the actually used symbols (e.g., keep
loadOrganizationsIndexData and filterOrganizations) to resolve the CI lint
error.
- Around line 193-205: The fallback to apiFetchServer in the
loadOrganizationsIndexData branch only attaches page/limit and thus drops active
filters; update the fallback to serialize and include all relevant
filter/search/sort parameters from params (e.g., any params.filters,
params.search, params.sort or other query props used by the page) into the
URLSearchParams before calling apiFetchServer so the API receives the same
filtering state as the JSON path; locate the code around
loadOrganizationsIndexData, params, queryParams and apiFetchServer and add logic
to iterate/serialize those filter fields into queryParams (handling
arrays/objects appropriately) so the constructed query preserves active filters.

In `@app/tech-stack/`[stack]/tech-stack-detail-client.tsx:
- Around line 78-80: Replace hardcoded hex values and custom teal classes with
the app's design tokens: instead of TEAL_COLORS = ["#0d9488", "#14b8a6",
"#2dd4bf", "#5eead4"] and any inline badge classes, import and reference the
shared color/token module (e.g., the design system or theme export) and map
those token values into the chart and badge props (replace TEAL_COLORS usage and
any hardcoded class names in the blocks around lines 154-172, 178-181, 366-370).
Update references in this file to use the token variables (for example
colors.teal[...], theme.tokens.primaryTeal, or the app-specific token names) so
charts and badges consume the central palette rather than inline hexes.

In `@app/tech-stack/page.tsx`:
- Line 10: Remove the unused type imports TechStackIndexData and TechSummary
from the import statement that currently imports loadTechStackIndexData,
TechStackIndexData, TechSummary in app/tech-stack/page.tsx; update the import to
only import loadTechStackIndexData so the file no longer imports unused types.

In `@app/tech-stack/tech-stack-client-wrapper.tsx`:
- Around line 185-201: The getColor function and the inline style on the
CardWrapper avatar use a hardcoded hex palette; replace them with design
tokens/variables instead: remove or refactor getColor to map stack.name (or its
first char) to a named token (e.g., theme color token keys or CSS variables) and
apply that token instead of an inline hex in the style prop on the element that
renders the avatar; ensure you reference the existing token names used across
the app (or a central color mapping constant) and keep a sensible fallback token
for unknown names while updating the usage in the component that renders
stack.name and the CardWrapper avatar.

In `@app/yearly/`[slug]/client-components.tsx:
- Line 7: Remove the unused Badge import from the component imports to resolve
the lint warning: update the import that currently brings in Badge (alongside
Button, Grid, Text) so Badge is no longer imported; keep the other imports
intact and do not reintroduce Badge unless the commented-out JSX that referenced
it is reinstated.
- Around line 186-197: Replace the custom inline search input with the shared
SearchBar component: remove the surrounding div/input block and render
<SearchBar value={search} onChange={setSearch} placeholder="Search by name or
organization..." /> in its place; ensure you import SearchBar and keep using the
existing state variables search and setSearch so debouncing, clear button and
size variants from SearchBar are applied.

In `@app/yearly/`[slug]/page.tsx:
- Around line 5-30: Remove unused imports and symbols to silence lint warnings:
delete the unused icon imports TrendingUp and Award from the lucide-react import
list, remove the unused type YearlyPageData import from loadYearlyPageData,
remove the unused function/helper BarRow, and remove the unused parameter named
index from any callbacks or map() usages in this module (or mark it as _index if
you must keep it). Update any references accordingly so only used
imports/symbols remain.
- Around line 189-223: The conic-gradient string and the tealColors array in the
project difficulty donut/legend are using hardcoded hex colors; replace those
with the app's design tokens or shared chart color helpers (e.g., CSS variables
like var(--color-teal-500) or a getChartColor(label) utility) so both the
gradient in the style prop (the conic-gradient in the div with className="w-full
h-full rounded-full") and the legend swatches (tealColors and the inline style
on the small square in the map over charts.project_difficulty_distribution.data)
reference tokens/helpers instead of raw hexes; update colorIndex usage to call
the helper or token mapping so the legend and gradient stay in sync.

In `@lib/organizations-page-types.ts`:
- Around line 134-150: The function indexDataToPaginatedResponse currently casts
indexData.organizations to Organization[], but
OrganizationsIndexData.organizations lacks fields (stats, years, contact,
social) present on Organization; instead define a narrower type (e.g.,
OrganizationListItem) that matches the index shape or update the return type to
PaginatedResponse<OrganizationListItem>; then change the function signature and
the items cast to use OrganizationListItem (or adjust all consumers to handle
the slimmer type) so the typing reflects the actual data shape and avoids unsafe
casts.

In `@scripts/generate-projects-data.js`:
- Around line 10-11: The file uses CommonJS require imports (fs, path) which
ESLint forbids and duplicates functionality in
scripts/generate-projects-data.ts; either delete this JavaScript file and rely
on scripts/generate-projects-data.ts, or convert it to ES module syntax by
replacing require usage with import statements and ensuring project node
resolution (e.g., add "type": "module" or rename to .mjs) so references to fs
and path become ES imports.

In `@scripts/generate-projects-data.ts`:
- Around line 206-211: The percentage calculation for topTechnologies can divide
by zero when yearlyData.metrics.total_organizations is 0; update the mapping
that builds topTechnologies to guard against zero (or falsy) total_organizations
by using a conditional fallback (e.g., treat percentage as 0 when
total_organizations is 0) before computing Math.round((t.value /
yearlyData.metrics.total_organizations) * 100), referencing the topTechnologies
construction, yearlyData.charts.top_languages and
yearlyData.metrics.total_organizations to locate the code.
♻️ Duplicate comments (19)
components/trending-orgs-client.tsx (1)

58-58: Fix invalid Tailwind gradient utility (and avoid raw white).

bg-linear-to-r isn’t a Tailwind class, so the overlay gradient won’t render; also via-white/0 hardcodes a color.

✅ Proposed fix
-          <div className="bg-linear-to-r from-background via-white/0 to-background z-10 absolute left-0 top-0 right-0 bottom-0 w-full h-full"></div>
+          <div className="bg-gradient-to-r from-background via-transparent to-background z-10 absolute inset-0 w-full h-full"></div>

As per coding guidelines, avoid hardcoded colors.

app/tech-stack/[stack]/tech-stack-detail-client.tsx (1)

322-395: Reuse the shared OrganizationCard instead of duplicating it.
This local card overlaps with the existing component’s responsibilities and props.

Based on learnings, prefer the shared OrganizationCard (extend it or map TechOrgSnapshot to the shared shape).

scripts/generate-organizations-data.js (2)

12-14: ESLint failure from CommonJS require imports.
The lint rule forbids require() in this repo and currently fails CI.

🛠️ Minimal lint-safe fix
+/* eslint-disable `@typescript-eslint/no-require-imports` */
 const { PrismaClient } = require('@prisma/client');
 const fs = require('fs');
 const path = require('path');

99-110: Sanitize org.slug before using it as a filename.
Untrusted slugs can create invalid paths or traverse directories.

🔒 Suggested hardening
-            const orgFile = path.join(OUTPUT_DIR, `${org.slug}.json`);
+            const safeSlug = String(org.slug).replace(/[^a-z0-9-_]/gi, "_");
+            const orgFile = path.join(OUTPUT_DIR, `${safeSlug}.json`);
scripts/fetch-external-projects.ts (1)

24-27: currentPage should be const or removed.
This triggers the prefer-const lint failure in CI.

🛠️ Minimal fix
-  let currentPage = 1;
+  const currentPage = 1;
scripts/generate-homepage-data.js (2)

11-13: ESLint failure from CommonJS require imports.
The current lint config rejects require() here.

🛠️ Minimal lint-safe fix
+/* eslint-disable `@typescript-eslint/no-require-imports` */
 const { PrismaClient } = require('@prisma/client');
 const fs = require('fs');
 const path = require('path');

91-98: Ensure Prisma disconnects on all exit paths.
If the function throws early, the client may remain connected.

🛠️ Safer shutdown flow
-generateHomepageData().catch((error) => {
-    console.error('[ERROR]', error);
-    process.exit(1);
-});
+generateHomepageData()
+  .catch((error) => {
+    console.error('[ERROR]', error);
+    process.exit(1);
+  })
+  .finally(() => {
+    prisma.$disconnect();
+  });
app/tech-stack/tech-stack-client-wrapper.tsx (1)

27-31: Move setVisibleCount out of useMemo to avoid render loops.
State updates inside memoized render logic can cause infinite re-renders.

🛠️ Safer reset with useEffect
-import { useState, useMemo } from "react";
+import { useEffect, useState, useMemo } from "react";
 ...
-  const filteredTechs = useMemo(() => {
-    // Reset visible count when search or sort changes
-    setVisibleCount(24);
-    
+  useEffect(() => {
+    setVisibleCount(24);
+  }, [techs, searchQuery, sortBy]);
+
+  const filteredTechs = useMemo(() => {
     let result = [...techs];
app/projects/[year]/client-components.tsx (1)

5-5: Remove the unused ExternalLink import.

CI reports ExternalLink as unused, causing a lint warning.

🧹 Proposed fix
-import { Search, ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
+import { Search, ChevronDown, ChevronUp } from "lucide-react";
scripts/generate-projects-data.ts (2)

165-178: Guard against division by zero in percentage calculations.

Multiple calculations divide by total_organizations without checking if it's zero, which could produce Infinity or NaN:

  • Line 168: topTechPercentage
  • Line 177: firstTimeOrgPercentage
🐛 Proposed fix
     // Top tech insight
     const topTech = yearlyData.charts.top_languages[0];
-    const topTechPercentage = topTech 
-      ? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
+    const topTechPercentage = topTech && yearlyData.metrics.total_organizations > 0
+      ? Math.round((topTech.value / yearlyData.metrics.total_organizations) * 100)
       : 0;
     const topTechData = topTech ? {
       name: topTech.label,
       percentage: topTechPercentage
     } : null;
     
     // First-time org percentage
-    const firstTimeOrgPercentage = Math.round(
-      (yearlyData.metrics.first_time_organizations / yearlyData.metrics.total_organizations) * 100
-    );
+    const firstTimeOrgPercentage = yearlyData.metrics.total_organizations > 0
+      ? Math.round(
+          (yearlyData.metrics.first_time_organizations / yearlyData.metrics.total_organizations) * 100
+        )
+      : 0;

222-227: Guard against division by zero in avg_projects_per_org.

If total_organizations is 0, this calculation will fail.

🐛 Proposed fix
       metrics: {
         total_projects: yearlyData.metrics.total_projects,
         total_organizations: yearlyData.metrics.total_organizations,
-        avg_projects_per_org: Math.round((yearlyData.metrics.total_projects / yearlyData.metrics.total_organizations) * 10) / 10,
+        avg_projects_per_org: yearlyData.metrics.total_organizations > 0
+          ? Math.round((yearlyData.metrics.total_projects / yearlyData.metrics.total_organizations) * 10) / 10
+          : 0,
         first_time_org_projects: firstTimeOrgProjects,
       },
scripts/generate-tech-stack-data.js (1)

11-13: Pipeline failure: require() style imports are forbidden.

This issue was previously flagged. Convert to ES module imports or rename to .cjs.

app/projects/[year]/page.tsx (1)

248-264: Pipeline warning: unused index variable.

Remove the unused index parameter to fix the lint warning.

🐛 Fix
-                      {charts.project_difficulty_distribution.data.map((diff, index) => {
+                      {charts.project_difficulty_distribution.data.map((diff) => {
app/organizations/page.tsx (3)

22-22: Increase ISR window to align with cache policy.

revalidate = 3600 is far shorter than the repo’s long-lived cache guidance for SEO pages. Please bump to a 30‑day ISR (or the shared long-cache constant) and keep tag-based invalidation consistent with other pages.
Based on learnings, ...

🛠️ Suggested adjustment
-export const revalidate = 3600; // 1 hour
+export const revalidate = 2592000; // 30 days

88-124: Include legacy category/tech params in the filter count.

Older links using ?category= or ?tech= can be routed to the static path and return unfiltered results because these params are ignored in filterCount.

🛠️ Suggested fix
 function shouldUseAPI(params: {
   q?: string;
   years?: string;
   categories?: string;
   techs?: string;
   topics?: string;
   firstTimeOnly?: string;
+  category?: string;
+  tech?: string;
   yearsLogic?: string;
   categoriesLogic?: string;
   techsLogic?: string;
   topicsLogic?: string;
 }): boolean {
@@
   const filterCount = [
     params.years && params.years.trim().length > 0,
     params.categories && params.categories.trim().length > 0,
     params.techs && params.techs.trim().length > 0,
     params.topics && params.topics.trim().length > 0,
+    params.category && params.category.trim().length > 0,
+    params.tech && params.tech.trim().length > 0,
     params.firstTimeOnly === 'true',
   ].filter(Boolean).length;

212-219: Map legacy category into static filtering.

Static filtering still drops params.category, so older links return incorrect results.

🛠️ Suggested fix
-      categories: params.categories ? params.categories.split(',') : undefined,
+      categories: params.categories
+        ? params.categories.split(',')
+        : params.category
+          ? [params.category]
+          : undefined,
scripts/populate-year-data.ts (1)

102-103: Remove the duplicated comment line.

The // 3. Process Projects comment appears twice.

🧹 Suggested cleanup
-  // 3. Process Projects
   // 3. Process Projects
app/yearly/[slug]/page.tsx (2)

32-34: Differentiate caching between current-year and historical data.

revalidate = false applies to every year, but current-year pages should use long ISR + tag invalidation while historical pages can be cached forever.
Based on learnings, ...


380-386: Aggregate mentors/contributors by org before rendering.

The current mapping generates one row per project, causing duplicate org rows and repeated mentor/contributor names.

🧹 Nitpick comments (14)
components/trending-orgs-client.tsx (2)

61-81: Reuse OrganizationCard (or add a compact variant) for org listings.

This carousel still renders a custom org tile; please reuse OrganizationCard and, if needed, add a compact/logo-only variant to keep listing styles consistent.

Based on learnings, reuse OrganizationCard for organization listings.


63-81: Prefer Next.js Link for internal navigation.

Using Link preserves client-side navigation and prefetching for internal routes.

♻️ Proposed change
-                  <a href={`/organizations/${org.slug}`}>
+                  <Link href={`/organizations/${org.slug}`}>
                     <div className="flex rounded-md aspect-square bg-muted items-center justify-center p-2 hover:bg-accent transition-colors overflow-hidden">
                       {org.img_r2_url ? (
                         <Image
                           src={org.img_r2_url}
                           alt={org.name}
                           width={100}
                           height={100}
                           className="w-full h-full object-cover rounded-md"
                           unoptimized={true}
                           loading="lazy"
                         />
                       ) : (
                         <span className="text-xl font-bold text-muted-foreground">
                           {org.name.charAt(0)}
                         </span>
                       )}
                     </div>
-                  </a>
+                  </Link>
+import Link from "next/link";
app/tech-stack/tech-stack-client-wrapper.tsx (1)

68-79: Prefer the shared SearchBar component for consistency.
This keeps search UX uniform across the app.

Based on learnings, reuse the existing SearchBar/filter components where possible.

app/tech-stack/page.tsx (1)

138-145: Placeholder values in chart data mapping.

The total: 0 and byYear: [] values are explicitly marked as unused. While functional, consider updating the PopularityGrowthChart interface to make these fields optional if they're truly not needed, rather than passing placeholder values.

lib/projects-page-types.ts (1)

107-109: Consider deriving available years dynamically.

The hardcoded year list [2016, 2017, ..., 2025] must be manually updated when new years are added. Consider deriving this from the filesystem or a manifest file to prevent staleness, or document that this needs updating annually.

scripts/generate-projects-data.ts (1)

13-100: Consider importing shared types from lib/projects-page-types.ts.

The YearlyData and ProjectYearPageData interfaces defined here duplicate or closely mirror the types in lib/projects-page-types.ts. Consider importing ProjectYearPageData from the shared types file to maintain a single source of truth and ensure consistency.

import type { ProjectYearPageData } from '../lib/projects-page-types';

// Keep YearlyData local as it represents the input format
interface YearlyData {
  // ... existing definition
}
scripts/generate-tech-stack-data.js (3)

47-50: normalizeName doesn't actually normalize capitalization.

The function only trims whitespace but the comment says "Capitalize properly". This means the first tech name encountered becomes canonical, which could result in inconsistent display names (e.g., "python" vs "Python" depending on source order).

Consider implementing proper title-case or keeping the most common capitalization like the existing API routes do.

♻️ Suggested fix
 function normalizeName(techName) {
-    // Capitalize properly
-    return techName.trim();
+    // Keep original casing but trim whitespace
+    // Note: First occurrence wins for display name
+    return techName.trim();
 }

Or implement proper normalization:

function normalizeName(techName) {
    const trimmed = techName.trim();
    // Title case common tech names, preserve acronyms
    if (trimmed.toLowerCase() === trimmed) {
        return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
    }
    return trimmed;
}

213-232: Hardcoded comparison years for growth calculation may become stale.

The growth metric compares 2020 vs 2025 specifically. Consider deriving these dynamically from YEARS to avoid needing manual updates.

♻️ Suggested improvement
-    // Calculate fastest growing (compare 2020 vs 2025)
+    // Calculate fastest growing (compare earliest vs latest year with data)
+    const baseYear = YEARS[Math.floor(YEARS.length / 2)]; // midpoint
+    const latestYear = YEARS[YEARS.length - 1];
     const fastestGrowing = allTechs
         .map((tech) => {
             const techData = techMap.get(tech.slug);
-            const count2020 = techData.byYear[2020]?.orgCount || 0;
-            const count2025 = techData.byYear[2025]?.orgCount || 0;
-            const growth = count2020 > 0
-                ? ((count2025 - count2020) / count2020) * 100
-                : count2025 > 5 ? 500 : 0;
+            const countBase = techData.byYear[baseYear]?.orgCount || 0;
+            const countLatest = techData.byYear[latestYear]?.orgCount || 0;
+            const growth = countBase > 0
+                ? ((countLatest - countBase) / countBase) * 100
+                : countLatest > 5 ? 500 : 0;
             return {
                 slug: tech.slug,
                 name: tech.name,
                 growth_pct: Math.round(growth),
-                first_year_count: count2020,
-                last_year_count: count2025,
+                first_year_count: countBase,
+                last_year_count: countLatest,
             };
         })
-        .filter((t) => t.last_year_count >= 3) // Must have at least 3 orgs in 2025
+        .filter((t) => t.last_year_count >= 3)

309-313: Missing prisma.$disconnect() in error path.

If generateTechStackData() throws, the Prisma client is never disconnected. While the process exits anyway, it's better practice to ensure cleanup.

♻️ Suggested fix
 // Run
-generateTechStackData().catch((error) => {
+generateTechStackData()
+  .catch((error) => {
     console.error('[ERROR]', error);
+    return prisma.$disconnect().finally(() => process.exit(1));
+  })
+  .finally(() => {
+    // Ensure disconnect even on unhandled promise scenarios
-    process.exit(1);
 });
app/projects/[year]/page.tsx (2)

55-63: Missing canonical URL in metadata.

Per coding guidelines, ensure canonical URLs are consistent across the application. The generateMetadata function should include an alternates.canonical field.

♻️ Suggested fix
   return {
     title: data.title,
     description: data.description,
     openGraph: {
       title: data.title,
       description: data.description,
     },
+    alternates: {
+      canonical: `/projects/${year}`,
+    },
   };

Based on learnings, ensure canonical URLs and consistent metadata patterns across the application.


226-245: Consider extracting repeated .find() calls for difficulty percentages.

The conic-gradient calculation repeatedly searches the array for each difficulty level. Extract these values once for clarity and minor performance improvement.

♻️ Suggested improvement
                   <div className="flex flex-col items-center justify-center">
                     <div className="relative w-48 h-48">
+                      {(() => {
+                        const beginnerPct = charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0;
+                        const intermediatePct = charts.project_difficulty_distribution.data.find(d => d.label === "Intermediate")?.percentage || 0;
+                        return (
                       <div
                         className="w-full h-full rounded-full"
                         style={{
                           background: `conic-gradient(
                             from 0deg,
-                            `#0d9488` 0% ${charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0}%,
-                            `#14b8a6` ${charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0}% ${(charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0) + (charts.project_difficulty_distribution.data.find(d => d.label === "Intermediate")?.percentage || 0)}%,
-                            `#2dd4bf` ${(charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0) + (charts.project_difficulty_distribution.data.find(d => d.label === "Intermediate")?.percentage || 0)}% 100%
+                            `#0d9488` 0% ${beginnerPct}%,
+                            `#14b8a6` ${beginnerPct}% ${beginnerPct + intermediatePct}%,
+                            `#2dd4bf` ${beginnerPct + intermediatePct}% 100%
                           )`,
                         }}
                       />
+                        );
+                      })()}
app/yearly/[slug]/client-components.tsx (2)

265-283: Consider using Search from lucide-react instead of custom SVG.

The Search icon is already available from lucide-react which is imported for ArrowDown and ArrowUp. This reduces code and maintains consistency.

♻️ Suggested fix
-import { ArrowDown, ArrowUp } from "lucide-react";
+import { ArrowDown, ArrowUp, Search } from "lucide-react";

Then replace usage:

-           <SearchIcon className="w-4 h-4" />
+           <Search className="w-4 h-4" />

And remove the custom SearchIcon component (lines 265-283).


119-125: Commented-out code should be removed or implemented.

The tech stack badges are commented out. If this feature is planned, consider creating a TODO issue. Otherwise, remove the dead code.

scripts/populate-year-data.ts (1)

127-148: Avoid O(N²) lookups when counting mentors/participants.

processedProjects.find(...) runs once per project; building a lookup map keeps this linear and speeds up large years.

♻️ Suggested refactor
   const processedProjects = projects.map(p => {
     let contributor = p.contributor || "Unknown";
@@
     return {
       id: p.project_id || p.id, // Use project_id if available
       title: p.project_title,
       org_slug: p.org_slug,
       contributor: contributor, // Add enriched contributor
       mentors: mentors,         // Add enriched mentors
       tech_stack: ["unknown"]   // Explicitly mark as "unknown"
     };
   });
+
+  const processedProjectsById = new Map(
+    processedProjects.map(pp => [pp.id, pp])
+  );

@@
   projects.forEach(p => {
-      const processed = processedProjects.find(pp => pp.id === (p.project_id || p.id));
+      const processed = processedProjectsById.get(p.project_id || p.id);
       if (processed) {
         if (processed.contributor && processed.contributor !== "Unknown") allParticipants.add(processed.contributor);

Comment on lines +262 to +314
// Chart 4: Simple Selection Bar Chart (Matches gsoc-year-client.tsx style)
export function SimpleSelectionChart({
data
}: {
data: Array<{ name: string; count: number }>;
}) {
const topItems = data.slice(0, 10);

return (
<div className="h-[320px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={topItems}
layout="vertical"
margin={{ top: 5, right: 50, left: 0, bottom: 5 }}
>
<XAxis
type="number"
tick={{ fontSize: 10, fill: "#6b7280" }}
tickLine={false}
axisLine={false}
/>
<YAxis
dataKey="name"
type="category"
tick={{ fontSize: 12, fill: "#374151", fontWeight: 500 }}
tickLine={false}
axisLine={false}
width={100}
/>
<Tooltip
contentStyle={{
backgroundColor: "white",
border: "1px solid #e5e7eb",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [value.toLocaleString(), "Selections"]}
/>
<Bar
dataKey="count"
radius={[0, 4, 4, 0]}
maxBarSize={28}
>
{topItems.map((_, index) => {
const colors = [
"#0d9488", "#14b8a6", "#2dd4bf", "#5eead4", "#99f6e4",
"#0d9488", "#14b8a6", "#2dd4bf", "#5eead4", "#99f6e4",
];
return (
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
);
})}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hardcoded chart colors; reuse theme tokens.

The new palette hardcodes hex values, which breaks the “use only existing color tokens” rule. Reuse the shared chart color helper instead.
As per coding guidelines, ...

♻️ Suggested change
-            {topItems.map((_, index) => {
-              const colors = [
-                "#0d9488", "#14b8a6", "#2dd4bf", "#5eead4", "#99f6e4",
-                "#0d9488", "#14b8a6", "#2dd4bf", "#5eead4", "#99f6e4",
-              ];
-              return (
-                <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
-              );
-            })}
+            {topItems.map((_, index) => (
+              <Cell key={`cell-${index}`} fill={getChartBarColor(index)} />
+            ))}
🤖 Prompt for AI Agents
In `@app/`[slug]/year-charts.tsx around lines 262 - 314, Replace the hardcoded hex
palette in SimpleSelectionChart with the shared chart color helper: remove the
local colors array in the Bar -> {topItems.map(...)} block and instead import
and call the project’s chart color helper (e.g., CHART_COLORS or getChartColors)
at the top of the module, then use that returned array in the Cell fill
expression (Cell key={`cell-${index}`} fill={sharedColors[index %
sharedColors.length]}). Ensure you update the import and variable name
references so SimpleSelectionChart uses the central color token provider rather
than hardcoded values.

Comment on lines +364 to +370
<<<<<<< HEAD
<aside className="hidden lg:block w-[280px] shrink-0 bg-background fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
<FiltersSidebar onFilterChange={handleFilterChange} filters={filters} />
=======
<aside className="hidden lg:block w-[280px] shrink-0 bg-white fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
<FiltersSidebar onFilterChange={handleFilterChange} filters={filters} availableTechs={initialTechs} />
>>>>>>> 3ff5d5c (fix(tech-stack): fixed)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Resolve merge conflict markers (build blocker).

Conflict markers are still present, causing the parse error in CI. Choose a single branch (prefer design tokens and the new availableTechs prop).

🛠️ Suggested resolution
-<<<<<<< HEAD
-      <aside className="hidden lg:block w-[280px] shrink-0 bg-background fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
-        <FiltersSidebar onFilterChange={handleFilterChange} filters={filters} />
-=======
-      <aside className="hidden lg:block w-[280px] shrink-0 bg-white fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
-        <FiltersSidebar onFilterChange={handleFilterChange} filters={filters} availableTechs={initialTechs} />
->>>>>>> 3ff5d5c (fix(tech-stack): fixed)
+      <aside className="hidden lg:block w-[280px] shrink-0 bg-background fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
+        <FiltersSidebar onFilterChange={handleFilterChange} filters={filters} availableTechs={initialTechs} />
       </aside>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<<<<<<< HEAD
<aside className="hidden lg:block w-[280px] shrink-0 bg-background fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
<FiltersSidebar onFilterChange={handleFilterChange} filters={filters} />
=======
<aside className="hidden lg:block w-[280px] shrink-0 bg-white fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
<FiltersSidebar onFilterChange={handleFilterChange} filters={filters} availableTechs={initialTechs} />
>>>>>>> 3ff5d5c (fix(tech-stack): fixed)
<aside className="hidden lg:block w-[280px] shrink-0 bg-background fixed top-20 lg:top-24 left-4 h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar">
<FiltersSidebar onFilterChange={handleFilterChange} filters={filters} availableTechs={initialTechs} />
</aside>
🧰 Tools
🪛 Biome (2.1.2)

[error] 365-365: expected , but instead found className

Remove className

(parse)


[error] 365-365: expected , but instead found =

Remove =

(parse)


[error] 366-366: expected > but instead found <

Remove <

(parse)


[error] 364-365: Expected corresponding JSX closing tag for 'HEAD'.

Opening tag

closing tag

(parse)


[error] 369-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 370-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 370-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 370-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 370-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 370-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 370-370: Unexpected token. Did you mean {'>'} or &gt;?

(parse)

🪛 GitHub Actions: CI

[error] 364-364: Parsing error: Merge conflict marker encountered

🤖 Prompt for AI Agents
In `@app/organizations/organizations-client.tsx` around lines 364 - 370, Remove
the Git conflict markers and keep the resolved aside block that uses the design
token background and supplies the new availableTechs prop: replace the
conflicted chunk with a single aside element using className="hidden lg:block
w-[280px] shrink-0 bg-background fixed top-20 lg:top-24 left-4
h-[calc(100vh-5rem)] lg:h-[calc(100vh-6rem)] overflow-y-auto custom-scrollbar"
and pass FiltersSidebar the props onFilterChange={handleFilterChange},
filters={filters}, availableTechs={initialTechs}; ensure no <<<<<<<, =======, or
>>>>>>> lines remain and the component compiles.

Comment thread app/organizations/page.tsx
Comment on lines +193 to +205
const indexData = await loadOrganizationsIndexData();
if (!indexData) {
if (process.env.NODE_ENV === 'development') {
console.warn('[ORGS] JSON not available, falling back to API');
}
// Fallback to API if JSON not available
const queryParams = new URLSearchParams();
if (params.page) queryParams.set("page", params.page.toString());
if (params.limit) queryParams.set("limit", params.limit.toString());
const query = queryParams.toString();
return apiFetchServer<PaginatedResponse<Organization>>(
`/api/organizations${query ? `?${query}` : ""}`
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fallback API call drops active filters.

If the JSON index is unavailable, the fallback API request only includes page/limit, so any active filters are lost and results become incorrect.

🛠️ Suggested fix
   const queryParams = new URLSearchParams();
   if (params.page) queryParams.set("page", params.page.toString());
   if (params.limit) queryParams.set("limit", params.limit.toString());
+  if (params.q) queryParams.set("q", params.q);
+  if (params.category) queryParams.set("category", params.category);
+  if (params.tech) queryParams.set("tech", params.tech);
+  if (params.years) queryParams.set("years", params.years);
+  if (params.categories) queryParams.set("categories", params.categories);
+  if (params.techs) queryParams.set("techs", params.techs);
+  if (params.topics) queryParams.set("topics", params.topics);
+  if (params.firstTimeOnly) queryParams.set("firstTimeOnly", params.firstTimeOnly);
+  if (params.yearsLogic) queryParams.set("yearsLogic", params.yearsLogic);
+  if (params.categoriesLogic) queryParams.set("categoriesLogic", params.categoriesLogic);
+  if (params.techsLogic) queryParams.set("techsLogic", params.techsLogic);
+  if (params.topicsLogic) queryParams.set("topicsLogic", params.topicsLogic);
🤖 Prompt for AI Agents
In `@app/organizations/page.tsx` around lines 193 - 205, The fallback to
apiFetchServer in the loadOrganizationsIndexData branch only attaches page/limit
and thus drops active filters; update the fallback to serialize and include all
relevant filter/search/sort parameters from params (e.g., any params.filters,
params.search, params.sort or other query props used by the page) into the
URLSearchParams before calling apiFetchServer so the API receives the same
filtering state as the JSON path; locate the code around
loadOrganizationsIndexData, params, queryParams and apiFetchServer and add logic
to iterate/serialize those filter fields into queryParams (handling
arrays/objects appropriately) so the constructed query preserves active filters.

Comment on lines +78 to +80
// Teal color palette for charts
const TEAL_COLORS = ["#0d9488", "#14b8a6", "#2dd4bf", "#5eead4"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace hardcoded chart + badge colors with design tokens.
Inline hex colors and custom teal classes bypass the shared design palette.

As per coding guidelines, please use existing color tokens/variables instead of hardcoded colors.

Also applies to: 154-172, 178-181, 366-370

🤖 Prompt for AI Agents
In `@app/tech-stack/`[stack]/tech-stack-detail-client.tsx around lines 78 - 80,
Replace hardcoded hex values and custom teal classes with the app's design
tokens: instead of TEAL_COLORS = ["#0d9488", "#14b8a6", "#2dd4bf", "#5eead4"]
and any inline badge classes, import and reference the shared color/token module
(e.g., the design system or theme export) and map those token values into the
chart and badge props (replace TEAL_COLORS usage and any hardcoded class names
in the blocks around lines 154-172, 178-181, 366-370). Update references in this
file to use the token variables (for example colors.teal[...],
theme.tokens.primaryTeal, or the app-specific token names) so charts and badges
consume the central palette rather than inline hexes.

Comment thread app/yearly/[slug]/page.tsx
Comment on lines +189 to +223
<div
className="w-full h-full rounded-full"
style={{
background: `conic-gradient(
from 0deg,
#0d9488 0% ${charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0}%,
#14b8a6 ${charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0}% ${(charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0) + (charts.project_difficulty_distribution.data.find(d => d.label === "Intermediate")?.percentage || 0)}%,
#2dd4bf ${(charts.project_difficulty_distribution.data.find(d => d.label === "Beginner")?.percentage || 0) + (charts.project_difficulty_distribution.data.find(d => d.label === "Intermediate")?.percentage || 0)}% 100%
)`,
}}
/>
<div className="absolute inset-[30%] bg-background rounded-full flex items-center justify-center">
<div className="text-center">
<Text className="text-2xl font-bold">{metrics.total_organizations}</Text>
<Text variant="small" className="text-muted-foreground">orgs</Text>
</div>
</div>
</div>

<div className="space-y-2 mt-4">
{charts.project_difficulty_distribution.data.map((diff: { label: string; value: number; percentage?: number }, index) => {
const tealColors = ["#0d9488", "#14b8a6", "#2dd4bf"];
// Map labels to match colors if order is guaranteed, typically Beginner/Intermediate/Advanced
// Our JSON has "Beginner", "Intermediate", "Advanced"
const colorIndex = diff.label === "Beginner" ? 0 : diff.label === "Intermediate" ? 1 : 2;

// We need to calculate percentage if it's missing or 0 in JSON (script sets 0 for beginner/advanced currently, need to rely on what's there)
const percentage = diff.percentage || Math.round((diff.value / metrics.total_projects) * 100) || 0;

return (
<div key={diff.label} className="flex items-center gap-3">
<div
className="w-4 h-4 rounded-sm"
style={{ backgroundColor: tealColors[colorIndex] }}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace hardcoded hex colors with design tokens.

The conic-gradient and legend colors are hardcoded, which violates the “use existing color tokens” rule. Use theme variables or the shared chart color helpers instead.
As per coding guidelines, ...

🧰 Tools
🪛 GitHub Actions: CI

[warning] 209-209: eslint: 'index' is defined but never used. (no-unused-vars)

🤖 Prompt for AI Agents
In `@app/yearly/`[slug]/page.tsx around lines 189 - 223, The conic-gradient string
and the tealColors array in the project difficulty donut/legend are using
hardcoded hex colors; replace those with the app's design tokens or shared chart
color helpers (e.g., CSS variables like var(--color-teal-500) or a
getChartColor(label) utility) so both the gradient in the style prop (the
conic-gradient in the div with className="w-full h-full rounded-full") and the
legend swatches (tealColors and the inline style on the small square in the map
over charts.project_difficulty_distribution.data) reference tokens/helpers
instead of raw hexes; update colorIndex usage to call the helper or token
mapping so the legend and gradient stay in sync.

Comment on lines +134 to +150
export function indexDataToPaginatedResponse(
indexData: OrganizationsIndexData,
page: number = 1,
limit: number = 20
): PaginatedResponse<Organization> {
const start = (page - 1) * limit;
const end = start + limit;
const items = indexData.organizations.slice(start, end);

return {
page,
limit,
total: indexData.total,
pages: Math.ceil(indexData.total / limit),
items: items as Organization[],
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: OrganizationsIndexData['organizations'] is not compatible with Organization[].

The index organization type (lines 21-39) includes url and first_time but lacks stats, years, contact, and social fields that Organization from lib/api.ts may have. The cast at line 148 could cause runtime issues if consumers expect the full Organization type.

Consider either:

  1. Defining a minimal OrganizationListItem type for list views
  2. Ensuring the return type reflects the actual data shape
♻️ Suggested fix - Option 1: Use more accurate typing
+// Minimal organization type for list views
+export type OrganizationListItem = OrganizationsIndexData['organizations'][number];

 export function indexDataToPaginatedResponse(
   indexData: OrganizationsIndexData,
   page: number = 1,
   limit: number = 20
-): PaginatedResponse<Organization> {
+): PaginatedResponse<OrganizationListItem> {
   const start = (page - 1) * limit;
   const end = start + limit;
   const items = indexData.organizations.slice(start, end);

   return {
     page,
     limit,
     total: indexData.total,
     pages: Math.ceil(indexData.total / limit),
-    items: items as Organization[],
+    items,
   };
 }
🤖 Prompt for AI Agents
In `@lib/organizations-page-types.ts` around lines 134 - 150, The function
indexDataToPaginatedResponse currently casts indexData.organizations to
Organization[], but OrganizationsIndexData.organizations lacks fields (stats,
years, contact, social) present on Organization; instead define a narrower type
(e.g., OrganizationListItem) that matches the index shape or update the return
type to PaginatedResponse<OrganizationListItem>; then change the function
signature and the items cast to use OrganizationListItem (or adjust all
consumers to handle the slimmer type) so the typing reflects the actual data
shape and avoids unsafe casts.

Comment on lines +10 to +11
const fs = require('fs');
const path = require('path');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use ES module imports or remove this file in favor of the TypeScript version.

ESLint reports that require() style imports are forbidden. This JavaScript file appears to duplicate the functionality of scripts/generate-projects-data.ts. Consider:

  1. Removing this .js file and using only the TypeScript version, or
  2. Converting to ES module syntax if a JavaScript version is required
🔧 Option 1: Convert to ES modules (if keeping .js)
-const fs = require('fs');
-const path = require('path');
+import * as fs from 'fs';
+import * as path from 'path';

Note: This would also require adding "type": "module" to package.json or using .mjs extension.

🔧 Option 2: Remove this file

Delete scripts/generate-projects-data.js and use only scripts/generate-projects-data.ts:

rm scripts/generate-projects-data.js

Run the TypeScript version with: npx ts-node scripts/generate-projects-data.ts

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fs = require('fs');
const path = require('path');
import * as fs from 'fs';
import * as path from 'path';
🧰 Tools
🪛 ESLint

[error] 10-10: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)


[error] 11-11: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🪛 GitHub Check: build-and-lint

[failure] 11-11:
A require() style import is forbidden


[failure] 10-10:
A require() style import is forbidden

🤖 Prompt for AI Agents
In `@scripts/generate-projects-data.js` around lines 10 - 11, The file uses
CommonJS require imports (fs, path) which ESLint forbids and duplicates
functionality in scripts/generate-projects-data.ts; either delete this
JavaScript file and rely on scripts/generate-projects-data.ts, or convert it to
ES module syntax by replacing require usage with import statements and ensuring
project node resolution (e.g., add "type": "module" or rename to .mjs) so
references to fs and path become ES imports.

Comment on lines +206 to +211
const topTechnologies = yearlyData.charts.top_languages.slice(0, 15).map(t => ({
label: t.label,
slug: t.label.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
value: t.value,
percentage: Math.round((t.value / yearlyData.metrics.total_organizations) * 100)
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against division by zero in topTechnologies percentage.

Same issue as above - if total_organizations is 0, this will produce Infinity.

🐛 Proposed fix
     const topTechnologies = yearlyData.charts.top_languages.slice(0, 15).map(t => ({
       label: t.label,
       slug: t.label.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
       value: t.value,
-      percentage: Math.round((t.value / yearlyData.metrics.total_organizations) * 100)
+      percentage: yearlyData.metrics.total_organizations > 0
+        ? Math.round((t.value / yearlyData.metrics.total_organizations) * 100)
+        : 0
     }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const topTechnologies = yearlyData.charts.top_languages.slice(0, 15).map(t => ({
label: t.label,
slug: t.label.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
value: t.value,
percentage: Math.round((t.value / yearlyData.metrics.total_organizations) * 100)
}));
const topTechnologies = yearlyData.charts.top_languages.slice(0, 15).map(t => ({
label: t.label,
slug: t.label.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
value: t.value,
percentage: yearlyData.metrics.total_organizations > 0
? Math.round((t.value / yearlyData.metrics.total_organizations) * 100)
: 0
}));
🤖 Prompt for AI Agents
In `@scripts/generate-projects-data.ts` around lines 206 - 211, The percentage
calculation for topTechnologies can divide by zero when
yearlyData.metrics.total_organizations is 0; update the mapping that builds
topTechnologies to guard against zero (or falsy) total_organizations by using a
conditional fallback (e.g., treat percentage as 0 when total_organizations is 0)
before computing Math.round((t.value / yearlyData.metrics.total_organizations) *
100), referencing the topTechnologies construction,
yearlyData.charts.top_languages and yearlyData.metrics.total_organizations to
locate the code.

@ketankauntia ketankauntia merged commit cdc58ea into master Jan 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant