Feat/v3 -> optimizations & new routes#84
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo 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 Warning
|
| 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
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
Estimated code review effort
🎯 5 (Critical) | ⏱️ ~120 minutes
Possibly related PRs
- Filter Sidebar + other changes #67: Overlapping changes to organizations filter UI and OrganizationsClient prop/signature updates.
- Pre Commit PR #58: Related edits to Husky hook scripts (
.husky/commit-msg,.husky/pre-commit). - v2 Major Changes #76: Broad refactor touching tech-stack, yearly, and organizations pages and static-data migration patterns.
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 | 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 | 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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
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.jsLinkcomponent for client-side navigation.Using a plain
<a>tag instead of Next.jsLinkloses 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.canonicalfield.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
StatCardcomponent appears to be duplicated inapp/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_COLORSarray 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 frequentlyBased on learnings, prefer long-lived caches over frequent regeneration for performance and scale.
app/organizations/organizations-client.tsx (1)
312-323: Remove unused callbackstoggleDifficultyandisDifficultySelected.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 importingProjectYearPageDatafrom the shared types module.The
ProjectYearPageDatainterface defined here duplicates the one inlib/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_atchanges 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 yearAlternatively, only update
published_atwhen actual data changes by comparing with the existing file.app/[slug]/year-charts.tsx (3)
200-205: Unusedyearparameter inOrganizationsProjectsChart.The
year?: stringprop 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.
SimpleSelectionChartuses a hardcodedcolorsarray while other charts in this file usegetChartBarColor(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.
SimpleSelectionChartusesTooltipfrom recharts directly, while other charts in this file useChartTooltipfrom@/components/ui/chart. This creates visual inconsistency across charts. Consider usingChartTooltipfor 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
count2020is 0 andcount2025is exactly 5, the growth is set to 0 due to the> 5threshold (line 221). Consider using>= 5if 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 techsapp/projects/[year]/page.tsx (2)
357-372: Hardcoded year bounds may drift out of sync.The year navigation buttons hardcode
2016and2025as boundaries, butgetAvailableProjectYears()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: Hardcodingtech_stack: ["unknown"]discards potential data.Every project is marked with
tech_stack: ["unknown"], even though the organization may havetechnologiesdata 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 aprojects.forEach()loop creates O(N²) complexity. SinceprocessedProjectsis derived fromprojectsin the same order, you can iterate directly overprocessedProjectsinstead.♻️ 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: ReuseOrganizationCardfor org listings.This custom card duplicates an org listing UI; please switch to the shared
OrganizationCardfor consistency and DRY. Based on learnings, reuse the existingOrganizationCardfor organization listings.
186-197: Use the sharedSearchBarfor 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 existingSearchBarand 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.
|
|
||
| # others | ||
| /sample-gsoc-guide/ | ||
| /new-api-details/ | ||
| /things-to-do/ No newline at end of file |
There was a problem hiding this comment.
🧩 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
fiRepository: 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/
fiRepository: 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.ymlRepository: 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.
| icon={<TrendingUp className="w-5 h-5 text-primary" />} | ||
| label="Latest Year" | ||
| value="2025" | ||
| /> |
There was a problem hiding this comment.
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.
| /** | ||
| * 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the existing OrganizationCard component
rg -n "export.*OrganizationCard" --type ts --type tsx -B 2 -A 20 | head -100Repository: 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 -30Repository: 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 -20Repository: ketankauntia/gsoc-orgs
Length of output: 242
🏁 Script executed:
# Read the existing OrganizationCard component
cat -n components/organization-card.tsxRepository: 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 -80Repository: 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 2Repository: 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 -80Repository: 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 -100Repository: ketankauntia/gsoc-orgs
Length of output: 4149
🏁 Script executed:
# Check what properties Organization has
rg -n "interface Organization|type Organization" -A 20 | head -120Repository: 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:
- Extending
OrganizationCardwith additional props (e.g.,showCategory,showStatus) to support the category and active status badges used here - Transforming
TechOrgSnapshotto matchOrganizationshape where possible (providingdescriptionandtechnologiesdata, 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.
| const { PrismaClient } = require('@prisma/client'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); |
There was a problem hiding this comment.
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.
| await prisma.$disconnect(); | ||
| } | ||
|
|
||
| // Run | ||
| generateHomepageData().catch((error) => { | ||
| console.error('[ERROR]', error); | ||
| process.exit(1); | ||
| }); |
There was a problem hiding this comment.
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.
| // 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; |
There was a problem hiding this comment.
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.
| // 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.
| const { PrismaClient } = require('@prisma/client'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); |
There was a problem hiding this comment.
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.
| 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.
| // 3. Process Projects | ||
| // 3. Process Projects |
There was a problem hiding this comment.
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.
| // 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.
There was a problem hiding this comment.
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) returnapp/organizations/[slug]/page.tsx (1)
64-79:generateMetadatamay trigger an uncached API fetch.
generateMetadata(line 88) callsgetOrganization, 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 |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -20Repository: ketankauntia/gsoc-orgs
Length of output: 536
🏁 Script executed:
# Check the .cursorrules file if it exists
git ls-files | grep -i cursorrulesRepository: ketankauntia/gsoc-orgs
Length of output: 78
🏁 Script executed:
# Search for revalidate patterns in the codebase
rg "revalidate\s*=" --type ts --type tsx | head -30Repository: ketankauntia/gsoc-orgs
Length of output: 93
🏁 Script executed:
# Search for revalidateTag patterns
rg "revalidateTag" --type ts --type tsxRepository: ketankauntia/gsoc-orgs
Length of output: 93
🏁 Script executed:
# Read .cursorrules to understand caching guidance
cat .cursorrulesRepository: ketankauntia/gsoc-orgs
Length of output: 10354
🏁 Script executed:
# Read the organizations page to understand current implementation
cat -n app/organizations/page.tsxRepository: ketankauntia/gsoc-orgs
Length of output: 11555
🏁 Script executed:
# Search for revalidate patterns without problematic file type flags
rg "revalidate" | head -30Repository: 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.
| 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', | ||
| }); |
There was a problem hiding this comment.
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).
| 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++; | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, andArrowUpDownas 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.
datais initialized from props once and only updated viafetchOrganizations. WhenneedsAPIis 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-risn’t a Tailwind class, so the overlay gradient won’t render; alsovia-white/0hardcodes 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 sharedOrganizationCardinstead 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 mapTechOrgSnapshotto the shared shape).scripts/generate-organizations-data.js (2)
12-14: ESLint failure from CommonJSrequireimports.
The lint rule forbidsrequire()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: Sanitizeorg.slugbefore 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:currentPageshould beconstor 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 CommonJSrequireimports.
The current lint config rejectsrequire()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: MovesetVisibleCountout ofuseMemoto 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 unusedExternalLinkimport.CI reports
ExternalLinkas 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_organizationswithout checking if it's zero, which could produceInfinityorNaN:
- 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_organizationsis 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: unusedindexvariable.Remove the unused
indexparameter 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 = 3600is 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 legacycategory/techparams 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 infilterCount.🛠️ 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 legacycategoryinto 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 Projectscomment appears twice.🧹 Suggested cleanup
- // 3. Process Projects // 3. Process Projectsapp/yearly/[slug]/page.tsx (2)
32-34: Differentiate caching between current-year and historical data.
revalidate = falseapplies 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: ReuseOrganizationCard(or add a compact variant) for org listings.This carousel still renders a custom org tile; please reuse
OrganizationCardand, 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.jsLinkfor internal navigation.Using
Linkpreserves 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 sharedSearchBarcomponent 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: 0andbyYear: []values are explicitly marked as unused. While functional, consider updating thePopularityGrowthChartinterface 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
YearlyDataandProjectYearPageDatainterfaces defined here duplicate or closely mirror the types inlib/projects-page-types.ts. Consider importingProjectYearPageDatafrom 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:normalizeNamedoesn'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
YEARSto 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: Missingprisma.$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
generateMetadatafunction should include analternates.canonicalfield.♻️ 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 usingSearchfrom lucide-react instead of custom SVG.The
Searchicon is already available fromlucide-reactwhich is imported forArrowDownandArrowUp. 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
SearchIconcomponent (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);
| // 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]} /> | ||
| ); | ||
| })} |
There was a problem hiding this comment.
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.
| <<<<<<< 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) |
There was a problem hiding this comment.
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.
| <<<<<<< 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 >?
(parse)
[error] 370-370: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 370-370: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 370-370: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 370-370: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 370-370: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 370-370: Unexpected token. Did you mean {'>'} or >?
(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.
| 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}` : ""}` | ||
| ); |
There was a problem hiding this comment.
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.
| // Teal color palette for charts | ||
| const TEAL_COLORS = ["#0d9488", "#14b8a6", "#2dd4bf", "#5eead4"]; | ||
|
|
There was a problem hiding this comment.
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.
| <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] }} | ||
| /> |
There was a problem hiding this comment.
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.
| 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[], | ||
| }; | ||
| } |
There was a problem hiding this comment.
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:
- Defining a minimal
OrganizationListItemtype for list views - 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.
| const fs = require('fs'); | ||
| const path = require('path'); |
There was a problem hiding this comment.
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:
- Removing this
.jsfile and using only the TypeScript version, or - 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.jsRun 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.
| 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.
| 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) | ||
| })); |
There was a problem hiding this comment.
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.
| 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.
Summary
Testing
Checklist
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.