2024-03-01 20:42:21 +01:00
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
2026-01-28 16:51:27 +08:00
|
|
|
import directoriesData from '../data/5chan-directories.json';
|
2024-03-01 20:42:21 +01:00
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export interface DirectoriesMetadata {
|
2024-05-17 14:55:41 +02:00
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
createdAt: number;
|
|
|
|
|
updatedAt: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export interface DirectoryCommunity {
|
2024-03-01 20:42:21 +01:00
|
|
|
title?: string;
|
|
|
|
|
address: string;
|
2025-11-03 22:06:18 +01:00
|
|
|
nsfw?: boolean;
|
2024-03-01 20:42:21 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export interface DirectoriesData {
|
2025-11-03 22:24:31 +01:00
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
createdAt: number;
|
|
|
|
|
updatedAt: number;
|
2026-01-28 16:51:27 +08:00
|
|
|
communities: DirectoryCommunity[];
|
2025-11-03 22:24:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export interface DirectoriesState {
|
|
|
|
|
communities: DirectoryCommunity[];
|
2025-02-20 15:51:29 +01:00
|
|
|
loading: boolean;
|
|
|
|
|
error: Error | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 21:27:18 +08:00
|
|
|
const GITHUB_URL = 'https://raw.githubusercontent.com/bitsocialhq/lists/master/5chan-directories.json';
|
2026-01-28 16:51:27 +08:00
|
|
|
const LOCALSTORAGE_KEY = '5chan-directories-cache';
|
|
|
|
|
const LOCALSTORAGE_TIMESTAMP_KEY = '5chan-directories-cache-timestamp';
|
2025-11-03 22:24:31 +01:00
|
|
|
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
let cacheCommunities: DirectoryCommunity[] | null = null;
|
|
|
|
|
let cacheMetadata: DirectoriesMetadata | null = null;
|
2026-02-15 14:40:48 +08:00
|
|
|
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
2024-03-01 20:42:21 +01:00
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
const getFromLocalStorage = (): DirectoriesData | null => {
|
2025-11-03 22:24:31 +01:00
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(LOCALSTORAGE_KEY);
|
|
|
|
|
const timestamp = localStorage.getItem(LOCALSTORAGE_TIMESTAMP_KEY);
|
|
|
|
|
if (cached && timestamp) {
|
|
|
|
|
const age = Date.now() - parseInt(timestamp, 10);
|
|
|
|
|
if (age < CACHE_MAX_AGE_MS) {
|
|
|
|
|
return JSON.parse(cached);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Failed to read from localStorage:', e);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
const saveToLocalStorage = (data: DirectoriesData) => {
|
2025-11-03 22:24:31 +01:00
|
|
|
try {
|
|
|
|
|
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(data));
|
|
|
|
|
localStorage.setItem(LOCALSTORAGE_TIMESTAMP_KEY, Date.now().toString());
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Failed to save to localStorage:', e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-15 14:40:48 +08:00
|
|
|
const fetchDirectoriesFromGitHub = async (): Promise<DirectoriesData> => {
|
|
|
|
|
const response = await fetch(GITHUB_URL, { cache: 'no-cache' });
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
2025-11-03 22:24:31 +01:00
|
|
|
}
|
2026-02-15 14:40:48 +08:00
|
|
|
const data = await response.json();
|
|
|
|
|
// Save successful fetch to localStorage
|
|
|
|
|
saveToLocalStorage(data);
|
|
|
|
|
return data;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchDirectoriesFromGitHubDeduped = async (): Promise<DirectoriesData> => {
|
|
|
|
|
if (!inFlightGitHubFetch) {
|
|
|
|
|
inFlightGitHubFetch = fetchDirectoriesFromGitHub().finally(() => {
|
|
|
|
|
inFlightGitHubFetch = null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return inFlightGitHubFetch;
|
2025-11-03 22:24:31 +01:00
|
|
|
};
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export const useDirectories = () => {
|
2025-11-25 22:24:48 +01:00
|
|
|
// Use vendored data as initial state to prevent theme flash on first load
|
|
|
|
|
// This ensures NSFW status is known synchronously before first render
|
2026-01-28 16:51:27 +08:00
|
|
|
const [state, setState] = useState<DirectoriesState>({
|
|
|
|
|
communities: (directoriesData as DirectoriesData).communities,
|
2025-02-20 15:51:29 +01:00
|
|
|
loading: true,
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
2024-03-01 20:42:21 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-11-04 14:59:22 +01:00
|
|
|
let isMounted = true;
|
2026-02-15 14:40:48 +08:00
|
|
|
const hydrateCommunities = (data: DirectoriesData) => {
|
|
|
|
|
cacheCommunities = data.communities;
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setState({
|
|
|
|
|
communities: data.communities,
|
|
|
|
|
loading: false,
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-04 14:59:22 +01:00
|
|
|
|
2024-03-01 20:42:21 +01:00
|
|
|
(async () => {
|
2026-02-15 14:40:48 +08:00
|
|
|
if (cacheCommunities) {
|
|
|
|
|
setState({
|
|
|
|
|
communities: cacheCommunities,
|
|
|
|
|
loading: false,
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2025-11-03 22:24:31 +01:00
|
|
|
// Check localStorage first
|
|
|
|
|
const cachedData = getFromLocalStorage();
|
|
|
|
|
if (cachedData) {
|
2026-02-15 14:40:48 +08:00
|
|
|
hydrateCommunities(cachedData);
|
2025-11-03 22:24:31 +01:00
|
|
|
}
|
2026-02-15 14:40:48 +08:00
|
|
|
}
|
2025-02-20 15:51:29 +01:00
|
|
|
|
2026-02-15 14:40:48 +08:00
|
|
|
try {
|
|
|
|
|
// Always attempt a background refresh from GitHub to pick up list updates
|
|
|
|
|
const directories = await fetchDirectoriesFromGitHubDeduped();
|
|
|
|
|
hydrateCommunities(directories);
|
2024-03-01 20:42:21 +01:00
|
|
|
} catch (e) {
|
2026-02-15 14:40:48 +08:00
|
|
|
console.warn('Failed to fetch directories from GitHub:', e);
|
|
|
|
|
// Only fall back if we don't already have memory/localStorage data
|
|
|
|
|
if (!cacheCommunities) {
|
|
|
|
|
hydrateCommunities(directoriesData as DirectoriesData);
|
2025-11-04 14:59:22 +01:00
|
|
|
}
|
2024-03-01 20:42:21 +01:00
|
|
|
}
|
|
|
|
|
})();
|
2025-11-04 14:59:22 +01:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
};
|
2024-03-01 20:42:21 +01:00
|
|
|
}, []);
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
// Always prefer cacheCommunities (module-level, stable reference) when available
|
|
|
|
|
// Only use state.communities during initial load before cache is populated
|
2025-11-07 12:49:58 +01:00
|
|
|
// This ensures a stable reference for memoization in consuming hooks
|
2026-01-28 16:51:27 +08:00
|
|
|
return cacheCommunities || state.communities;
|
2025-02-20 15:51:29 +01:00
|
|
|
};
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export const useDirectoriesState = () => {
|
2025-11-25 22:24:48 +01:00
|
|
|
// Use vendored data as fallback to prevent theme flash on first load
|
2026-01-28 16:51:27 +08:00
|
|
|
const [state, setState] = useState<DirectoriesState>({
|
|
|
|
|
communities: cacheCommunities || (directoriesData as DirectoriesData).communities,
|
|
|
|
|
loading: !cacheCommunities,
|
2025-02-20 15:51:29 +01:00
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-11-04 14:59:22 +01:00
|
|
|
let isMounted = true;
|
2026-02-15 14:40:48 +08:00
|
|
|
const hydrateCommunities = (data: DirectoriesData) => {
|
|
|
|
|
cacheCommunities = data.communities;
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setState({
|
|
|
|
|
communities: data.communities,
|
|
|
|
|
loading: false,
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-04 14:59:22 +01:00
|
|
|
|
2025-02-20 15:51:29 +01:00
|
|
|
(async () => {
|
2026-02-15 14:40:48 +08:00
|
|
|
if (cacheCommunities) {
|
|
|
|
|
setState({
|
|
|
|
|
communities: cacheCommunities,
|
|
|
|
|
loading: false,
|
|
|
|
|
error: null,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2025-11-03 22:24:31 +01:00
|
|
|
// Check localStorage first
|
|
|
|
|
const cachedData = getFromLocalStorage();
|
|
|
|
|
if (cachedData) {
|
2026-02-15 14:40:48 +08:00
|
|
|
hydrateCommunities(cachedData);
|
2025-11-03 22:24:31 +01:00
|
|
|
}
|
2026-02-15 14:40:48 +08:00
|
|
|
}
|
2025-02-20 15:51:29 +01:00
|
|
|
|
2026-02-15 14:40:48 +08:00
|
|
|
try {
|
|
|
|
|
// Always attempt a background refresh from GitHub to pick up list updates
|
|
|
|
|
const directories = await fetchDirectoriesFromGitHubDeduped();
|
|
|
|
|
hydrateCommunities(directories);
|
2025-02-20 15:51:29 +01:00
|
|
|
} catch (e) {
|
2026-02-15 14:40:48 +08:00
|
|
|
console.warn('Failed to fetch directories from GitHub:', e);
|
|
|
|
|
// Only fall back if we don't already have memory/localStorage data
|
|
|
|
|
if (!cacheCommunities) {
|
|
|
|
|
hydrateCommunities(directoriesData as DirectoriesData);
|
2025-11-04 14:59:22 +01:00
|
|
|
}
|
2025-02-20 15:51:29 +01:00
|
|
|
}
|
|
|
|
|
})();
|
2025-11-04 14:59:22 +01:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
};
|
2025-02-20 15:51:29 +01:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return state;
|
2024-03-01 20:42:21 +01:00
|
|
|
};
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export const useDirectoryAddresses = () => {
|
|
|
|
|
const directories = useDirectories();
|
|
|
|
|
return useMemo(() => directories.map((community) => community.address), [directories]);
|
2024-03-01 20:42:21 +01:00
|
|
|
};
|
|
|
|
|
|
2026-01-28 16:51:27 +08:00
|
|
|
export const useDirectoriesMetadata = () => {
|
|
|
|
|
const [metadata, setMetadata] = useState<DirectoriesMetadata | null>(null);
|
2024-05-17 14:55:41 +02:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-11-04 14:59:22 +01:00
|
|
|
let isMounted = true;
|
2026-02-15 14:40:48 +08:00
|
|
|
const hydrateMetadata = (data: DirectoriesData) => {
|
|
|
|
|
const nextMetadata: DirectoriesMetadata = {
|
|
|
|
|
title: data.title,
|
|
|
|
|
description: data.description,
|
|
|
|
|
createdAt: data.createdAt,
|
|
|
|
|
updatedAt: data.updatedAt,
|
|
|
|
|
};
|
|
|
|
|
cacheMetadata = nextMetadata;
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setMetadata(nextMetadata);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-04 14:59:22 +01:00
|
|
|
|
2024-05-17 14:55:41 +02:00
|
|
|
(async () => {
|
2026-02-15 14:40:48 +08:00
|
|
|
if (cacheMetadata) {
|
|
|
|
|
setMetadata(cacheMetadata);
|
|
|
|
|
} else {
|
2025-11-03 22:24:31 +01:00
|
|
|
// Check localStorage first
|
|
|
|
|
const cachedData = getFromLocalStorage();
|
|
|
|
|
if (cachedData) {
|
2026-02-15 14:40:48 +08:00
|
|
|
hydrateMetadata(cachedData);
|
2025-11-03 22:24:31 +01:00
|
|
|
}
|
2026-02-15 14:40:48 +08:00
|
|
|
}
|
2025-11-03 22:24:31 +01:00
|
|
|
|
2026-02-15 14:40:48 +08:00
|
|
|
try {
|
|
|
|
|
// Always attempt a background refresh from GitHub to pick up metadata updates
|
|
|
|
|
const directories = await fetchDirectoriesFromGitHubDeduped();
|
|
|
|
|
hydrateMetadata(directories);
|
2024-05-17 14:55:41 +02:00
|
|
|
} catch (e) {
|
2026-02-15 14:40:48 +08:00
|
|
|
console.warn('Failed to fetch directory metadata from GitHub:', e);
|
|
|
|
|
// Only fall back if we don't already have memory/localStorage data
|
|
|
|
|
if (!cacheMetadata) {
|
|
|
|
|
hydrateMetadata(directoriesData as DirectoriesData);
|
2025-11-04 14:59:22 +01:00
|
|
|
}
|
2024-05-17 14:55:41 +02:00
|
|
|
}
|
|
|
|
|
})();
|
2025-11-04 14:59:22 +01:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
};
|
2024-05-17 14:55:41 +02:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return cacheMetadata || metadata;
|
|
|
|
|
};
|