diff --git a/README.md b/README.md index 7cb9cea..8c27c4e 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ - **Fully Browser-Based** — Runs entirely in the browser using GitHub APIs with no backend server required. - **Organization Overview Dashboard** — Explore repositories, contributors, activity trends, tech stack distribution, and organization growth insights. +- **Smart Organization Search with Autocomplete** — GitHub org suggestions with real-time dropdown search - **Advanced Repository Analytics** — Analyze repository activity, contributor density, issue and PR trends, health metrics, and lifecycle status. diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index cffcbd3..208127a 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -1,142 +1,205 @@ -import React, { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { FiSearch, FiX } from 'react-icons/fi' -import { useApp } from '../context/AppContext' -import { C, Spinner } from '../components/UI' + import React, { useState,useEffect } from 'react' + import { useNavigate } from 'react-router-dom' + import { FiSearch, FiX } from 'react-icons/fi' + import { useApp } from '../context/AppContext' + import { C, Spinner } from '../components/UI' + import { searchOrgs } from '../services/github' -const QUICK = ['AOSSIE-Org', 'DjedAlliance', 'StabilityNexus'] + const QUICK = ['AOSSIE-Org', 'DjedAlliance', 'StabilityNexus'] -export default function HomePage() { - const { explore, loading, loadMsg, error } = useApp() - const navigate = useNavigate() - const [input, setInput] = useState('') - const [chips, setChips] = useState([]) + export default function HomePage() { + const { explore, loading, loadMsg, error } = useApp() + const navigate = useNavigate() + const [input, setInput] = useState('') + const [chips, setChips] = useState([]) + const[suggestions, setSuggestions] = useState([]) + const[showSuggestion, setShowSuggestions] = useState(false) - const recent = JSON.parse(localStorage.getItem('oe_recent') || '[]') + useEffect(() => { + if (!input.trim()) { + setSuggestions([]) + return + } + const t = setTimeout(async () => { + const result = await searchOrgs(input.trim()) + setSuggestions(result.slice(0,6)) + setShowSuggestions(true) + }, 300) + return () => clearTimeout(t) + }, [input]) - const addChip = raw => { - const parts = raw.split(/[,+\s]+/).map(s => s.trim()).filter(Boolean) - setChips(prev => [...new Set([...prev, ...parts])]) - setInput('') - } + const selectSuggestion = (login) => { + setChips(prev => [...new Set([...prev,login])]) + setInput('') + setSuggestion([]) + setShowSuggestions(false) + } - const removeChip = c => setChips(prev => prev.filter(x => x !== c)) + const recent = JSON.parse(localStorage.getItem('oe_recent') || '[]') - const handleKey = e => { - if ((e.key === 'Enter' || e.key === ',') && input.trim()) { - e.preventDefault() - addChip(input) - } - if (e.key === 'Backspace' && !input && chips.length) { - setChips(prev => prev.slice(0, -1)) + const addChip = raw => { + const parts = raw.split(/[,+\s]+/).map(s => s.trim()).filter(Boolean) + setChips(prev => [...new Set([...prev, ...parts])]) + setInput('') } - } - const go = async (targets) => { - const orgs = targets || (chips.length ? chips : input.trim() ? [input.trim()] : []) - if (!orgs.length) return - const success = await explore(orgs) - if(success) navigate('/overview') - } + const removeChip = c => setChips(prev => prev.filter(x => x !== c)) - return ( -
- {/* Hero */} -
-

- Architect Your{' '} - Insights -

-

- Unified analytics across one or many GitHub organizations. Multi-org portfolio analysis, contributor network graphs, time-series trends, and governance audits — entirely in the browser. -

-
+ const handleKey = e => { + if ((e.key === 'Enter' || e.key === ',') && input.trim()) { + e.preventDefault() + addChip(input) + } + if (e.key === 'Backspace' && !input && chips.length) { + setChips(prev => prev.slice(0, -1)) + } + } - {/* Search */} -
-
- - {chips.map(c => ( - - {c} - removeChip(c)} /> - - ))} - setInput(e.target.value)} - onKeyDown={handleKey} - onBlur={() => input.trim() && addChip(input)} - placeholder={chips.length ? 'Add another org...' : 'AOSSIE-Org, StabilityNexus, DjedAlliance...'} - style={{ flex: 1, minWidth: 160, background: 'none', color: 'var(--text)', fontSize: 14, padding: '4px 8px', border: 'none', outline: 'none' }} - /> - -
-

- Type an org name and press Enter or comma to add. Add multiple orgs to analyze as a unified portfolio. -

- {error &&

{error}

} -
+ const go = async (targets) => { + const orgs = targets || (chips.length ? chips : input.trim() ? [input.trim()] : []) + if (!orgs.length) return + const success = await explore(orgs) + if(success) navigate('/overview') + } - {/* Loading */} - {loading && ( -
- -

{loadMsg}

+ return ( +
+ {/* Hero */} +
+

+ Architect Your{' '} + Insights +

+

+ Unified analytics across one or many GitHub organizations. Multi-org portfolio analysis, contributor network graphs, time-series trends, and governance audits — entirely in the browser. +

- )} - - {/* Recent */} - {recent.length > 0 && !loading && ( -
- Recent searches -
- {recent.map(r => ( - + + {/* Search */} +
+
+ + {chips.map(c => ( + + {c} + removeChip(c)} /> + ))} + setInput(e.target.value)} + onKeyDown={handleKey} + onBlur={() => !showSuggestions && input.trim() && addChip(input)} + aria-label="Search GitHub organizations" + placeholder={chips.length ? 'Add another org...' : 'AOSSIE-Org, StabilityNexus, DjedAlliance...'} + style={{ flex: 1, minWidth: 160, background: 'none', color: 'var(--text)', fontSize: 14, padding: '4px 8px', border: 'none', outline: 'none' }} + /> + +
+

+ Type an org name and press Enter or comma to add. Add multiple orgs to analyze as a unified portfolio. +

+ {error &&

{error}

} + {showSuggestion && suggestions.length > 0 && ( +
+
+ {suggestions.map(org => ( +
selectSuggestion(org.login)} + style={{ + padding: '10px 12px', + cursor: 'pointer', + fontSize: 13, + display: 'flex', + justifyContent: 'space-between' + }} + > + {org.login} + + org + +
+ ))} +
+ )} +
- )} - {/* Quick explore */} - {!loading && ( -
- Quick explore -
- {QUICK.map(q => ( - - ))} + {/* Loading */} + {loading && ( +
+ +

{loadMsg}

-
- )} + )} + + {/* Recent */} + {recent.length > 0 && !loading && ( +
+ Recent searches +
+ {recent.map(r => ( + + ))} +
+
+ )} - {/* Stats bar */} -
- {[['5,000', 'req/hr with PAT', 'var(--green)'], ['1HR', 'intelligent cache', 'var(--green)'], ['ZERO', 'backend latency', 'var(--accent)']].map(([v, l, color]) => ( -
-
{v}
-
{l}
+ {/* Quick explore */} + {!loading && ( +
+ Quick explore +
+ {QUICK.map(q => ( + + ))} +
- ))} + )} + + {/* Stats bar */} +
+ {[['5,000', 'req/hr with PAT', 'var(--green)'], ['1HR', 'intelligent cache', 'var(--green)'], ['ZERO', 'backend latency', 'var(--accent)']].map(([v, l, color]) => ( +
+
{v}
+
{l}
+
+ ))} +
-
- ) -} + ) + } diff --git a/src/services/github.js b/src/services/github.js index 8b10597..1d11002 100644 --- a/src/services/github.js +++ b/src/services/github.js @@ -85,6 +85,19 @@ export async function fetchRepos(org, pat) { return all } +export const searchOrgs = async (query) => { + if (!query) return [] + + const res = await fetch( + `https://api.github.com/search/users?q=${encodeURIComponent(query)}+type:org` + ) + if (!res.ok) return [] + + const data = await res.json() + return data.items || [] +} + + export async function fetchContributors(org, repo, pat) { try { return await fetchWithCache(