From b96f7eb9c6adf0b06dae17545fdbf1c7a49c3421 Mon Sep 17 00:00:00 2001
From: Solo Shun <88045720+soloshun@users.noreply.github.com>
Date: Sun, 25 Jan 2026 18:04:10 +0000
Subject: [PATCH] feat(web): add particles background, mobile responsive
array-search animation, implementation checklist roadmap and git contributors
ui
---
apps/web/src/app/page.tsx | 2 +
.../components/animations/array-search.tsx | 42 ++-
.../sections/algorithm-checklist.tsx | 272 +++++++++++++++
apps/web/src/components/sections/footer.tsx | 5 +-
apps/web/src/components/sections/header.tsx | 15 +-
.../src/components/sections/open-source.tsx | 322 ++++++++++++++++--
.../components/ui/particles-background.tsx | 4 +-
apps/web/src/lib/algorithms-data.ts | 205 +++++++++++
8 files changed, 818 insertions(+), 49 deletions(-)
create mode 100644 apps/web/src/components/sections/algorithm-checklist.tsx
create mode 100644 apps/web/src/lib/algorithms-data.ts
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index a676623..f2fac72 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -4,6 +4,7 @@ import { Features } from "@/components/sections/features";
import { Architecture } from "@/components/sections/architecture";
import { TechStack } from "@/components/sections/tech-stack";
import { Roadmap } from "@/components/sections/roadmap";
+import { AlgorithmChecklist } from "@/components/sections/algorithm-checklist";
import { FAQ } from "@/components/sections/faq";
import { OpenSource } from "@/components/sections/open-source";
import { Footer } from "@/components/sections/footer";
@@ -24,6 +25,7 @@ export default function Home() {
+
diff --git a/apps/web/src/components/animations/array-search.tsx b/apps/web/src/components/animations/array-search.tsx
index 0285ed0..689a89b 100644
--- a/apps/web/src/components/animations/array-search.tsx
+++ b/apps/web/src/components/animations/array-search.tsx
@@ -3,17 +3,40 @@
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
-const array = [2, 5, 8, 11, 12, 16, 23, 28, 38, 56, 60, 67, 72, 81, 91, 99, 123, 212];
+const DESKTOP_ARRAY = [2, 5, 8, 11, 12, 16, 23, 28, 38, 56, 60, 67, 72, 81, 91, 99, 123, 212];
+const MOBILE_ARRAY = [67, 69, 70, 77, 81, 98];
export function ArraySearch() {
+ const [array, setArray] = useState(DESKTOP_ARRAY);
const target = 67;
const [currentIndex, setCurrentIndex] = useState(-1);
const [left, setLeft] = useState(0);
- const [right, setRight] = useState(array.length - 1);
+ const [right, setRight] = useState(DESKTOP_ARRAY.length - 1);
const [found, setFound] = useState(false);
+ // Handle responsive array
useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth < 768) {
+ setArray(MOBILE_ARRAY);
+ } else {
+ setArray(DESKTOP_ARRAY);
+ }
+ };
+
+ // Initial check
+ handleResize();
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ useEffect(() => {
+ let isCancelled = false;
+
const animateBinarySearch = async () => {
+ if (isCancelled) return;
+
setCurrentIndex(-1);
setLeft(0);
setRight(array.length - 1);
@@ -23,6 +46,8 @@ export function ArraySearch() {
let r = array.length - 1;
while (l <= r) {
+ if (isCancelled) return;
+
const mid = Math.floor((l + r) / 2);
setCurrentIndex(mid);
setLeft(l);
@@ -39,14 +64,19 @@ export function ArraySearch() {
}
}
- await new Promise((resolve) => setTimeout(resolve, 2500));
+ if (!isCancelled) {
+ await new Promise((resolve) => setTimeout(resolve, 2500));
+ }
};
const interval = setInterval(animateBinarySearch, 8000);
animateBinarySearch();
- return () => clearInterval(interval);
- }, []);
+ return () => {
+ isCancelled = true;
+ clearInterval(interval);
+ };
+ }, [array]); // Restart animation when array changes
return (
@@ -58,7 +88,7 @@ export function ArraySearch() {
return (
t.implemented).length;
+
+ return (
+
+
setIsOpen(!isOpen)}
+ className="w-full text-left text-[hsl(var(--foreground))]/80 text-xs sm:text-sm font-medium flex items-center gap-2 hover:text-[hsl(var(--foreground))] transition-colors py-1"
+ >
+ {isOpen ? (
+
+ ) : (
+
+ )}
+ {name}
+
+ {implementedCount}/{topics.length}
+
+
+
+
+ {isOpen && (
+
+ {topics.map((item, index) => (
+
+
+
+ {index === topics.length - 1 ? "└─" : "├─"}
+
+ {item.name}
+
+
+ {item.implemented ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+interface CategorySectionProps {
+ category: Category;
+ visible: boolean;
+ setVisible: (visible: boolean) => void;
+}
+
+function CategorySection({ category, visible, setVisible }: CategorySectionProps) {
+ const { total, implemented } = getCategoryCount(category);
+
+ return (
+
+
setVisible(!visible)}
+ >
+ {visible ? (
+
+ ) : (
+
+ )}
+ {category.title}
+
+ {implemented}/{total}
+
+
+
+
+ {visible && (
+
+ {Object.entries(category.items).map(([topic, { topics }]) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export function AlgorithmChecklist() {
+ const [dsVisible, setDsVisible] = useState(true);
+ const [algoVisible, setAlgoVisible] = useState(true);
+ const { total, implemented } = getTotalCount();
+ const progress = Math.round((implemented / total) * 100);
+
+ return (
+
+ {/* Background */}
+
+
+
+
+ {/* Noise overlay */}
+
+
+
+ {/* Section header */}
+
+
+
+ / Implementation Roadmap
+
+
+
+ THE AMBITIOUS LIST
+
+
+
+ {total}+ algorithms and data structures. This is what we're building together.
+ Pick one and contribute, or suggest new ones. ML/DL, algorithms from other fields, and visualizers are all welcome.
+
+
+ {/* Progress bar */}
+
+
+ Progress
+ {implemented}/{total} ({progress}%)
+
+
+
+
+
+
+
+ {/* Terminal-style container */}
+
+ {/* Terminal header */}
+
+
+
+ opendsa@roadmap ~ $
+
+
+
+ {/* Terminal content */}
+
+ {/* Header banner */}
+
+
+
+ OpenDSA Implementation Roadmap
+
+
+
+ Click on any category to expand/collapse
+
+
+
+ {/* Two columns */}
+
+
+ {/* Legend */}
+
+ Status:
+
+ Implemented
+
+
+ Pending
+
+
+
+ {/* Cursor */}
+
+ $
+
+
+
+
+
+ {/* Future plans */}
+
+
+ Open to Contributions: {" "}
+ Machine Learning & Deep Learning Algorithm Visualizers etc...
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/sections/footer.tsx b/apps/web/src/components/sections/footer.tsx
index 14cf254..9919bad 100644
--- a/apps/web/src/components/sections/footer.tsx
+++ b/apps/web/src/components/sections/footer.tsx
@@ -20,6 +20,7 @@ const footerLinks = {
{ label: "GitHub", href: "https://github.com/soloshun/opendsa" },
{ label: "Discord", href: "#" },
{ label: "Twitter", href: "#" },
+ { label: "Support Us ☕", href: "https://docs.opendsa.dev/sponsors" },
],
};
@@ -46,10 +47,10 @@ export function Footer() {
{/* Gradient from bottom */}
-
+
{/* Background */}
-
+
{/* Noise overlay */}
diff --git a/apps/web/src/components/sections/header.tsx b/apps/web/src/components/sections/header.tsx
index 59b70c7..e453214 100644
--- a/apps/web/src/components/sections/header.tsx
+++ b/apps/web/src/components/sections/header.tsx
@@ -1,14 +1,19 @@
"use client";
import { motion } from "framer-motion";
-import { Github, Menu, X, Sun } from "lucide-react";
+import {
+ Github,
+ Menu,
+ X,
+ // Sun
+} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
const navLinks = [
{ href: "#features", label: "Features" },
{ href: "#roadmap", label: "Roadmap" },
- { href: "#open-source", label: "Open Source" },
+ // { href: "#open-source", label: "Open Source" },
{ href: "https://docs.opendsa.dev", label: "Docs", external: true },
];
@@ -20,7 +25,7 @@ export function Header() {
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, ease: "easeOut" }}
- className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-5xl"
+ className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[95%] max-w-6xl"
>
{/* Main nav container - rounded pill style */}
@@ -54,9 +59,9 @@ export function Header() {
{/* Right side buttons */}
{/* Theme toggle placeholder */}
-
+ {/*
-
+ */}
{/* GitHub star button */}
= 100) {
+ return {
+ tier: "legendary",
+ color: "from-amber-400 via-yellow-500 to-amber-600",
+ border: "border-amber-400/60",
+ glow: "shadow-amber-500/30",
+ badge: "bg-gradient-to-r from-amber-500 to-yellow-500",
+ textColor: "text-amber-400",
+ emoji: "👑",
+ };
+ }
+ if (contributions >= 50) {
+ return {
+ tier: "gold",
+ color: "from-yellow-400 to-amber-500",
+ border: "border-yellow-400/50",
+ glow: "shadow-yellow-500/20",
+ badge: "bg-gradient-to-r from-yellow-500 to-amber-500",
+ textColor: "text-yellow-400",
+ emoji: "⭐",
+ };
+ }
+ if (contributions >= 20) {
+ return {
+ tier: "silver",
+ color: "from-slate-300 to-slate-400",
+ border: "border-slate-400/40",
+ glow: "shadow-slate-400/15",
+ badge: "bg-gradient-to-r from-slate-400 to-slate-500",
+ textColor: "text-slate-300",
+ emoji: "",
+ };
+ }
+ if (contributions >= 10) {
+ return {
+ tier: "bronze",
+ color: "from-orange-400 to-orange-600",
+ border: "border-orange-400/30",
+ glow: "shadow-orange-500/10",
+ badge: "bg-gradient-to-r from-orange-500 to-orange-600",
+ textColor: "text-orange-400",
+ emoji: "",
+ };
+ }
+ return {
+ tier: "contributor",
+ color: "from-[hsl(var(--primary))] to-emerald-600",
+ border: "border-[hsl(var(--border))]",
+ glow: "",
+ badge: "bg-[hsl(var(--primary))]",
+ textColor: "text-[hsl(var(--primary))]",
+ emoji: "",
+ };
+}
+
+function formatContributions(count: number): string {
+ if (count >= 100) return "99+";
+ return count.toString();
+}
+
+interface ContributorAvatarProps {
+ contributor: Contributor;
+ index: number;
+}
+
+function ContributorAvatar({ contributor, index }: ContributorAvatarProps) {
+ const [isLoaded, setIsLoaded] = useState(false);
+ const [isInView, setIsInView] = useState(false);
+ const ref = useRef
(null);
+ const tier = getContributorTier(contributor.contributions);
+
+ // Intersection Observer for lazy loading
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setIsInView(true);
+ observer.disconnect();
+ }
+ },
+ { rootMargin: "100px" }
+ );
+
+ if (ref.current) {
+ observer.observe(ref.current);
+ }
+
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+
+ {/* Glow effect for top contributors */}
+ {tier.tier === "legendary" && (
+
+ )}
+ {tier.tier === "gold" && (
+
+ )}
+
+ {/* Avatar container */}
+
+ {isInView ? (
+
setIsLoaded(true)}
+ sizes="56px"
+ />
+ ) : null}
+
+ {/* Loading skeleton */}
+ {(!isInView || !isLoaded) && (
+
+ )}
+
+
+ {/* Crown for legendary contributors */}
+ {tier.emoji && (
+
+ {tier.emoji}
+
+ )}
+
+ {/* Contribution badge */}
+
+ {formatContributions(contributor.contributions)}
+
+
+ {/* Hover tooltip */}
+
+
+ {/* Username */}
+
+ @{contributor.login}
+
+ {/* Contribution count */}
+
+
+ {contributor.contributions} commits
+ {tier.emoji && {tier.emoji} }
+
+ {/* Tier badge */}
+
+ {tier.tier}
+
+ {/* Arrow */}
+
+
+
+
+
+ );
+}
+
export function OpenSource() {
const [stats, setStats] = useState({ stars: 0, forks: 0, contributors: 1 });
+ const [contributors, setContributors] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
- async function fetchGitHubStats() {
+ async function fetchGitHubData() {
try {
// Fetch repo data
const repoRes = await fetch("https://api.github.com/repos/soloshun/opendsa");
@@ -29,28 +215,21 @@ export function OpenSource() {
}));
}
- // Fetch contributors count
- const contribRes = await fetch("https://api.github.com/repos/soloshun/opendsa/contributors?per_page=1");
+ // Fetch contributors
+ const contribRes = await fetch("https://api.github.com/repos/soloshun/opendsa/contributors?per_page=50");
if (contribRes.ok) {
- const linkHeader = contribRes.headers.get("Link");
- if (linkHeader) {
- const match = linkHeader.match(/page=(\d+)>; rel="last"/);
- if (match) {
- setStats(prev => ({ ...prev, contributors: parseInt(match[1]) }));
- }
- } else {
- const data = await contribRes.json();
- setStats(prev => ({ ...prev, contributors: data.length || 1 }));
- }
+ const contribData: Contributor[] = await contribRes.json();
+ setContributors(contribData);
+ setStats(prev => ({ ...prev, contributors: contribData.length }));
}
} catch (error) {
- console.error("Failed to fetch GitHub stats:", error);
+ console.error("Failed to fetch GitHub data:", error);
} finally {
setLoading(false);
}
}
- fetchGitHubStats();
+ fetchGitHubData();
}, []);
return (
@@ -78,18 +257,12 @@ export function OpenSource() {
transition={{ duration: 0.5 }}
className="text-center"
>
- {/* Icon */}
-
-
-
+ {/* Heading */}
+
+
+ / Open Source
+
- {/* Heading - creative layout */}
BUILT BY THE COMMUNITY
@@ -101,13 +274,96 @@ export function OpenSource() {
the best algorithm visualization platform.
+ {/* Contributors showcase */}
+
+ {/* Contributors header */}
+
+
+
+
+ Contributors
+ ({stats.contributors})
+
+
+
+
+ {/* Contributors grid */}
+
+ {/* Terminal-like header */}
+
+
+
+ git log --format="%an" | sort -u
+
+
+
+ {/* Contributors avatars */}
+
+ {loading ? (
+
+ {[...Array(8)].map((_, i) => (
+
+ ))}
+
+ ) : contributors.length > 0 ? (
+
+ {contributors.map((contributor, index) => (
+
+ ))}
+
+ ) : (
+
+ Be the first contributor!
+
+ )}
+
+
+ {/* Legend */}
+
+
+
+ 👑 100+ commits
+
+
+
+ ⭐ 50+ commits
+
+
+
+ 20+ commits
+
+
+
+ Contributor
+
+
+
+
+
{/* Stats */}
-
+
{[
{ icon:
, label: "Stars", value: loading ? "..." : stats.stars.toString() },
{ icon:
, label: "Forks", value: loading ? "..." : stats.forks.toString() },
{ icon:
, label: "Contributors", value: loading ? "..." : stats.contributors.toString() },
- { icon:
, label: "Sponsors", value: "0" },
+ { icon:
, label: "MIT License", value: "Free" },
].map((stat, index) => (
@@ -145,9 +401,9 @@ export function OpenSource() {
href="https://github.com/soloshun/opendsa/blob/main/CONTRIBUTING.md"
target="_blank"
rel="noopener noreferrer"
- className="w-full sm:w-auto flex items-center justify-center gap-2 rounded-full border border-[hsl(var(--border))] bg-transparent px-6 sm:px-8 py-3.5 sm:py-4 text-sm sm:text-base font-medium text-[hsl(var(--foreground))] transition-all hover:bg-[hsl(var(--secondary))] hover:border-[hsl(var(--primary))]/50"
+ className="w-full sm:w-auto flex items-center justify-center gap-2 rounded-full border border-[hsl(var(--border))] bg-transparent px-6 sm:px-8 py-3.5 sm:py-4 text-sm sm:text-base font-medium text-[hsl(var(--foreground))] transition-all hover:bg-[hsl(var(--secondary))]/80 hover:border-[hsl(var(--primary))]/50"
>
- Contribute
+ Become a Contributor
diff --git a/apps/web/src/components/ui/particles-background.tsx b/apps/web/src/components/ui/particles-background.tsx
index 63dc8df..a78d770 100644
--- a/apps/web/src/components/ui/particles-background.tsx
+++ b/apps/web/src/components/ui/particles-background.tsx
@@ -16,9 +16,7 @@ export function ParticlesBackground() {
});
}, []);
- const particlesLoaded = useCallback(async (container: Container | undefined) => {
- console.log("Particles loaded", container);
- }, []);
+ const particlesLoaded = useCallback(async () => { }, []);
const options: ISourceOptions = useMemo(
() => ({
diff --git a/apps/web/src/lib/algorithms-data.ts b/apps/web/src/lib/algorithms-data.ts
new file mode 100644
index 0000000..af0b8d8
--- /dev/null
+++ b/apps/web/src/lib/algorithms-data.ts
@@ -0,0 +1,205 @@
+// Comprehensive list of algorithms and data structures for OpenDSA
+// This is an ambitious roadmap - contributions welcome!
+
+export interface AlgorithmTopic {
+ name: string;
+ implemented: boolean;
+ description?: string;
+}
+
+export interface CategoryItem {
+ topics: AlgorithmTopic[];
+}
+
+export interface Category {
+ title: string;
+ items: Record;
+}
+
+export interface AlgorithmCategories {
+ dataStructures: Category;
+ algorithms: Category;
+}
+
+export const categories: AlgorithmCategories = {
+ dataStructures: {
+ title: "Data Structures",
+ items: {
+ Arrays: {
+ topics: [
+ { name: "Basic Array Operations", implemented: true },
+ { name: "Dynamic Arrays", implemented: false },
+ { name: "Multi-dimensional Arrays", implemented: false },
+ ],
+ },
+ "Linked Lists": {
+ topics: [
+ { name: "Singly Linked List", implemented: false },
+ { name: "Doubly Linked List", implemented: false },
+ { name: "Circular Linked List", implemented: false },
+ ],
+ },
+ Stacks: {
+ topics: [
+ { name: "Basic Stack Operations", implemented: false },
+ { name: "Applications (Balancing Parentheses)", implemented: false },
+ ],
+ },
+ Queues: {
+ topics: [
+ { name: "Basic Queue", implemented: false },
+ { name: "Circular Queue", implemented: false },
+ { name: "Priority Queue", implemented: false },
+ { name: "Deque (Double-Ended Queue)", implemented: false },
+ ],
+ },
+ Trees: {
+ topics: [
+ { name: "Binary Tree", implemented: false },
+ { name: "Binary Search Tree (BST)", implemented: false },
+ { name: "AVL Tree (Self-Balancing BST)", implemented: false },
+ { name: "Red-Black Tree", implemented: false },
+ { name: "Segment Tree", implemented: false },
+ { name: "Trie", implemented: false },
+ { name: "B-Tree and B+ Tree", implemented: false },
+ ],
+ },
+ Heaps: {
+ topics: [
+ { name: "Min Heap", implemented: false },
+ { name: "Max Heap", implemented: false },
+ { name: "Fibonacci Heap", implemented: false },
+ ],
+ },
+ Graphs: {
+ topics: [
+ { name: "Directed/Undirected Graphs", implemented: false },
+ { name: "Weighted/Unweighted Graphs", implemented: false },
+ { name: "Adjacency Matrix", implemented: false },
+ { name: "Adjacency List", implemented: false },
+ { name: "Edge List", implemented: false },
+ ],
+ },
+ Hashing: {
+ topics: [
+ { name: "Hash Tables", implemented: false },
+ { name: "Hash Maps", implemented: false },
+ { name: "Open Addressing", implemented: false },
+ { name: "Separate Chaining", implemented: false },
+ ],
+ },
+ },
+ },
+ algorithms: {
+ title: "Algorithms",
+ items: {
+ "Sorting (Easy)": {
+ topics: [
+ { name: "Bubble Sort", implemented: true },
+ { name: "Selection Sort", implemented: false },
+ { name: "Insertion Sort", implemented: false },
+ ],
+ },
+ "Sorting (Intermediate)": {
+ topics: [
+ { name: "Merge Sort", implemented: false },
+ { name: "Quick Sort", implemented: false },
+ ],
+ },
+ "Sorting (Advanced)": {
+ topics: [
+ { name: "Heap Sort", implemented: false },
+ { name: "Counting Sort", implemented: false },
+ { name: "Radix Sort", implemented: false },
+ { name: "Bucket Sort", implemented: false },
+ ],
+ },
+ Searching: {
+ topics: [
+ { name: "Linear Search", implemented: true },
+ { name: "Binary Search", implemented: false },
+ { name: "Jump Search", implemented: false },
+ { name: "Exponential Search", implemented: false },
+ ],
+ },
+ "Graph Algorithms": {
+ topics: [
+ { name: "Breadth-First Search (BFS)", implemented: false },
+ { name: "Depth-First Search (DFS)", implemented: false },
+ { name: "Dijkstra's Algorithm", implemented: false },
+ { name: "Floyd-Warshall", implemented: false },
+ { name: "Bellman-Ford", implemented: false },
+ { name: "Prim's MST", implemented: false },
+ { name: "Kruskal's MST", implemented: false },
+ { name: "A* Search", implemented: false },
+ ],
+ },
+ "Dynamic Programming": {
+ topics: [
+ { name: "Fibonacci", implemented: false },
+ { name: "Coin Change (Minimum Coins)", implemented: false },
+ { name: "Longest Common Subsequence (LCS)", implemented: false },
+ { name: "Knapsack Problem", implemented: false },
+ { name: "Matrix Chain Multiplication", implemented: false },
+ ],
+ },
+ Backtracking: {
+ topics: [
+ { name: "Rat in a Maze", implemented: false },
+ { name: "N-Queens Problem", implemented: false },
+ { name: "Knight's Tour", implemented: false },
+ { name: "Sudoku Solver", implemented: false },
+ ],
+ },
+ "Greedy Algorithms": {
+ topics: [
+ { name: "Activity Selection", implemented: false },
+ { name: "Fractional Knapsack", implemented: false },
+ { name: "Huffman Coding", implemented: false },
+ { name: "Job Sequencing", implemented: false },
+ ],
+ },
+ "Miscellaneous Algorithms": {
+ topics: [
+ { name: "Sieve of Eratosthenes (Prime Numbers)", implemented: false },
+ { name: "Euclidean Algorithm (GCD)", implemented: false },
+ { name: "KMP Pattern Matching", implemented: false },
+ { name: "Rabin-Karp Algorithm", implemented: false },
+ { name: "Bit Manipulation", implemented: false },
+ { name: "Randomized Algorithms (QuickSelect)", implemented: false },
+ ],
+ },
+ },
+ },
+};
+
+// Helper functions
+export function getTotalCount(): { total: number; implemented: number } {
+ let total = 0;
+ let implemented = 0;
+
+ (Object.values(categories) as Category[]).forEach((category) => {
+ Object.values(category.items).forEach((item: CategoryItem) => {
+ item.topics.forEach((topic) => {
+ total++;
+ if (topic.implemented) implemented++;
+ });
+ });
+ });
+
+ return { total, implemented };
+}
+
+export function getCategoryCount(category: Category): { total: number; implemented: number } {
+ let total = 0;
+ let implemented = 0;
+
+ Object.values(category.items).forEach((item) => {
+ item.topics.forEach((topic) => {
+ total++;
+ if (topic.implemented) implemented++;
+ });
+ });
+
+ return { total, implemented };
+}