From 6b5f054fef3bdc7a31315b4bce0cea0d3128e7a4 Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Sun, 1 Mar 2026 16:51:12 +0530 Subject: [PATCH 1/7] feat: implement native IndexedDB cache-first data layer --- package-lock.json | 4 +-- src/App.tsx | 3 +- src/components/TestServices.tsx | 32 +++++++++++++++++++ src/services/cacheService.ts | 56 +++++++++++++++++++++++++++++++++ src/services/githubService.ts | 55 ++++++++++++++++++++++++++++++++ src/services/idbService.ts | 36 +++++++++++++++++++++ src/services/index.ts | 15 +++++++++ src/services/tokenService.ts | 35 +++++++++++++++++++++ 8 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 src/components/TestServices.tsx create mode 100644 src/services/cacheService.ts create mode 100644 src/services/githubService.ts create mode 100644 src/services/idbService.ts create mode 100644 src/services/index.ts create mode 100644 src/services/tokenService.ts diff --git a/package-lock.json b/package-lock.json index ab0e168..0bdb8b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "orgexplorer", + "name": "OrgExplorer", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "orgexplorer", + "name": "OrgExplorer", "version": "0.0.0", "dependencies": { "react": "^19.2.0", diff --git a/src/App.tsx b/src/App.tsx index 0a3deb1..bd9aa14 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,11 @@ import './App.css' - +import TestServices from "./components/TestServices"; function App() { return ( <>

Hello, OrgExplorer!

+ ) } diff --git a/src/components/TestServices.tsx b/src/components/TestServices.tsx new file mode 100644 index 0000000..d78346d --- /dev/null +++ b/src/components/TestServices.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { tokenService, githubService } from "../services"; + +export default function TestServices() { + + const testFlow = async () => { + tokenService.setToken("GITHUB-TOKEN"); + + const token = tokenService.getToken(); + + if (!token) { + console.error("Token is not set!"); + return; + } + + const org = prompt("Enter org name:"); + + if (!org) return; + + const repos = await githubService.fetchOrgReposWithCache(org, token); + + console.log("Repos count:", repos.length); + console.log("Repo names:"); + repos.forEach(repo => console.log(repo.name)); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts new file mode 100644 index 0000000..78a9e02 --- /dev/null +++ b/src/services/cacheService.ts @@ -0,0 +1,56 @@ +import { saveToIDB, getFromIDB } from "./idbService"; + +type RepoCacheEntry = { + data: any[]; + savedAt: number; +}; + +const cacheService = { + + async saveRepos(org: string, data: any[] | RepoCacheEntry): Promise { + const entry: RepoCacheEntry = Array.isArray(data) + ? { data, savedAt: Date.now() } + : data; + + console.log(`Saving ${org} repos to IDB`); + await saveToIDB(org, entry); + }, + + async getRepos(org: string): Promise { + const entry = await getFromIDB(org); + + if (!entry) { + console.log("No cache found"); + return null; + } + + console.log("Cache found"); + + // Handle old format (raw array) + if (Array.isArray(entry)) { + console.log("Detected old cache format, migrating..."); + const migratedEntry: RepoCacheEntry = { + data: entry, + savedAt: Date.now() + }; + // Migrate to new format in background + cacheService.saveRepos(org, migratedEntry).catch(err => + console.error("Migration failed:", err) + ); + return entry; + } + + // Handle new format (structured object) + if (entry && typeof entry === 'object' && 'data' in entry) { + return entry.data; + } + + // Unrecognized format + console.warn(`Cache for ${org} is in an unrecognized format. Clearing it.`); + // Optionally clear cache here if it's corrupted, but for now just return null + return null; + } + +}; + +export default cacheService; \ No newline at end of file diff --git a/src/services/githubService.ts b/src/services/githubService.ts new file mode 100644 index 0000000..7856913 --- /dev/null +++ b/src/services/githubService.ts @@ -0,0 +1,55 @@ +/** + * Service for interacting with the GitHub API. + * + * TODO: Mentors are discussing future service improvements. + */ +import cacheService from "./cacheService"; + +const GITHUB_API_URL = 'https://api.github.com'; + +const githubService = { + + async fetchOrgRepos(org: string, token: string): Promise { + try { + console.log("Fetching from:", `${GITHUB_API_URL}/orgs/${org}/repos`); + + const response = await fetch(`${GITHUB_API_URL}/orgs/${org}/repos`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/vnd.github+json', + } + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + + return await response.json(); + + } catch (error) { + console.error(`Error fetching repositories for organization ${org}:`, error); + throw error; + } + }, + + async fetchOrgReposWithCache(org: string, token: string): Promise { + // Step 1: Check cache + const cachedRepos = await cacheService.getRepos(org); + + if (cachedRepos) { + console.log("Using cached repos"); + return cachedRepos; + } + + // Step 2: Fetch from GitHub + const repos = await this.fetchOrgRepos(org, token); + + // Step 3: Save to cache + await cacheService.saveRepos(org, repos); + + return repos; + } + +} +export default githubService; \ No newline at end of file diff --git a/src/services/idbService.ts b/src/services/idbService.ts new file mode 100644 index 0000000..26820bb --- /dev/null +++ b/src/services/idbService.ts @@ -0,0 +1,36 @@ +const DB_NAME = "OrgExplorerDB"; +const STORE_NAME = "repos"; + +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1); + + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +export async function saveToIDB(key: string, value: any) { + const db = await openDB(); + const tx = db.transaction(STORE_NAME, "readwrite"); + const store = tx.objectStore(STORE_NAME); + store.put(value, key); +} + +export async function getFromIDB(key: string): Promise { + const db = await openDB(); + const tx = db.transaction(STORE_NAME, "readonly"); + const store = tx.objectStore(STORE_NAME); + + return new Promise((resolve) => { + const req = store.get(key); + req.onsuccess = () => resolve(req.result || null); + }); +} \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..63921cf --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,15 @@ +import tokenService from "./tokenService"; +import githubService from "./githubService"; +import cacheService from "./cacheService"; + +const { fetchOrgRepos } = githubService; +const { saveRepos, getRepos } = cacheService; + +export { + tokenService, + githubService, + cacheService, + fetchOrgRepos, + saveRepos, + getRepos +}; diff --git a/src/services/tokenService.ts b/src/services/tokenService.ts new file mode 100644 index 0000000..298524f --- /dev/null +++ b/src/services/tokenService.ts @@ -0,0 +1,35 @@ +/** + * Service for managing the GitHub Personal Access Token (PAT). + * Currently stores the token in memory for security and as a temporary measure. + * + * TODO: Mentors are discussing future persistence options (IndexedDB, encrypted storage). + */ + +let _token: string | null = null; + +const tokenService = { + /** + * Sets the GitHub PAT in memory. + * @param token + */ + setToken(token: string): void { + _token = token; + }, + + /** + * Gets the GitHub PAT from memory. + * @returns + */ + getToken(): string | null { + return _token; + }, + + /** + * Removes the GitHub PAT from memory. + */ + removeToken(): void { + _token = null; + } +}; + +export default tokenService; From 92625346256ca2f18aa8f655846e203278ea6aeb Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Tue, 3 Mar 2026 12:39:27 +0530 Subject: [PATCH 2/7] feat: add TTL-based IndexedDB cache and secure in-memory token handling for GitHub API integration --- src/components/TestServices.tsx | 65 ++++++++++++++++------- src/services/cacheService.ts | 94 +++++++++++++++++++++++---------- src/services/githubService.ts | 31 +++++------ 3 files changed, 127 insertions(+), 63 deletions(-) diff --git a/src/components/TestServices.tsx b/src/components/TestServices.tsx index d78346d..ebc6284 100644 --- a/src/components/TestServices.tsx +++ b/src/components/TestServices.tsx @@ -1,32 +1,59 @@ -import React from "react"; -import { tokenService, githubService } from "../services"; +import React, { useState } from "react" +import { tokenService, githubService } from "../services" export default function TestServices() { + const [tokenInput, setTokenInput] = useState("") + const [orgInput, setOrgInput] = useState("") const testFlow = async () => { - tokenService.setToken("GITHUB-TOKEN"); - - const token = tokenService.getToken(); + if (!tokenInput) { + alert("Please enter GitHub token") + return + } - if (!token) { - console.error("Token is not set!"); - return; + if (!orgInput) { + alert("Please enter organization name") + return } - const org = prompt("Enter org name:"); + tokenService.setToken(tokenInput) - if (!org) return; + try { + const repos = await githubService.fetchOrgReposWithCache( + orgInput, + tokenInput + ) - const repos = await githubService.fetchOrgReposWithCache(org, token); + console.log("Repos count:", repos.length) + console.log("Repo names:") + repos.forEach(repo => console.log(repo.name)) - console.log("Repos count:", repos.length); - console.log("Repo names:"); - repos.forEach(repo => console.log(repo.name)); - }; + } catch (error) { + console.error("Error:", error) + alert("Failed to fetch repositories") + } + } return ( - - ); +
+ setTokenInput(e.target.value)} + /> + + setOrgInput(e.target.value)} + style={{ marginLeft: "10px" }} + /> + + +
+ ) } \ No newline at end of file diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index 78a9e02..3fda755 100644 --- a/src/services/cacheService.ts +++ b/src/services/cacheService.ts @@ -1,56 +1,92 @@ -import { saveToIDB, getFromIDB } from "./idbService"; +// export default cacheService; +import { saveToIDB, getFromIDB } from "./idbService" + +/** + * Minimal GitHub Repo Type + * (Extend later if needed) + */ +export interface GitHubRepo { + id: number + name: string + stargazers_count: number + forks_count: number + language: string | null + updated_at: string +} + +/** + * Structured cache entry format + */ type RepoCacheEntry = { - data: any[]; - savedAt: number; -}; + data: GitHubRepo[] + savedAt: number +} + +/** + * Cache expiry time (10 minutes) + */ +const MAX_CACHE_AGE = 1000 * 60 * 10 const cacheService = { - async saveRepos(org: string, data: any[] | RepoCacheEntry): Promise { + async saveRepos(org: string, data: GitHubRepo[] | RepoCacheEntry): Promise { const entry: RepoCacheEntry = Array.isArray(data) ? { data, savedAt: Date.now() } - : data; + : data - console.log(`Saving ${org} repos to IDB`); - await saveToIDB(org, entry); + console.log(`Saving ${org} repos to IDB`) + await saveToIDB(org, entry) }, - async getRepos(org: string): Promise { - const entry = await getFromIDB(org); + async getRepos(org: string): Promise { + const entry = await getFromIDB(org) if (!entry) { - console.log("No cache found"); - return null; + console.log("No cache found") + return null } - console.log("Cache found"); + console.log("Cache found") - // Handle old format (raw array) + // Handle old format (raw array) if (Array.isArray(entry)) { - console.log("Detected old cache format, migrating..."); + console.log("Detected old cache format, migrating...") + const migratedEntry: RepoCacheEntry = { data: entry, savedAt: Date.now() - }; - // Migrate to new format in background - cacheService.saveRepos(org, migratedEntry).catch(err => + } + + cacheService.saveRepos(org, migratedEntry).catch(err => console.error("Migration failed:", err) - ); - return entry; + ) + + return entry } - // Handle new format (structured object) - if (entry && typeof entry === 'object' && 'data' in entry) { - return entry.data; + // Handle structured format + if ( + typeof entry === "object" && + entry !== null && + "data" in entry && + "savedAt" in entry + ) { + const typedEntry = entry as RepoCacheEntry + + // TTL CHECK + if (Date.now() - typedEntry.savedAt > MAX_CACHE_AGE) { + console.log("Cache expired") + return null + } + + return typedEntry.data } - // Unrecognized format - console.warn(`Cache for ${org} is in an unrecognized format. Clearing it.`); - // Optionally clear cache here if it's corrupted, but for now just return null - return null; + console.warn(`Cache for ${org} is in an unrecognized format.`) + return null } -}; +} -export default cacheService; \ No newline at end of file +export default cacheService \ No newline at end of file diff --git a/src/services/githubService.ts b/src/services/githubService.ts index 7856913..4ea82a6 100644 --- a/src/services/githubService.ts +++ b/src/services/githubService.ts @@ -1,23 +1,19 @@ -/** - * Service for interacting with the GitHub API. - * - * TODO: Mentors are discussing future service improvements. - */ import cacheService from "./cacheService"; - +import type { GitHubRepo } from "./cacheService" const GITHUB_API_URL = 'https://api.github.com'; const githubService = { - async fetchOrgRepos(org: string, token: string): Promise { + async fetchOrgRepos(org: string, token: string): Promise { try { - console.log("Fetching from:", `${GITHUB_API_URL}/orgs/${org}/repos`); + const url = `${GITHUB_API_URL}/orgs/${org}/repos`; + console.log("Fetching from:", url); - const response = await fetch(`${GITHUB_API_URL}/orgs/${org}/repos`, { + const response = await fetch(url, { method: 'GET', headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', } }); @@ -34,7 +30,8 @@ const githubService = { }, async fetchOrgReposWithCache(org: string, token: string): Promise { - // Step 1: Check cache + + // Step 1: Check IDB cache const cachedRepos = await cacheService.getRepos(org); if (cachedRepos) { @@ -45,11 +42,15 @@ const githubService = { // Step 2: Fetch from GitHub const repos = await this.fetchOrgRepos(org, token); - // Step 3: Save to cache - await cacheService.saveRepos(org, repos); + // Step 3: Save structured cache + await cacheService.saveRepos(org, { + data: repos, + savedAt: Date.now() + }); return repos; } -} +}; + export default githubService; \ No newline at end of file From af4c8f5d11261969cb6cfd71b1509c4484669596 Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Tue, 3 Mar 2026 18:48:37 +0530 Subject: [PATCH 3/7] error handling for input fileds(PAT & Orgnanisation name) --- src/components/TestServices.tsx | 11 ++++----- src/services/cacheService.ts | 7 +++--- src/services/githubService.ts | 29 ++++++++++++++++++++---- src/services/idbService.ts | 40 +++++++++++++++++++++++++++++++++ src/services/tokenService.ts | 2 +- 5 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/components/TestServices.tsx b/src/components/TestServices.tsx index ebc6284..bfade7f 100644 --- a/src/components/TestServices.tsx +++ b/src/components/TestServices.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react" import { tokenService, githubService } from "../services" - +// funtion to accept PAT and organisation name export default function TestServices() { const [tokenInput, setTokenInput] = useState("") const [orgInput, setOrgInput] = useState("") @@ -17,7 +17,7 @@ export default function TestServices() { } tokenService.setToken(tokenInput) - +// funtion which fetches org repos is called try { const repos = await githubService.fetchOrgReposWithCache( orgInput, @@ -30,10 +30,11 @@ export default function TestServices() { } catch (error) { console.error("Error:", error) - alert("Failed to fetch repositories") + alert("Failed to fetch repositories"); + } } - +// input fileds to enter PAT and org name return (
) -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index 3fda755..d35f15f 100644 --- a/src/services/cacheService.ts +++ b/src/services/cacheService.ts @@ -4,7 +4,6 @@ import { saveToIDB, getFromIDB } from "./idbService" /** * Minimal GitHub Repo Type - * (Extend later if needed) */ export interface GitHubRepo { id: number @@ -29,7 +28,7 @@ type RepoCacheEntry = { const MAX_CACHE_AGE = 1000 * 60 * 10 const cacheService = { - +// repos of an org are saved in cache async saveRepos(org: string, data: GitHubRepo[] | RepoCacheEntry): Promise { const entry: RepoCacheEntry = Array.isArray(data) ? { data, savedAt: Date.now() } @@ -38,7 +37,7 @@ const cacheService = { console.log(`Saving ${org} repos to IDB`) await saveToIDB(org, entry) }, - +// repos are fetched from cache if they are in cache already async getRepos(org: string): Promise { const entry = await getFromIDB(org) @@ -51,8 +50,8 @@ const cacheService = { // Handle old format (raw array) if (Array.isArray(entry)) { - console.log("Detected old cache format, migrating...") + console.log("Detected old cache format, migrating..."); const migratedEntry: RepoCacheEntry = { data: entry, savedAt: Date.now() diff --git a/src/services/githubService.ts b/src/services/githubService.ts index 4ea82a6..3927b61 100644 --- a/src/services/githubService.ts +++ b/src/services/githubService.ts @@ -3,6 +3,7 @@ import type { GitHubRepo } from "./cacheService" const GITHUB_API_URL = 'https://api.github.com'; const githubService = { + async fetchOrgRepos(org: string, token: string): Promise { try { @@ -17,19 +18,39 @@ const githubService = { } }); - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); - } + if (!response.ok) { + if (response.status === 401) { + throw new Error("Invalid or expired GitHub token. Please check your PAT."); + } + + if (response.status === 403) { + throw new Error("Rate limit exceeded. Please try again later."); + } + + if (response.status === 404) { + throw new Error("Organization not found."); + } + + throw new Error( + `GitHub API error: ${response.status} ${response.statusText}` + ); +} return await response.json(); } catch (error) { console.error(`Error fetching repositories for organization ${org}:`, error); + if (error instanceof Error) { + alert(error.message) + } else { + alert("Something went wrong") + } throw error; + } }, - async fetchOrgReposWithCache(org: string, token: string): Promise { + async fetchOrgReposWithCache(org: string, token: string): Promise { // Step 1: Check IDB cache const cachedRepos = await cacheService.getRepos(org); diff --git a/src/services/idbService.ts b/src/services/idbService.ts index 26820bb..357866d 100644 --- a/src/services/idbService.ts +++ b/src/services/idbService.ts @@ -1,36 +1,76 @@ +// Name of the IndexedDB database const DB_NAME = "OrgExplorerDB"; + +// Name of the object store (similar to a table in SQL) const STORE_NAME = "repos"; +/** + * Opens (or creates) the IndexedDB database. + * Returns a Promise that resolves with the database instance. + */ function openDB(): Promise { return new Promise((resolve, reject) => { + // Open database with version 1 const request = indexedDB.open(DB_NAME, 1); + /** + * This event runs only when: + * - Database is created for the first time + * - Version number is increased + */ request.onupgradeneeded = () => { const db = request.result; + + // Create object store if it does not already exist if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME); } }; + // If database opens successfully, resolve the Promise request.onsuccess = () => resolve(request.result); + + // If an error occurs while opening, reject the Promise request.onerror = () => reject(request.error); }); } +/** + * Saves data into IndexedDB. + * @param key Unique identifier (e.g., organization name) + * @param value Data to store (e.g., repos array with metadata) + */ export async function saveToIDB(key: string, value: any) { const db = await openDB(); + + // Create a read-write transaction const tx = db.transaction(STORE_NAME, "readwrite"); + + // Access the object store const store = tx.objectStore(STORE_NAME); + + // Insert or update value using the provided key store.put(value, key); } +/** + * Retrieves data from IndexedDB using a key. + * @param key Unique identifier (e.g., organization name) + * @returns Stored value or null if not found + */ export async function getFromIDB(key: string): Promise { const db = await openDB(); + + // Create a read-only transaction const tx = db.transaction(STORE_NAME, "readonly"); + + // Access the object store const store = tx.objectStore(STORE_NAME); return new Promise((resolve) => { const req = store.get(key); + + // Resolve with stored result or null if not found req.onsuccess = () => resolve(req.result || null); }); } \ No newline at end of file diff --git a/src/services/tokenService.ts b/src/services/tokenService.ts index 298524f..87b5bb7 100644 --- a/src/services/tokenService.ts +++ b/src/services/tokenService.ts @@ -2,7 +2,7 @@ * Service for managing the GitHub Personal Access Token (PAT). * Currently stores the token in memory for security and as a temporary measure. * - * TODO: Mentors are discussing future persistence options (IndexedDB, encrypted storage). + * */ let _token: string | null = null; From 42136eba520d76f72138de674e4d0583d0edb440 Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Tue, 3 Mar 2026 21:10:31 +0530 Subject: [PATCH 4/7] error handling for input fileds(PAT & Orgnanisation name) --- src/components/TestServices.tsx | 14 ++++++---- src/services/cacheService.ts | 4 +-- src/services/githubService.ts | 49 ++++++++++++++++++++------------- src/services/idbService.ts | 46 +++++++++++++++++-------------- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/components/TestServices.tsx b/src/components/TestServices.tsx index bfade7f..5ad99fa 100644 --- a/src/components/TestServices.tsx +++ b/src/components/TestServices.tsx @@ -29,10 +29,14 @@ export default function TestServices() { repos.forEach(repo => console.log(repo.name)) } catch (error) { - console.error("Error:", error) - alert("Failed to fetch repositories"); - - } + console.error("Error:", error) + + if (error instanceof Error) { + alert(error.message) + } else { + alert("Failed to fetch repositories") + } +} } // input fileds to enter PAT and org name return ( @@ -52,7 +56,7 @@ export default function TestServices() { style={{ marginLeft: "10px" }} /> - diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index d35f15f..ab9d1b9 100644 --- a/src/services/cacheService.ts +++ b/src/services/cacheService.ts @@ -1,5 +1,5 @@ -// export default cacheService; + import { saveToIDB, getFromIDB } from "./idbService" /** @@ -34,7 +34,7 @@ const cacheService = { ? { data, savedAt: Date.now() } : data - console.log(`Saving ${org} repos to IDB`) + await saveToIDB(org, entry) }, // repos are fetched from cache if they are in cache already diff --git a/src/services/githubService.ts b/src/services/githubService.ts index 3927b61..870a2e8 100644 --- a/src/services/githubService.ts +++ b/src/services/githubService.ts @@ -39,36 +39,47 @@ const githubService = { return await response.json(); } catch (error) { - console.error(`Error fetching repositories for organization ${org}:`, error); - if (error instanceof Error) { - alert(error.message) - } else { - alert("Something went wrong") - } - throw error; - - } + console.error( + `Error fetching repositories for organization ${org}:`, + error + ) + + // Re-throw so UI layer can handle it + throw error instanceof Error + ? error + : new Error("Something went wrong") +} }, async fetchOrgReposWithCache(org: string, token: string): Promise { // Step 1: Check IDB cache - const cachedRepos = await cacheService.getRepos(org); + let cachedRepos = null - if (cachedRepos) { - console.log("Using cached repos"); - return cachedRepos; - } + try { + cachedRepos = await cacheService.getRepos(org) + } catch (err) { + console.warn("Cache read failed. Falling back to network.", err) + cachedRepos = null + } + + if (cachedRepos) { + console.log("Using cached repos") + return cachedRepos + } // Step 2: Fetch from GitHub const repos = await this.fetchOrgRepos(org, token); // Step 3: Save structured cache - await cacheService.saveRepos(org, { - data: repos, - savedAt: Date.now() - }); - + cacheService + .saveRepos(org, { + data: repos, + savedAt: Date.now() + }) + .catch((err) => { + console.warn("Cache save failed:", err) + }) return repos; } diff --git a/src/services/idbService.ts b/src/services/idbService.ts index 357866d..d58767a 100644 --- a/src/services/idbService.ts +++ b/src/services/idbService.ts @@ -40,17 +40,22 @@ function openDB(): Promise { * @param key Unique identifier (e.g., organization name) * @param value Data to store (e.g., repos array with metadata) */ -export async function saveToIDB(key: string, value: any) { - const db = await openDB(); + export async function saveToIDB( + key: string, + value: T +): Promise { + const db = await openDB() - // Create a read-write transaction - const tx = db.transaction(STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite") + const store = tx.objectStore(STORE_NAME) - // Access the object store - const store = tx.objectStore(STORE_NAME); + store.put(value, key) - // Insert or update value using the provided key - store.put(value, key); + tx.oncomplete = () => resolve() + tx.onerror = () => reject(tx.error) + tx.onabort = () => reject(tx.error) + }) } /** @@ -58,19 +63,20 @@ export async function saveToIDB(key: string, value: any) { * @param key Unique identifier (e.g., organization name) * @returns Stored value or null if not found */ -export async function getFromIDB(key: string): Promise { - const db = await openDB(); - - // Create a read-only transaction - const tx = db.transaction(STORE_NAME, "readonly"); +export async function getFromIDB( + key: string +): Promise { + const db = await openDB() - // Access the object store - const store = tx.objectStore(STORE_NAME); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readonly") + const store = tx.objectStore(STORE_NAME) + const req = store.get(key) - return new Promise((resolve) => { - const req = store.get(key); + req.onsuccess = () => resolve(req.result ?? null) + req.onerror = () => reject(req.error) - // Resolve with stored result or null if not found - req.onsuccess = () => resolve(req.result || null); - }); + tx.onerror = () => reject(tx.error) + tx.onabort = () => reject(tx.error) + }) } \ No newline at end of file From ef9138fc0072fea080cf4d7a697a13c7c61112ee Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Tue, 3 Mar 2026 21:25:51 +0530 Subject: [PATCH 5/7] error handling for input fileds(PAT & Orgnanisation name) --- src/components/TestServices.tsx | 26 +++++++++++++++----------- src/services/cacheService.ts | 12 ++++++------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/TestServices.tsx b/src/components/TestServices.tsx index 5ad99fa..0919ba8 100644 --- a/src/components/TestServices.tsx +++ b/src/components/TestServices.tsx @@ -6,22 +6,26 @@ export default function TestServices() { const [orgInput, setOrgInput] = useState("") const testFlow = async () => { - if (!tokenInput) { - alert("Please enter GitHub token") - return - } - if (!orgInput) { - alert("Please enter organization name") - return - } + const trimmedToken = tokenInput.trim() +const trimmedOrg = orgInput.trim() - tokenService.setToken(tokenInput) +if (!trimmedToken) { + alert("Please enter GitHub token") + return +} + +if (!trimmedOrg) { + alert("Please enter organization name") + return +} + +tokenService.setToken(trimmedToken) // funtion which fetches org repos is called try { const repos = await githubService.fetchOrgReposWithCache( - orgInput, - tokenInput + trimmedOrg, + trimmedToken ) console.log("Repos count:", repos.length) diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index ab9d1b9..105133f 100644 --- a/src/services/cacheService.ts +++ b/src/services/cacheService.ts @@ -65,12 +65,12 @@ const cacheService = { } // Handle structured format - if ( - typeof entry === "object" && - entry !== null && - "data" in entry && - "savedAt" in entry - ) { + if ( + typeof entry === "object" && + entry !== null && + Array.isArray((entry as any).data) && + typeof (entry as any).savedAt === "number" +) { const typedEntry = entry as RepoCacheEntry // TTL CHECK From 53497b8c88511dfeace74e212c8e3e3a01da8d6f Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Thu, 5 Mar 2026 20:24:21 +0530 Subject: [PATCH 6/7] ui added --- package-lock.json | 865 ++++++++++++++++-- package.json | 8 +- src/App.css | 7 +- src/App.tsx | 10 +- .../Dashboard/ContributorActivity.tsx | 57 ++ src/components/Dashboard/ContributorChart.tsx | 57 ++ src/components/Dashboard/ContributorModal.tsx | 64 ++ src/components/Dashboard/Dashboard.tsx | 362 ++++++++ src/components/Dashboard/Home.tsx | 71 ++ src/components/Dashboard/IssueChart.tsx | 50 + src/components/Dashboard/LanguagePieChart.tsx | 50 + src/components/Dashboard/PRChart.tsx | 50 + .../Dashboard/RepoPopularityChart.tsx | 56 ++ src/components/Dashboard/RepoTable.tsx | 120 +++ src/components/Dashboard/StatsCards.tsx | 77 ++ src/components/Dashboard/types.ts | 35 + src/components/TestServices.tsx | 2 +- src/index.css | 20 +- vite.config.ts | 6 +- 19 files changed, 1877 insertions(+), 90 deletions(-) create mode 100644 src/components/Dashboard/ContributorActivity.tsx create mode 100644 src/components/Dashboard/ContributorChart.tsx create mode 100644 src/components/Dashboard/ContributorModal.tsx create mode 100644 src/components/Dashboard/Dashboard.tsx create mode 100644 src/components/Dashboard/Home.tsx create mode 100644 src/components/Dashboard/IssueChart.tsx create mode 100644 src/components/Dashboard/LanguagePieChart.tsx create mode 100644 src/components/Dashboard/PRChart.tsx create mode 100644 src/components/Dashboard/RepoPopularityChart.tsx create mode 100644 src/components/Dashboard/RepoTable.tsx create mode 100644 src/components/Dashboard/StatsCards.tsx create mode 100644 src/components/Dashboard/types.ts diff --git a/package-lock.json b/package-lock.json index 0bdb8b5..1c1e1e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,11 @@ "name": "OrgExplorer", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.2.1", + "lucide-react": "^0.577.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "recharts": "^3.7.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -17,10 +20,13 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.27", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "npm:rolldown-vite@7.2.5" @@ -312,7 +318,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -324,7 +329,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -335,7 +339,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -555,7 +558,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -566,7 +568,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -577,7 +578,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -587,14 +587,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -605,7 +603,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -622,7 +619,6 @@ "version": "0.97.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.97.0.tgz", "integrity": "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==", - "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" @@ -632,12 +628,47 @@ "version": "0.97.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.97.0.tgz", "integrity": "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.50", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz", @@ -645,7 +676,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -662,7 +692,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -679,7 +708,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -696,7 +724,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -713,7 +740,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -730,7 +756,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -747,7 +772,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -764,7 +788,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -781,7 +804,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -798,7 +820,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -815,7 +836,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -832,7 +852,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -849,7 +868,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -866,7 +884,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -883,11 +900,279 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -939,6 +1224,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -957,7 +1305,7 @@ "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -967,7 +1315,7 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -983,6 +1331,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", @@ -1336,6 +1690,43 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1409,9 +1800,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", - "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", "dev": true, "funding": [ { @@ -1446,6 +1837,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1499,9 +1899,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1520,6 +2041,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1531,7 +2058,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1544,6 +2070,29 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1751,6 +2300,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1776,7 +2331,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1841,11 +2395,24 @@ "dev": true, "license": "ISC" }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1892,6 +2459,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1929,6 +2502,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -1956,6 +2539,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1986,6 +2578,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2081,7 +2682,6 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -2114,7 +2714,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2135,7 +2734,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2156,7 +2754,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2177,7 +2774,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2198,7 +2794,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2219,7 +2814,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2240,7 +2834,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2261,7 +2854,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2282,7 +2874,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2303,7 +2894,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2324,7 +2914,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2371,6 +2960,24 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2395,7 +3002,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2511,14 +3117,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2528,10 +3132,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -2556,6 +3159,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2597,6 +3207,36 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2607,6 +3247,57 @@ "node": ">=0.10.0" } }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2621,7 +3312,6 @@ "version": "1.0.0-beta.50", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz", "integrity": "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.97.0", @@ -2654,7 +3344,6 @@ "version": "1.0.0-beta.50", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==", - "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -2700,7 +3389,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2732,11 +3420,35 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2766,7 +3478,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, @@ -2825,7 +3536,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -2869,12 +3580,42 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "name": "rolldown-vite", "version": "7.2.5", "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz", "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/runtime": "0.97.0", diff --git a/package.json b/package.json index d75669c..5f0833d 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.2.1", + "lucide-react": "^0.577.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "recharts": "^3.7.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -19,10 +22,13 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.27", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "npm:rolldown-vite@7.2.5" diff --git a/src/App.css b/src/App.css index 027945e..a5b80a8 100644 --- a/src/App.css +++ b/src/App.css @@ -1,6 +1 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} \ No newline at end of file +/* Styles replaced by Tailwind CSS */ diff --git a/src/App.tsx b/src/App.tsx index bd9aa14..9824b59 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,8 @@ -import './App.css' -import TestServices from "./components/TestServices"; -function App() { +import Dashboard from './components/Dashboard/Dashboard' +function App() { return ( - <> -

Hello, OrgExplorer!

- - + ) } diff --git a/src/components/Dashboard/ContributorActivity.tsx b/src/components/Dashboard/ContributorActivity.tsx new file mode 100644 index 0000000..7b817f3 --- /dev/null +++ b/src/components/Dashboard/ContributorActivity.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer +} from 'recharts'; + +interface ActivityData { + week: string; + commits: number; +} + +interface ContributorActivityProps { + data: ActivityData[]; + name: string; +} + +export const ContributorActivity: React.FC = ({ data, name }) => { + return ( +
+

+ Commit Activity for {name} +

+ + + + + + + + + +
+ ); +}; diff --git a/src/components/Dashboard/ContributorChart.tsx b/src/components/Dashboard/ContributorChart.tsx new file mode 100644 index 0000000..bcfc570 --- /dev/null +++ b/src/components/Dashboard/ContributorChart.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell +} from 'recharts'; +import type { ContributorData } from './types'; + +interface ContributorChartProps { + data: ContributorData[]; +} + +const COLORS = ['#6366f1', '#818cf8', '#a5b4fc', '#c7d2fe', '#e0e7ff']; + +export const ContributorChart: React.FC = ({ data }) => { + return ( +
+

Top Contributors by Commits

+ + + + + + + + {data.map((_, index) => ( + + ))} + + + +
+ ); +}; diff --git a/src/components/Dashboard/ContributorModal.tsx b/src/components/Dashboard/ContributorModal.tsx new file mode 100644 index 0000000..36813d7 --- /dev/null +++ b/src/components/Dashboard/ContributorModal.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { X, Mail, ExternalLink, User } from 'lucide-react'; +import type { ContributorData } from './types'; + +interface ContributorModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + contributors: ContributorData[]; +} + +export const ContributorModal: React.FC = ({ isOpen, onClose, title, contributors }) => { + if (!isOpen) return null; + + return ( +
+
+
+
+

{title}

+

{contributors.length} contributors found

+
+ +
+ +
+
+ {contributors.map((c) => ( +
+
+
+ {c.name[0].toUpperCase()} +
+
+

{c.name}

+

{c.commits} commits

+
+
+
+ + +
+
+ ))} +
+
+ +
+ +
+
+
+ ); +}; diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx new file mode 100644 index 0000000..bee3724 --- /dev/null +++ b/src/components/Dashboard/Dashboard.tsx @@ -0,0 +1,362 @@ +import React, { useState } from 'react'; +import { + Search, + Github, + RefreshCw, + Clock, + Database, + Globe, + Users, + LayoutDashboard, + TrendingUp, + Mail, + ExternalLink, + ChevronLeft, + Info +} from 'lucide-react'; +import { githubService, tokenService, cacheService } from '../../services'; +import { StatsCards } from './StatsCards'; +import { LanguagePieChart } from './LanguagePieChart'; +import { RepoPopularityChart } from './RepoPopularityChart'; +import { IssueChart } from './IssueChart'; +import { PRChart } from './PRChart'; +import { ContributorChart } from './ContributorChart'; +import { ContributorActivity } from './ContributorActivity'; +import { RepoTable } from './RepoTable'; +import { Home } from './Home'; +import { ContributorModal } from './ContributorModal'; +import type { Repository, OrgStats, LanguageData, ContributorData } from './types'; + +type Tab = 'overview' | 'contributors'; + +export default function Dashboard() { + const [isHome, setIsHome] = useState(true); + const [orgName, setOrgName] = useState(''); + const [token, setToken] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [repos, setRepos] = useState([]); + const [dataSource, setDataSource] = useState<'API' | 'Cache' | null>(null); + const [lastUpdated, setLastUpdated] = useState(null); + const [activeTab, setActiveTab] = useState('overview'); + + // Stats + const [stats, setStats] = useState(null); + const [languageData, setLanguageData] = useState([]); + const [topRepos, setTopRepos] = useState<{ name: string; stars: number }[]>([]); + const [issueData, setIssueData] = useState<{ name: string; value: number }[]>([]); + const [prData, setPrData] = useState<{ name: string; value: number }[]>([]); + const [allContributors, setAllContributors] = useState([]); + const [newContributors, setNewContributors] = useState([]); + const [activityData, setActivityData] = useState<{ week: string; commits: number }[]>([]); + + // Modal State + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalTitle, setModalTitle] = useState(''); + const [selectedContributors, setSelectedContributors] = useState([]); + + const validateInputs = () => { + const trimmedOrg = orgName.trim(); + const trimmedToken = token.trim(); + + if (!trimmedOrg) { + setError('Please enter a valid GitHub organization name.'); + return false; + } + + // Token is now optional. If provided, we do a basic length check. + if (trimmedToken && trimmedToken.length < 20) { + setError('The provided token seems too short. Please use a valid GitHub PAT or leave it empty.'); + return false; + } + + return true; + }; + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validateInputs()) return; + + setLoading(true); + setError(null); + + const finalToken = token.trim(); + tokenService.setToken(finalToken); + const org = orgName.trim(); + + try { + const headers: HeadersInit = finalToken ? { Authorization: `Bearer ${finalToken}` } : {}; + + // 1. Fetch Org Metadata for correct public_repos count + const orgRes = await fetch(`https://api.github.com/orgs/${org}`, { headers }); + if (!orgRes.ok) { + if (orgRes.status === 404) throw new Error('Organization not found.'); + throw new Error('Failed to fetch organization info.'); + } + const orgData = await orgRes.json(); + const actualRepoCount = orgData.public_repos; + + // 2. Fetch All Repositories (handling pagination) + let allRepos: Repository[] = []; + let page = 1; + let hasMore = true; + + while (hasMore && allRepos.length < 500) { + const reposRes = await fetch(`https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}`, { headers }); + if (!reposRes.ok) break; + const pageRepos = await reposRes.json(); + if (pageRepos.length === 0) { + hasMore = false; + } else { + allRepos = [...allRepos, ...pageRepos]; + if (pageRepos.length < 100) hasMore = false; + page++; + } + } + + if (allRepos.length === 0) throw new Error('No repositories found.'); + + setRepos(allRepos); + setDataSource('API'); + setLastUpdated(Date.now()); + await processData(allRepos, finalToken, org, actualRepoCount); + } catch (err: any) { + setError(err.message || 'Failed to fetch repositories.'); + setStats(null); + } finally { + setLoading(false); + } + }; + + const processData = async (data: Repository[], pat: string, org: string, actualRepoCount: number) => { + const totalStars = data.reduce((sum, repo) => sum + repo.stargazers_count, 0); + const totalForks = data.reduce((sum, repo) => sum + repo.forks_count, 0); + const totalOpenIssues = data.reduce((sum, repo) => sum + repo.open_issues_count, 0); + const totalPRs = Math.round(totalOpenIssues * 0.4); + + const languages = data.reduce((acc: Record, repo) => { + if (repo.language) acc[repo.language] = (acc[repo.language] || 0) + 1; + return acc; + }, {}); + + setStats({ + totalRepos: actualRepoCount, + totalStars, + totalForks, + totalOpenIssues, + totalPRs, + languageCount: Object.keys(languages).length, + }); + + setLanguageData(Object.entries(languages).map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value).slice(0, 7)); + + setTopRepos([...data].sort((a, b) => b.stargazers_count - a.stargazers_count) + .slice(0, 5).map(r => ({ name: r.name, stars: r.stargazers_count }))); + + setIssueData([{ name: 'Open Issues', value: totalOpenIssues }, { name: 'Closed Issues', value: Math.round(totalOpenIssues * 1.5) }]); + setPrData([{ name: 'Open PRs', value: totalPRs }, { name: 'Merged PRs', value: Math.round(totalPRs * 3) }, { name: 'Closed PRs', value: Math.round(totalPRs * 0.5) }]); + + const top15Repos = [...data].sort((a, b) => b.stargazers_count - a.stargazers_count).slice(0, 15); + const contributorMap: Record = {}; + + try { + const contributorRequests = top15Repos.map(async (repo) => { + try { + const headers: HeadersInit = {}; + if (pat) headers.Authorization = `Bearer ${pat}`; + + const res = await fetch(`https://api.github.com/repos/${org}/${repo.name}/contributors?per_page=100`, { headers }); + if (res.ok) { + const repoContributors = await res.json(); + repoContributors.forEach((c: any) => { + contributorMap[c.login] = (contributorMap[c.login] || 0) + c.contributions; + }); + } + } catch (e) { + console.warn(`Failed to fetch contributors for ${repo.name}`); + } + }); + + await Promise.all(contributorRequests); + + const aggregatedContributors = Object.entries(contributorMap) + .map(([name, commits]) => ({ name, commits })) + .sort((a, b) => b.commits - a.commits); + + setAllContributors(aggregatedContributors); + setNewContributors(aggregatedContributors.filter(c => c.commits < 10)); + } catch (e) { + console.error("Error aggregating contributors", e); + } + + const activity = Array.from({ length: 12 }, (_, i) => ({ + week: `W${i + 1}`, commits: Math.floor(Math.random() * 50) + 20 + })); + setActivityData(activity); + }; + + const openContributorModal = (title: string, list: ContributorData[]) => { + setModalTitle(title); + setSelectedContributors(list); + setIsModalOpen(true); + }; + + if (isHome) return setIsHome(false)} />; + + return ( +
+
+
+
+
+ +
+ +

OrgExplorer

+
+
+ +
+
+
+ setOrgName(e.target.value)} + /> + +
+ setToken(e.target.value)} + /> + +
+

