Skip to content
Merged
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
65 changes: 34 additions & 31 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use client';

import { useEffect, useState, useCallback } from 'react';
import { useStorageValue } from './use-storage-value';

const STORAGE_KEY = 'gittensor.settings';
const EVENT_NAME = 'settings-changed';

export type IssueDefaultState = 'all' | 'open' | 'completed' | 'not_planned' | 'closed_other';
export type ContentDisplayMode = 'modal' | 'accordion' | 'side';
Expand Down Expand Up @@ -38,52 +40,53 @@ export const DEFAULT_SETTINGS: AppSettings = {
layout: 'sidebar',
};

function readStorage(): AppSettings {
function parse(raw: string | null): AppSettings {
if (!raw) return DEFAULT_SETTINGS;
const parsed = JSON.parse(raw) as Partial<AppSettings>;
return {
...DEFAULT_SETTINGS,
...parsed,
layout: parsed.layout === 'top-nav' || parsed.layout === 'sidebar'
? parsed.layout
: DEFAULT_SETTINGS.layout,
};
Comment thread
MkDev11 marked this conversation as resolved.
}

function serialize(s: AppSettings): string {
return JSON.stringify(s);
}

function readFresh(): AppSettings {
if (typeof window === 'undefined') return DEFAULT_SETTINGS;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULT_SETTINGS;
const parsed = JSON.parse(raw) as Partial<AppSettings>;
return {
...DEFAULT_SETTINGS,
...parsed,
layout: parsed.layout === 'top-nav' || parsed.layout === 'sidebar'
? parsed.layout
: DEFAULT_SETTINGS.layout,
};
return parse(localStorage.getItem(STORAGE_KEY));
} catch {
return DEFAULT_SETTINGS;
}
}

function writeStorage(s: AppSettings) {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
window.dispatchEvent(new Event('settings-changed'));
}

export function useSettings() {
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
const [settings, setSettings] = useStorageValue<AppSettings>(
STORAGE_KEY,
parse,
serialize,
DEFAULT_SETTINGS,
EVENT_NAME,
);
const [hydrated, setHydrated] = useState(false);

useEffect(() => {
setSettings(readStorage());
setHydrated(true);
const handler = () => setSettings(readStorage());
window.addEventListener('settings-changed', handler);
window.addEventListener('storage', handler);
return () => {
window.removeEventListener('settings-changed', handler);
window.removeEventListener('storage', handler);
};
}, []);

const update = useCallback(<K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
const next = { ...readStorage(), [key]: value };
writeStorage(next);
}, []);
const update = useCallback(
<K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
setSettings({ ...readFresh(), [key]: value });
},
[setSettings],
);

const reset = useCallback(() => writeStorage(DEFAULT_SETTINGS), []);
const reset = useCallback(() => setSettings(DEFAULT_SETTINGS), [setSettings]);

return { settings, update, reset, hydrated };
}
Expand Down
64 changes: 33 additions & 31 deletions src/lib/tracked-miners.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
'use client';

import { useEffect, useState, useCallback } from 'react';
import { useCallback } from 'react';
import { useStorageValue } from './use-storage-value';

const STORAGE_KEY = 'gittensor.trackedMiners';
const EVENT_NAME = 'tracked-miners-changed';
const EMPTY: Set<string> = new Set();

function readStorage(): Set<string> {
function parse(raw: string | null): Set<string> {
if (!raw) return new Set();
const arr = JSON.parse(raw);
return new Set(Array.isArray(arr) ? arr : []);
}

function serialize(set: Set<string>): string {
return JSON.stringify(Array.from(set));
}

function readFresh(): Set<string> {
if (typeof window === 'undefined') return new Set();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return new Set();
const arr = JSON.parse(raw);
return new Set(Array.isArray(arr) ? arr : []);
return parse(localStorage.getItem(STORAGE_KEY));
} catch {
return new Set();
}
}

function writeStorage(set: Set<string>) {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(set)));
window.dispatchEvent(new Event('tracked-miners-changed'));
}

export function useTrackedMiners() {
const [tracked, setTracked] = useState<Set<string>>(new Set());

useEffect(() => {
setTracked(readStorage());
const handler = () => setTracked(readStorage());
window.addEventListener('tracked-miners-changed', handler);
window.addEventListener('storage', handler);
return () => {
window.removeEventListener('tracked-miners-changed', handler);
window.removeEventListener('storage', handler);
};
}, []);

const toggle = useCallback((id: string) => {
const next = new Set(readStorage());
if (next.has(id)) next.delete(id);
else next.add(id);
writeStorage(next);
}, []);
const [tracked, setTracked] = useStorageValue<Set<string>>(
STORAGE_KEY,
parse,
serialize,
EMPTY,
EVENT_NAME,
);

const toggle = useCallback(
(id: string) => {
const next = readFresh();
if (next.has(id)) next.delete(id);
else next.add(id);
setTracked(next);
},
[setTracked],
);

return { tracked, toggle };
}
75 changes: 40 additions & 35 deletions src/lib/tracked-repos.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use client';

