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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/components/GoalTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ const RECURRENCE_LABELS: Record<Recurrence, string> = {

export function useGoalTracker() {
const [goals, setGoals] = useState<Goal[]>([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(() => {
if (typeof window !== "undefined") {
try {
return !window.localStorage.getItem("devtrack:swr:goals");
} catch (e) {}
}
return true;
});
const [syncing, setSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
Expand All @@ -62,13 +69,40 @@ export function useGoalTracker() {
const initialLoadDoneRef = useRef<boolean>(false);

const loadGoals = useCallback(async () => {
const cacheKey = "devtrack:swr:goals";

if (typeof window !== "undefined") {
try {
const cached = window.localStorage.getItem(cacheKey);
if (cached) {
const parsed = JSON.parse(cached);
if (parsed && parsed.data) {
setGoals(parsed.data);
if (parsed.timestamp) {
setLastUpdated(new Date(parsed.timestamp));
setMinutesAgo(Math.floor((Date.now() - parsed.timestamp) / 60000));
}
}
}
} catch (e) {
// ignore
}
}

const response = await fetch("/api/goals");
if (!response.ok) {
throw new Error(`Failed to load goals (HTTP ${response.status})`);
}
const data: { goals: Goal[] } = await response.json();
const fetchedGoals = data.goals ?? [];
setGoals(fetchedGoals);

if (typeof window !== "undefined") {
try {
window.localStorage.setItem(cacheKey, JSON.stringify({ data: fetchedGoals, timestamp: Date.now() }));
} catch (e) {}
}

return fetchedGoals;
}, []);

Expand Down
49 changes: 44 additions & 5 deletions src/components/PRMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,39 @@ export default function PRMetrics() {
const [staleThresholdDays, setStaleThresholdDays] = useState(14);

const fetchMetrics = useCallback(() => {
setLoading(true);
setError(null);

const url =
selectedAccount !== null
? `/api/metrics/prs?accountId=${encodeURIComponent(selectedAccount)}&range=${range}`
: `/api/metrics/prs?range=${range}`;

const cacheKey = `devtrack:swr:prs:${selectedAccount || 'default'}:${range}`;
let hasStale = false;

if (typeof window !== "undefined") {
try {
const cached = window.localStorage.getItem(cacheKey);
if (cached) {
const parsed = JSON.parse(cached);
if (parsed && parsed.data) {
setMetrics(parsed.data);
if (parsed.timestamp) {
setLastUpdated(new Date(parsed.timestamp));
setMinutesAgo(Math.floor((Date.now() - parsed.timestamp) / 60000));
}
setLoading(false);
hasStale = true;
}
}
} catch (e) {
// Ignore parsing errors or local storage disabled
}
}

if (!hasStale) {
setLoading(true);
}
setError(null);

fetch(url)
.then((r) => {
if (!r.ok) throw new Error("API error");
Expand All @@ -76,9 +101,23 @@ export default function PRMetrics() {
setMetrics(data);
setLastUpdated(new Date());
setMinutesAgo(0);

if (typeof window !== "undefined") {
try {
window.localStorage.setItem(cacheKey, JSON.stringify({ data, timestamp: Date.now() }));
} catch (e) {
// Ignore quota exceeded or storage disabled
}
}
})
.catch(() => {
if (!hasStale) {
setError("We couldn't load your PR analytics right now. Please try again in a moment.");
}
})
.catch(() => setError("We couldn't load your PR analytics right now. Please try again in a moment."))
.finally(() => setLoading(false));
.finally(() => {
if (!hasStale) setLoading(false);
});
}, [selectedAccount, range]);

useEffect(() => {
Expand Down
52 changes: 47 additions & 5 deletions src/components/StreakTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,37 @@ export function useStreakTracker() {
}, []);

const fetchStreak = useCallback(async () => {
setLoading(true);
const cacheKeyStreak = `devtrack:swr:streak:${selectedAccount || 'default'}`;
const cacheKeyContrib = `devtrack:swr:contrib:${selectedAccount || 'default'}`;
let hasStale = false;

if (typeof window !== "undefined") {
try {
const cachedStreak = window.localStorage.getItem(cacheKeyStreak);
const cachedContrib = window.localStorage.getItem(cacheKeyContrib);
if (cachedStreak && cachedContrib) {
const parsedStreak = JSON.parse(cachedStreak);
const parsedContrib = JSON.parse(cachedContrib);
if (parsedStreak && parsedStreak.data && parsedContrib && parsedContrib.data) {
setData(parsedStreak.data);
setContributionData(parsedContrib.data);
setFreezeDates(parsedStreak.data.freezeDates || []);
if (parsedStreak.timestamp) {
setLastUpdated(new Date(parsedStreak.timestamp));
setMinutesAgo(Math.floor((Date.now() - parsedStreak.timestamp) / 60000));
}
setLoading(false);
hasStale = true;
}
}
} catch (e) {
// Ignore parsing errors or local storage disabled
}
}

if (!hasStale) {
setLoading(true);
}
setError(null);

try {
Expand Down Expand Up @@ -111,13 +141,25 @@ export function useStreakTracker() {
setData(streakData);
setContributionData(contribData);
setFreezeDates(streakData.freezeDates || []);

setLastUpdated(new Date());
setMinutesAgo(0);

if (typeof window !== "undefined") {
try {
window.localStorage.setItem(cacheKeyStreak, JSON.stringify({ data: streakData, timestamp: Date.now() }));
window.localStorage.setItem(cacheKeyContrib, JSON.stringify({ data: contribData, timestamp: Date.now() }));
} catch (e) {
// Ignore quota exceeded or storage disabled
}
}
} catch (err) {
console.error("Failed to fetch streak data:", err);
setError("We couldn't load your streak data right now. Please try again in a moment.");
if (!hasStale) {
setError("We couldn't load your streak data right now. Please try again in a moment.");
}
} finally {
setLoading(false);
setLastUpdated(new Date());
setMinutesAgo(0);
if (!hasStale) setLoading(false);
}
}, [selectedAccount]);

Expand Down