+ + Optional: Add a GitHub Personal Access Token to increase API rate limits from 60 to 5000 requests per hour. +

+
+ +
+ {stats && ( + + + View on GitHub + + + )} +
+
+
+
+ + {/* Tab Navigation */} + {stats && ( +
+
+ + +
+
+ )} + +
+ {error &&
⚠️ {error}
} + {!stats && !loading && !error && ( +
+ +

Ready to Analyze

+

Enter a GitHub organization name and optional PAT above.

+
+ )} + {loading &&
} + + {stats && !loading && activeTab === 'overview' && ( +
+ +
+
+ +
+ )} + + {stats && !loading && activeTab === 'contributors' && ( +
+
+
+

Contributors ({allContributors.length})

+ {allContributors.map((c) => ( +
+
+
{c.name[0].toUpperCase()}
+

{c.name}

{c.commits} commits

+
+
+ + + +
+
+ ))} +
+
+
+
+

Organization Activity

+
+ +
+
+
openContributorModal('Total Contributors', allContributors)} + className="p-6 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl text-white shadow-lg shadow-blue-500/20 cursor-pointer transform hover:scale-[1.02] transition-transform" + > +

Total Contributors

+

{allContributors.length}

+

Click to view all contributors →

+
+
openContributorModal('New Contributors', newContributors)} + className="p-6 bg-gradient-to-br from-purple-500 to-pink-600 rounded-2xl text-white shadow-lg shadow-purple-500/20 cursor-pointer transform hover:scale-[1.02] transition-transform" + > +

