diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx
index 66f7de0..10a1a9f 100644
--- a/src/context/AppContext.jsx
+++ b/src/context/AppContext.jsx
@@ -1,32 +1,33 @@
import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { fetchOrg, fetchRepos, fetchContributors, fetchIssues, fetchRateLimit, fetchPulls } from '../services/github'
import { buildAnalyticalModel, getTopRepositories } from '../services/analytics'
+import { storage,STORAGE_KEYS } from '../utils/storage'
import { saveAnalysis, loadAnalysis } from '../services/cache'
const Ctx = createContext(null)
function getStoredRateLimit() {
- const stored = localStorage.getItem('oe_rate_limit')
+ const stored = storage.get(STORAGE_KEYS.RATE_LIMIT)
if (!stored) return null
try {
- const data = JSON.parse(stored)
+
- if (Date.now() > data.reset * 1000) {
- localStorage.removeItem('oe_rate_limit')
+ if (Date.now() > stored.reset * 1000) {
+ storage.remove(STORAGE_KEYS.RATE_LIMIT)
return null
}
- return data
+ return stored
} catch {
- localStorage.removeItem('oe_rate_limit')
+ storage.remove(STORAGE_KEYS.RATE_LIMIT)
return null
}
}
export function AppProvider({ children }) {
- const [pat, setPat] = useState(() => localStorage.getItem('oe_pat') || '')
+ const [pat, setPat] = useState(() => storage.get(STORAGE_KEYS.PAT) || '')
const [orgs, setOrgs] = useState([])
const [model, setModel] = useState(null)
const [issuesData, setIssuesData] = useState({})
@@ -38,7 +39,7 @@ export function AppProvider({ children }) {
const [error, setError] = useState('')
const [totalRepo, setTotalRepo] = useState(0)
const [advanceAnalyticsLoading, setAdvanceAnalyticsLoading] = useState(false);
- const [advanceAnalyticsComplete, setAdvanceAnalyticsComplete] = useState(false)
+ const [advanceAnalyticsComplete, setAdvanceAnalyticsComplete] = useState(false)
const [isComplete, setIsComplete] = useState(false)
const [auditComplete, setAuditComplete] = useState(false)
const [lastOrgNames, setLastOrgNames] = useState([])
@@ -101,7 +102,7 @@ export function AppProvider({ children }) {
useEffect(() => {
const handler = e => {
setRateLimit(e.detail)
- localStorage.setItem('oe_rate_limit', JSON.stringify(e.detail))
+ storage.set(STORAGE_KEYS.RATE_LIMIT, e.detail)
}
window.addEventListener('rate-limit-update', handler)
@@ -115,7 +116,7 @@ export function AppProvider({ children }) {
if (!rateLimit?.reset) return
const timeout = setTimeout(() => {
- localStorage.removeItem('oe_rate_limit')
+ storage.remove(STORAGE_KEYS.RATE_LIMIT)
setRateLimit(null)
}, Math.max(0, rateLimit.reset * 1000 - Date.now()))
@@ -132,7 +133,7 @@ export function AppProvider({ children }) {
}, [pat])
const savePat = useCallback(token => {
setPat(token)
- token ? localStorage.setItem('oe_pat', token) : localStorage.removeItem('oe_pat')
+ token ? storage.set(STORAGE_KEYS.PAT, token) : storage.remove(STORAGE_KEYS.PAT)
}, [])
// Multi-org explore
@@ -184,9 +185,9 @@ export function AppProvider({ children }) {
setIsComplete(!!pat)
// Save to recent searches
- const prev = JSON.parse(localStorage.getItem('oe_recent') || '[]')
+ const prev = storage.get(STORAGE_KEYS.RECENT_SEARCHES) || []
const entry = orgNames.join(', ')
- localStorage.setItem('oe_recent', JSON.stringify([...new Set([entry, ...prev])].slice(0, 6)))
+ storage.set(STORAGE_KEYS.RECENT_SEARCHES, [...new Set([entry, ...prev])].slice(0, 6))
return builtModel
} catch (err) {
setError(err.message === 'RATE_LIMIT'
@@ -344,28 +345,28 @@ export function AppProvider({ children }) {
}, [model, isComplete, runFullExplore, auditRepos, selectAnalysisRepos, pat, govLoading, advanceAnalyticsLoading])
const STALE_DAYS = 90
-
+
const staleRepoStats = useMemo(() => {
const now = Date.now()
-
+
return Object.entries(issuesData || {}).map(([key, issues]) => {
const [org, repo] = key.split('/')
-
+
const normalIssues = issues.filter(i => !i.pull_request)
-
+
const openIssues = normalIssues.filter(i => i.state === 'open')
-
+
const staleIssues = openIssues.filter(i => {
const updated = new Date(i.updated_at).getTime()
const diffDays = (now - updated) / (1000 * 60 * 60 * 24)
return diffDays >= STALE_DAYS
})
-
+
const ratio =
openIssues.length === 0
? 0
: Math.round((staleIssues.length / openIssues.length) * 100)
-
+
return {
id: key,
org,
@@ -391,4 +392,4 @@ export function AppProvider({ children }) {
)
}
-export const useApp = () => useContext(Ctx)
+export const useApp = () => useContext(Ctx)
\ No newline at end of file
diff --git a/src/pages/ContributorProfilePage.jsx b/src/pages/ContributorProfilePage.jsx
index 3cafec8..98b9bb1 100644
--- a/src/pages/ContributorProfilePage.jsx
+++ b/src/pages/ContributorProfilePage.jsx
@@ -4,6 +4,8 @@ import { FiArrowLeft, FiDownload, FiExternalLink, FiCalendar, FiBriefcase, FiAle
import { useApp } from '../context/AppContext'
import { C, PageTitle, Spinner, StatCard } from '../components/UI'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
+import { storage,STORAGE_KEYS } from '../utils/storage'
+
// Reusable ContributionTable component
function ContributionTable({ items, dateHeader, resolveStatus }) {
@@ -128,15 +130,12 @@ export default function ContributorProfilePage() {
let list = orgs.map(o => o.login)
if (!list.length) {
try {
- const rawRecent = localStorage.getItem('oe_recent')
- if (rawRecent) {
- const recent = JSON.parse(rawRecent)
- if (Array.isArray(recent) && recent.length && typeof recent[0] === 'string') {
- list = recent[0].split(',').map(s => s.trim()).filter(Boolean)
- }
+ const recent = storage.get(STORAGE_KEYS.RECENT_SEARCHES)
+ if (Array.isArray(recent) && recent.length && typeof recent[0] === 'string') {
+ list = recent[0].split(',').map(s => s.trim()).filter(Boolean)
}
} catch (e) {
- console.error('Failed to parse oe_recent from localStorage:', e)
+ console.error('Failed to parse recent searches from storage:', e)
}
}
return list
@@ -305,18 +304,18 @@ export default function ContributorProfilePage() {
// Time-series charting data (Chronological sorting by YYYY-MM)
const chartData = useMemo(() => {
const monthlyBuckets = {}
-
+
filteredContribs.forEach(item => {
const date = new Date(item.created_at)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const yyyymm = `${year}-${month}`
const displayName = date.toLocaleString('default', { month: 'short', year: '2-digit' }) // e.g. "May 26"
-
+
if (!monthlyBuckets[yyyymm]) {
monthlyBuckets[yyyymm] = { yyyymm, name: displayName, PRs: 0, Issues: 0 }
}
-
+
if (item.pull_request) {
monthlyBuckets[yyyymm].PRs++
} else {
@@ -492,9 +491,9 @@ export default function ContributorProfilePage() {
p.isMerged).length} Merged`} accent="var(--blue)" />
i.state === 'closed').length} Closed`} accent="var(--amber)" />
- i.repository_url?.split('/').pop())).size}
+ i.repository_url?.split('/').pop())).size}
sub="distinct repositories"
accent="var(--green)"
/>
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index cffcbd3..ec2195a 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -3,7 +3,7 @@ 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 { storage, STORAGE_KEYS } from '../utils/storage'
const QUICK = ['AOSSIE-Org', 'DjedAlliance', 'StabilityNexus']
export default function HomePage() {
@@ -12,7 +12,8 @@ export default function HomePage() {
const [input, setInput] = useState('')
const [chips, setChips] = useState([])
- const recent = JSON.parse(localStorage.getItem('oe_recent') || '[]')
+ const recent = storage.get(STORAGE_KEYS.RECENT_SEARCHES) || []
+
const addChip = raw => {
const parts = raw.split(/[,+\s]+/).map(s => s.trim()).filter(Boolean)
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 0000000..8d5cd74
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,40 @@
+// src/utils/storage.js
+
+export const STORAGE_KEYS = {
+ PAT: "oe_pat",
+ RATE_LIMIT: "oe_rate_limit",
+ RECENT_SEARCHES: "oe_recent",
+};
+
+export const storage = {
+ get: (key) => {
+ try {
+ const value = localStorage.getItem(key);
+ if (!value) return null;
+ try {
+ return JSON.parse(value);
+ } catch {
+ return value;
+ }
+ } catch (error) {
+ console.error(`Error reading ${key} from localStorage:`, error);
+ return null;
+ }
+ },
+
+ set: (key, value) => {
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ } catch (error) {
+ console.error(`Error writing ${key} to localStorage:`, error);
+ }
+ },
+
+ remove: (key) => {
+ try {
+ localStorage.removeItem(key);
+ } catch (error) {
+ console.error(`Error removing ${key} from localStorage:`, error);
+ }
+ },
+};