Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@
## 2024-06-25 - DebouncedInput Component Memoization
**Learning:** The `DebouncedInput` component was used in several places to prevent excessive parent re-renders while typing. However, because `DebouncedInput` itself was not wrapped in `React.memo()`, it was still re-rendering unnecessarily whenever its parent components re-rendered for reasons unrelated to the input (e.g. other form fields changing, complex page state updates). This unnecessary re-rendering could destroy and recreate its internal debouncing logic and cause extra reconciliation work.
**Action:** When creating utility wrapper components designed to optimize performance (like `DebouncedInput`), ensure the component itself is memoized with `React.memo()` so it fully shields itself from irrelevant parent state updates.

## 2024-07-09 - Avoid Multiple Array Iterations for Data Aggregation
**Learning:** In functions like `computeStats` (in `about-page.tsx` and `use-platform-stats.ts`) that compute multiple aggregate metrics from an array of objects, chaining methods like `.map().filter()`, `.flatMap()`, or doing multiple explicit `for` loops across the entire dataset causes unnecessary intermediate array allocations and redundant O(n) passes. In extreme cases, doing this 8-10 times over a large dataset blocks the main thread.
**Action:** Always consolidate multiple map/reduce/filter operations into a single O(n) `for` loop. Keep multiple accumulator dictionaries/counters and update them all at once per item. Also, derive counts (like total distinct items) directly from the keys/size of the accumulators instead of recalculating them from the original array.
27 changes: 22 additions & 5 deletions src/hooks/use-platform-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@ export interface PlatformStats {
techCount: number;
}

// ⚡ Bolt: Consolidated multiple array passes (.map.filter, .filter, .flatMap) into a single O(n) loop
function computeStats(
profiles: Array<{ city?: string | null; open_to_collaboration?: boolean | null; tech_stack?: string[] | null }>
): PlatformStats {
const total = profiles.length;
if (total === 0) return { totalUsers: 0, totalCities: 0, collaborationRate: 0, techCount: 0 };
const cities = new Set(profiles.map((p) => p.city).filter(Boolean)).size;
const collab = profiles.filter((p) => p.open_to_collaboration).length;
const allTechs = new Set(profiles.flatMap((p) => p.tech_stack ?? [])).size;

let collab = 0;
const citiesSet = new Set<string>();
const techsSet = new Set<string>();

for (const p of profiles) {
if (p.open_to_collaboration) {
collab++;
}
if (p.city) {
citiesSet.add(p.city);
}
if (p.tech_stack) {
for (const tech of p.tech_stack) {
techsSet.add(tech);
}
}
}

return {
totalUsers: total,
totalCities: cities,
totalCities: citiesSet.size,
collaborationRate: Math.round((collab / total) * 100),
techCount: allTechs,
techCount: techsSet.size,
};
}

Expand Down
95 changes: 60 additions & 35 deletions src/pages/about-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const CATEGORY_COLORS: Record<string, string> = {
"Autres profils": "bg-chart-5",
};