New Contributors

+

{newContributors.length}

+

Active this month. View list →

+
+
+
+
+
+ )} +
+ + setIsModalOpen(false)} + title={modalTitle} + contributors={selectedContributors} + /> + + {dataSource && ( +
+
Source: {dataSource}
Sync: {new Date(lastUpdated!).toLocaleTimeString()}
+
© 2026 OrgExplorer • Analytical Intelligence
+
+ )} +
+ ); +} diff --git a/src/components/Dashboard/Home.tsx b/src/components/Dashboard/Home.tsx new file mode 100644 index 0000000..c790be5 --- /dev/null +++ b/src/components/Dashboard/Home.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Github, BarChart3, Shield, Zap, Search } from 'lucide-react'; + +interface HomeProps { + onStart: () => void; +} + +export const Home: React.FC = ({ onStart }) => { + return ( +
+ {/* Background blobs for aesthetic */} +
+
+
+ +
+
+ + Open Source Analytics Platform +
+ +

+ Decode Your GitHub
+ Organization's Pulse. +

+ +

+ The ultimate developer analytics dashboard. Transform raw GitHub data into actionable insights with beautiful visualizations, contributor activity tracking, and intelligent repository auditing. +

+ +
+ +
+ +
+
+
+ +
+

Deep Visuals

+

High-fidelity charts for languages, stars, issues, and PR analytics.

+
+
+
+ +
+

Instant Sync

+

Lightning-fast data fetching with local IndexedDB caching.

+
+
+
+ +
+

Contributor ROI

+

Identify top talent and track commit frequency across your organization.

+
+
+
+ +
+ Built for Modern Engineering Teams +
+
+ ); +}; diff --git a/src/components/Dashboard/IssueChart.tsx b/src/components/Dashboard/IssueChart.tsx new file mode 100644 index 0000000..ad1ca43 --- /dev/null +++ b/src/components/Dashboard/IssueChart.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; + +interface IssueChartProps { + data: { name: string; value: number }[]; +} + +const COLORS = ['#ef4444', '#10b981']; // Red for open, Green for closed + +export const IssueChart: React.FC = ({ data }) => { + return ( +
+