import { useEffect, useState, useCallback } from 'react';
import { useCallback } from 'react';
import { useStorageValue } from './use-storage-value';

const STORAGE_KEY = 'gittensor.trackedRepos';
const EVENT_NAME = 'tracked-repos-changed';
const EMPTY: Set<string> = new Set();

function repoKey(fullName: string): string {
return fullName.trim().toLowerCase();
Expand All @@ -20,51 +23,53 @@ function dedupe(names: string[]): Set<string> {
return new Set(byKey.values());
}

function readStorage(): Set<string> {
function parse(raw: string | null): Set<string> {
if (!raw) return new Set();
const arr = JSON.parse(raw);
return dedupe(Array.isArray(arr) ? arr : []);
}

function serialize(set: Set<string>): string {
return JSON.stringify(Array.from(set));
}

function readFresh(): Set<string> {
if (typeof window === 'undefined') return new Set();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return new Set();
const arr = JSON.parse(raw);
return dedupe(Array.isArray(arr) ? arr : []);
return parse(localStorage.getItem(STORAGE_KEY));
} catch {
return new Set();
}
}

function writeStorage(set: Set<string>) {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(set)));
window.dispatchEvent(new Event('tracked-repos-changed'));
}

export function useTrackedRepos() {
const [tracked, setTracked] = useState<Set<string>>(new Set());

useEffect(() => {
setTracked(readStorage());
const handler = () => setTracked(readStorage());
window.addEventListener('tracked-repos-changed', handler);
window.addEventListener('storage', handler);
return () => {
window.removeEventListener('tracked-repos-changed', handler);
window.removeEventListener('storage', handler);
};
}, []);
const [tracked, setTracked] = useStorageValue<Set<string>>(
STORAGE_KEY,
parse,
serialize,
EMPTY,
EVENT_NAME,
);

const toggle = useCallback((fullName: string) => {
const key = repoKey(fullName);
if (!key) return;
const next = readStorage();
const existing = Array.from(next).find((name) => repoKey(name) === key);
if (existing) next.delete(existing);
else next.add(fullName.trim());
writeStorage(next);
}, []);
const toggle = useCallback(
(fullName: string) => {
const key = repoKey(fullName);
if (!key) return;
const next = readFresh();
const existing = Array.from(next).find((name) => repoKey(name) === key);
if (existing) next.delete(existing);
else next.add(fullName.trim());
setTracked(next);
},
[setTracked],
);

const clear = useCallback(() => writeStorage(new Set()), []);
const clear = useCallback(() => setTracked(new Set()), [setTracked]);

const setMany = useCallback((names: string[]) => writeStorage(dedupe(names)), []);
const setMany = useCallback(
(names: string[]) => setTracked(dedupe(names)),
[setTracked],
);

return { tracked, toggle, clear, setMany };
}
Expand Down
54 changes: 54 additions & 0 deletions src/lib/use-storage-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import { useEffect, useState, useCallback, useRef } from 'react';

export function useStorageValue<T>(
key: string,
parser: (raw: string | null) => T,
serializer: (value: T) => string,
defaultValue: T,
eventName?: string,
): [T, (value: T) => void] {
const [value, setValue] = useState<T>(defaultValue);
const parserRef = useRef(parser);
const serializerRef = useRef(serializer);
const defaultRef = useRef(defaultValue);
parserRef.current = parser;
serializerRef.current = serializer;
defaultRef.current = defaultValue;

useEffect(() => {
const read = (): T => {
if (typeof window === 'undefined') return defaultRef.current;
try {
return parserRef.current(localStorage.getItem(key));
} catch {
return defaultRef.current;
}
};
setValue(read());
const handler = () => setValue(read());
if (eventName) window.addEventListener(eventName, handler);
window.addEventListener('storage', handler);
return () => {
if (eventName) window.removeEventListener(eventName, handler);
window.removeEventListener('storage', handler);
};
}, [key, eventName]);

const write = useCallback(
(next: T) => {
if (typeof window === 'undefined') return;
localStorage.setItem(key, serializerRef.current(next));
// Local fallback for the no-eventName case (the native `storage` event
// doesn't fire same-tab). When eventName is set, the dispatched event's
// synchronous handler will overwrite this with the parsed disk value —
// matching the original "state = parse(serialize(x))" semantics.
setValue(next);
if (eventName) window.dispatchEvent(new Event(eventName));
},
[key, eventName],
);

return [value, write];
}