// ⚡ Bolt: Consolidated multiple iterations into a single O(n) pass over profiles.
function computeStats(profiles: RawProfile[]): ComputedStats {
const total = profiles.length;
if (total === 0) {
Expand All @@ -123,9 +124,64 @@ function computeStats(profiles: RawProfile[]): ComputedStats {
}

const roleCounts: Record<string, number> = {};
for (const p of profiles)
const expCounts: Record<string, number> = {};
const techCounts: Record<string, number> = {};
const cityCounts: Record<string, number> = {};

let collab = 0;
let activeLastWeek = 0;

const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);

const months = Array.from({ length: 6 }, (_, i) => {
const d = new Date(now.getFullYear(), now.getMonth() - 5 + i, 1);
return {
month: d.toLocaleDateString("fr-FR", { month: "short" }),
year: d.getFullYear(),
monthNum: d.getMonth(),
count: 0,
};
});

// ⚡ Bolt: Single pass loop for all aggregations
for (const p of profiles) {
// Roles
roleCounts[p.role_type] = (roleCounts[p.role_type] || 0) + 1;

// Experience
expCounts[p.experience_level] = (expCounts[p.experience_level] || 0) + 1;

// Techs
if (p.tech_stack) {
for (const t of p.tech_stack) {
techCounts[t] = (techCounts[t] || 0) + 1;
}
}

// Cities
if (p.city) {
cityCounts[p.city] = (cityCounts[p.city] || 0) + 1;
}

// Growth
const d = new Date(p.created_at);
const entry = months.find(
(m) => m.year === d.getFullYear() && m.monthNum === d.getMonth()
);
if (entry) entry.count++;

// Collaboration
if (p.open_to_collaboration) {
collab++;
}

// Active last week
if (p.last_seen_at && new Date(p.last_seen_at) >= weekAgo) {
activeLastWeek++;
}
}

// Regroupement dynamique : agrège tous les role_type connus dans des
// catégories plus larges, n'affiche que celles ayant au moins 1 profil
// et trie par population décroissante.
Expand All @@ -143,9 +199,6 @@ function computeStats(profiles: RawProfile[]): ComputedStats {
.filter((r) => r.pct > 0)
.sort((a, b) => b.pct - a.pct);

const expCounts: Record<string, number> = {};
for (const p of profiles)
expCounts[p.experience_level] = (expCounts[p.experience_level] || 0) + 1;
const experience = [
{
label: "Junior",
Expand All @@ -164,10 +217,6 @@ function computeStats(profiles: RawProfile[]): ComputedStats {
},
];

const techCounts: Record<string, number> = {};
for (const p of profiles)
for (const t of p.tech_stack ?? [])
techCounts[t] = (techCounts[t] || 0) + 1;
const techs = Object.entries(techCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
Expand All @@ -176,9 +225,6 @@ function computeStats(profiles: RawProfile[]): ComputedStats {
pct: Math.round((count / total) * 100),
}));

const cityCounts: Record<string, number> = {};
for (const p of profiles)
if (p.city) cityCounts[p.city] = (cityCounts[p.city] || 0) + 1;
const sortedCities = Object.entries(cityCounts).sort((a, b) => b[1] - a[1]);
const cities = sortedCities
.slice(0, 3)
Expand All @@ -187,36 +233,15 @@ function computeStats(profiles: RawProfile[]): ComputedStats {
if (otherCount > 0)
cities.push({ name: "Autres", pct: Math.round((otherCount / total) * 100) });

const now = new Date();
const months = Array.from({ length: 6 }, (_, i) => {
const d = new Date(now.getFullYear(), now.getMonth() - 5 + i, 1);
return {
month: d.toLocaleDateString("fr-FR", { month: "short" }),
year: d.getFullYear(),
monthNum: d.getMonth(),
count: 0,
};
});
for (const p of profiles) {
const d = new Date(p.created_at);
const entry = months.find(
(m) => m.year === d.getFullYear() && m.monthNum === d.getMonth()
);
if (entry) entry.count++;
}
const maxCount = Math.max(...months.map((m) => m.count), 1);
const growth = months.map((m) => ({
month: m.month,
value: Math.round((m.count / maxCount) * 100),
}));

const collab = profiles.filter((p) => p.open_to_collaboration).length;
const totalCities = new Set(profiles.map((p) => p.city).filter(Boolean)).size;
const allTechs = new Set(profiles.flatMap((p) => p.tech_stack ?? [])).size;
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const activeLastWeek = profiles.filter(
(p) => p.last_seen_at && new Date(p.last_seen_at) >= weekAgo
).length;
// ⚡ Bolt: Derive distinct counts from keys instead of doing additional passes
const totalCities = Object.keys(cityCounts).length;
const allTechs = Object.keys(techCounts).length;

return {
total,
Expand Down
Loading