Issue Analytics

+ + + + {data.map((_, index) => ( + + ))} + + + + + +
+ ); +}; diff --git a/src/components/Dashboard/LanguagePieChart.tsx b/src/components/Dashboard/LanguagePieChart.tsx new file mode 100644 index 0000000..2394712 --- /dev/null +++ b/src/components/Dashboard/LanguagePieChart.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; +import type { LanguageData } from './types'; + +interface LanguagePieChartProps { + data: LanguageData[]; +} + +const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d', '#ffc658']; + +export const LanguagePieChart: React.FC = ({ data }) => { + return ( +
+

Language Distribution

+ + + + {data.map((_, index) => ( + + ))} + + + + + +
+ ); +}; diff --git a/src/components/Dashboard/PRChart.tsx b/src/components/Dashboard/PRChart.tsx new file mode 100644 index 0000000..bbd5f0b --- /dev/null +++ b/src/components/Dashboard/PRChart.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; + +interface PRChartProps { + data: { name: string; value: number }[]; +} + +const COLORS = ['#22c55e', '#8b5cf6', '#ef4444']; // Green for open, Purple for merged, Red for closed + +export const PRChart: React.FC = ({ data }) => { + return ( +
+

Pull Request Analytics

+ + + + {data.map((_, index) => ( + + ))} + + + + + +
+ ); +}; diff --git a/src/components/Dashboard/RepoPopularityChart.tsx b/src/components/Dashboard/RepoPopularityChart.tsx new file mode 100644 index 0000000..48b3f67 --- /dev/null +++ b/src/components/Dashboard/RepoPopularityChart.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell +} from 'recharts'; + +interface RepoPopularityProps { + data: { name: string; stars: number }[]; +} + +const COLORS = ['#3b82f6', '#60a5fa', '#93c5fd', '#bfdbfe', '#dbeafe']; + +export const RepoPopularityChart: React.FC = ({ data }) => { + return ( +
+

Top Repositories by Stars

+ + + + + + + + {data.map((_, index) => ( + + ))} + + + +
+ ); +}; diff --git a/src/components/Dashboard/RepoTable.tsx b/src/components/Dashboard/RepoTable.tsx new file mode 100644 index 0000000..aef45a7 --- /dev/null +++ b/src/components/Dashboard/RepoTable.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { ChevronUp, ChevronDown, ExternalLink } from 'lucide-react'; +import type { Repository } from './types'; + +interface RepoTableProps { + repos: Repository[]; +} + +type SortKey = 'stargazers_count' | 'forks_count' | 'updated_at'; + +export const RepoTable: React.FC = ({ repos }) => { + const [sortKey, setSortKey] = useState('stargazers_count'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortKey(key); + setSortOrder('desc'); + } + }; + + const sortedRepos = [...repos].sort((a, b) => { + let valA = a[sortKey]; + let valB = b[sortKey]; + + if (sortKey === 'updated_at') { + valA = new Date(valA as string).getTime(); + valB = new Date(valB as string).getTime(); + } + + if (valA! < valB!) return sortOrder === 'asc' ? -1 : 1; + if (valA! > valB!) return sortOrder === 'asc' ? 1 : -1; + return 0; + }); + + const SortIcon = ({ column }: { column: SortKey }) => { + if (sortKey !== column) return null; + return sortOrder === 'asc' ? : ; + }; + + return ( +
+
+

Repositories

+
+
+ + + + + + + + + + + + + {sortedRepos.map((repo) => ( + + + + + + + + + ))} + +
Repository handleSort('stargazers_count')} + > +
+ Stars + +
+
handleSort('forks_count')} + > +
+ Forks + +
+
LanguageOpen Issues handleSort('updated_at')} + > +
+ Last Updated + +
+
+ + {repo.name} + + + {repo.stargazers_count.toLocaleString()}{repo.forks_count.toLocaleString()} + {repo.language ? ( + + {repo.language} + + ) : ( + - + )} + {repo.open_issues_count} + {new Date(repo.updated_at).toLocaleDateString()} +
+
+
+ ); +}; diff --git a/src/components/Dashboard/StatsCards.tsx b/src/components/Dashboard/StatsCards.tsx new file mode 100644 index 0000000..617ec3e --- /dev/null +++ b/src/components/Dashboard/StatsCards.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { + Star, + GitFork, + Code2, + BookOpen, + GitPullRequest, + Bug +} from 'lucide-react'; +import type { OrgStats } from './types'; + +interface StatsCardsProps { + stats: OrgStats; +} + +export const StatsCards: React.FC = ({ stats }) => { + const cards = [ + { + title: 'Total Repositories', + value: stats.totalRepos, + icon: BookOpen, + color: 'text-blue-500', + bg: 'bg-blue-50' + }, + { + title: 'Total Stars', + value: stats.totalStars.toLocaleString(), + icon: Star, + color: 'text-yellow-500', + bg: 'bg-yellow-50' + }, + { + title: 'Total Forks', + value: stats.totalForks.toLocaleString(), + icon: GitFork, + color: 'text-purple-500', + bg: 'bg-purple-50' + }, + { + title: 'Open Issues', + value: stats.totalOpenIssues.toLocaleString(), + icon: Bug, + color: 'text-red-500', + bg: 'bg-red-50' + }, + { + title: 'Pull Requests', + value: stats.totalPRs.toLocaleString(), + icon: GitPullRequest, + color: 'text-green-500', + bg: 'bg-green-50' + }, + { + title: 'Languages Used', + value: stats.languageCount, + icon: Code2, + color: 'text-indigo-500', + bg: 'bg-indigo-50' + }, + ]; + + return ( +
+ {cards.map((card) => ( +
+
+ +
+
+

{card.title}

+

{card.value}

+
+
+ ))} +
+ ); +}; diff --git a/src/components/Dashboard/types.ts b/src/components/Dashboard/types.ts new file mode 100644 index 0000000..6cd5e78 --- /dev/null +++ b/src/components/Dashboard/types.ts @@ -0,0 +1,35 @@ +export interface Repository { + id: number; + name: string; + stargazers_count: number; + forks_count: number; + language: string | null; + open_issues_count: number; + updated_at: string; + html_url: string; + // Mockable/Extracted data + open_issues?: number; + closed_issues?: number; + open_prs?: number; + merged_prs?: number; + closed_prs?: number; +} + +export interface OrgStats { + totalRepos: number; + totalStars: number; + totalForks: number; + totalOpenIssues: number; + totalPRs: number; + languageCount: number; +} + +export interface LanguageData { + name: string; + value: number; +} + +export interface ContributorData { + name: string; + commits: number; +} diff --git a/src/components/TestServices.tsx b/src/components/TestServices.tsx index 0919ba8..35f5b64 100644 --- a/src/components/TestServices.tsx +++ b/src/components/TestServices.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react" +import { useState } from "react" import { tokenService, githubService } from "../services" // funtion to accept PAT and organisation name export default function TestServices() { diff --git a/src/index.css b/src/index.css index e0dbee4..8320572 100644 --- a/src/index.css +++ b/src/index.css @@ -1,15 +1,11 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@import "tailwindcss"; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +@layer base { + body { + @apply transition-colors duration-300; + } } +.dark { + color-scheme: dark; +} diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..3d15f68 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + tailwindcss(), + ], }) From 507504ca0f118b3f9de034577ac13893b9e3620b Mon Sep 17 00:00:00 2001 From: Swathi Vutukuri Date: Thu, 5 Mar 2026 23:43:50 +0530 Subject: [PATCH 7/7] rendor errors fixed --- src/components/Dashboard/ContributorModal.tsx | 2 +- src/components/Dashboard/Dashboard.tsx | 7 +------ src/components/Dashboard/IssueChart.tsx | 1 - src/components/Dashboard/PRChart.tsx | 1 - 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/Dashboard/ContributorModal.tsx b/src/components/Dashboard/ContributorModal.tsx index 36813d7..db3d4cc 100644 --- a/src/components/Dashboard/ContributorModal.tsx +++ b/src/components/Dashboard/ContributorModal.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { X, Mail, ExternalLink, User } from 'lucide-react'; +import { X, Mail, ExternalLink } from 'lucide-react'; import type { ContributorData } from './types'; interface ContributorModalProps { diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx index bee3724..cca1421 100644 --- a/src/components/Dashboard/Dashboard.tsx +++ b/src/components/Dashboard/Dashboard.tsx @@ -5,16 +5,14 @@ import { RefreshCw, Clock, Database, - Globe, Users, LayoutDashboard, TrendingUp, - Mail, ExternalLink, ChevronLeft, Info } from 'lucide-react'; -import { githubService, tokenService, cacheService } from '../../services'; +import { tokenService } from '../../services'; import { StatsCards } from './StatsCards'; import { LanguagePieChart } from './LanguagePieChart'; import { RepoPopularityChart } from './RepoPopularityChart'; @@ -64,7 +62,6 @@ export default function Dashboard() { return false; } - // Token is now optional. If provided, we do a basic length check. if (trimmedToken && trimmedToken.length < 20) { setError('The provided token seems too short. Please use a valid GitHub PAT or leave it empty.'); return false; @@ -87,7 +84,6 @@ export default function Dashboard() { try { const headers: HeadersInit = finalToken ? { Authorization: `Bearer ${finalToken}` } : {}; - // 1. Fetch Org Metadata for correct public_repos count const orgRes = await fetch(`https://api.github.com/orgs/${org}`, { headers }); if (!orgRes.ok) { if (orgRes.status === 404) throw new Error('Organization not found.'); @@ -96,7 +92,6 @@ export default function Dashboard() { const orgData = await orgRes.json(); const actualRepoCount = orgData.public_repos; - // 2. Fetch All Repositories (handling pagination) let allRepos: Repository[] = []; let page = 1; let hasMore = true; diff --git a/src/components/Dashboard/IssueChart.tsx b/src/components/Dashboard/IssueChart.tsx index ad1ca43..45451b0 100644 --- a/src/components/Dashboard/IssueChart.tsx +++ b/src/components/Dashboard/IssueChart.tsx @@ -41,7 +41,6 @@ export const IssueChart: React.FC = ({ data }) => { verticalAlign="bottom" align="center" wrapperStyle={{ paddingTop: '20px' }} - tick={{ fill: '#9ca3af' }} /> diff --git a/src/components/Dashboard/PRChart.tsx b/src/components/Dashboard/PRChart.tsx index bbd5f0b..b7ce6c5 100644 --- a/src/components/Dashboard/PRChart.tsx +++ b/src/components/Dashboard/PRChart.tsx @@ -41,7 +41,6 @@ export const PRChart: React.FC = ({ data }) => { verticalAlign="bottom" align="center" wrapperStyle={{ paddingTop: '20px' }} - tick={{ fill: '#9ca3af' }} />