Initial commit - Church Music Database
This commit is contained in:
143
new-site/frontend/src/hooks/useData.js
Normal file
143
new-site/frontend/src/hooks/useData.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Custom hooks for accessing cached data from the store
|
||||
* These hooks provide a clean interface for components to fetch and use data
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import useDataStore from "../stores/dataStore";
|
||||
|
||||
/**
|
||||
* Hook to get all songs with caching
|
||||
* @returns {Object} { songs, loading, error, refetch }
|
||||
*/
|
||||
export function useSongs() {
|
||||
const songs = useDataStore((state) => state.songs);
|
||||
const loading = useDataStore((state) => state.songsLoading);
|
||||
const error = useDataStore((state) => state.songsError);
|
||||
const fetchSongs = useDataStore((state) => state.fetchSongs);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSongs();
|
||||
}, [fetchSongs]);
|
||||
|
||||
return {
|
||||
songs,
|
||||
loading,
|
||||
error,
|
||||
refetch: () => fetchSongs(true),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get a single song by ID
|
||||
* @param {number|string} id - Song ID
|
||||
* @returns {Object} { song, loading, error }
|
||||
*/
|
||||
export function useSong(id) {
|
||||
const songDetails = useDataStore((state) => state.songDetails);
|
||||
const loading = useDataStore((state) => state.songDetailsLoading);
|
||||
const error = useDataStore((state) => state.songDetailsError);
|
||||
const fetchSongDetail = useDataStore((state) => state.fetchSongDetail);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchSongDetail(id);
|
||||
}
|
||||
}, [id, fetchSongDetail]);
|
||||
|
||||
return {
|
||||
song: songDetails[id] || null,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get all worship lists with caching
|
||||
* @returns {Object} { lists, loading, error, refetch }
|
||||
*/
|
||||
export function useLists() {
|
||||
const lists = useDataStore((state) => state.lists);
|
||||
const loading = useDataStore((state) => state.listsLoading);
|
||||
const error = useDataStore((state) => state.listsError);
|
||||
const fetchLists = useDataStore((state) => state.fetchLists);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLists();
|
||||
}, [fetchLists]);
|
||||
|
||||
return {
|
||||
lists,
|
||||
loading,
|
||||
error,
|
||||
refetch: () => fetchLists(true),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get all profiles with caching
|
||||
* @returns {Object} { profiles, loading, error, refetch }
|
||||
*/
|
||||
export function useProfiles() {
|
||||
const profiles = useDataStore((state) => state.profiles);
|
||||
const loading = useDataStore((state) => state.profilesLoading);
|
||||
const error = useDataStore((state) => state.profilesError);
|
||||
const fetchProfiles = useDataStore((state) => state.fetchProfiles);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfiles();
|
||||
}, [fetchProfiles]);
|
||||
|
||||
return {
|
||||
profiles,
|
||||
loading,
|
||||
error,
|
||||
refetch: () => fetchProfiles(true),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get dashboard stats with caching
|
||||
* @returns {Object} { stats, loading, error, refetch }
|
||||
*/
|
||||
export function useStats() {
|
||||
const stats = useDataStore((state) => state.stats);
|
||||
const loading = useDataStore((state) => state.statsLoading);
|
||||
const error = useDataStore((state) => state.statsError);
|
||||
const fetchStats = useDataStore((state) => state.fetchStats);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [fetchStats]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
error,
|
||||
refetch: () => fetchStats(true),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access invalidation functions
|
||||
* Use these after mutations to refresh data
|
||||
* @returns {Object} { invalidateSongs, invalidateLists, invalidateProfiles, invalidateStats, invalidateAll }
|
||||
*/
|
||||
export function useInvalidate() {
|
||||
const invalidateSongs = useDataStore((state) => state.invalidateSongs);
|
||||
const invalidateLists = useDataStore((state) => state.invalidateLists);
|
||||
const invalidateProfiles = useDataStore((state) => state.invalidateProfiles);
|
||||
const invalidateStats = useDataStore((state) => state.invalidateStats);
|
||||
const invalidateAll = useDataStore((state) => state.invalidateAll);
|
||||
|
||||
return {
|
||||
invalidateSongs,
|
||||
invalidateLists,
|
||||
invalidateProfiles,
|
||||
invalidateStats,
|
||||
invalidateAll,
|
||||
};
|
||||
}
|
||||
|
||||
// Export the store itself for direct access when needed
|
||||
export { useDataStore };
|
||||
259
new-site/frontend/src/hooks/useDataFetch.js
Normal file
259
new-site/frontend/src/hooks/useDataFetch.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Custom hooks for data fetching with caching
|
||||
*
|
||||
* These hooks provide a simple interface to fetch data with automatic caching.
|
||||
* They follow the same pattern as React Query / SWR for familiarity.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import useDataStore from "@stores/dataStore";
|
||||
|
||||
/**
|
||||
* Hook to fetch and use songs data
|
||||
* @param {Object} options - { enabled: true, refetchOnMount: false }
|
||||
* @returns {{ songs: Array, loading: boolean, error: string|null, refetch: Function }}
|
||||
*/
|
||||
export function useSongs(options = {}) {
|
||||
const { enabled = true, refetchOnMount = false } = options;
|
||||
|
||||
const songs = useDataStore((state) => state.songs);
|
||||
const loading = useDataStore((state) => state.songsLoading);
|
||||
const error = useDataStore((state) => state.songsError);
|
||||
const fetchSongs = useDataStore((state) => state.fetchSongs);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
fetchSongs(refetchOnMount);
|
||||
}
|
||||
}, [enabled, refetchOnMount, fetchSongs]);
|
||||
|
||||
const refetch = useCallback(() => fetchSongs(true), [fetchSongs]);
|
||||
|
||||
return { songs, loading, error, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and use a single song
|
||||
* @param {string} id - Song ID
|
||||
* @param {Object} options - { enabled: true }
|
||||
* @returns {{ song: Object|null, loading: boolean, refetch: Function }}
|
||||
*/
|
||||
export function useSong(id, options = {}) {
|
||||
const { enabled = true } = options;
|
||||
|
||||
const songDetails = useDataStore((state) => state.songDetails);
|
||||
const loadingMap = useDataStore((state) => state.songDetailsLoading);
|
||||
const fetchSongDetail = useDataStore((state) => state.fetchSongDetail);
|
||||
|
||||
const song = songDetails[id]?.data || null;
|
||||
const loading = loadingMap[id] || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && id) {
|
||||
fetchSongDetail(id);
|
||||
}
|
||||
}, [enabled, id, fetchSongDetail]);
|
||||
|
||||
const refetch = useCallback(
|
||||
() => fetchSongDetail(id, true),
|
||||
[fetchSongDetail, id],
|
||||
);
|
||||
|
||||
return { song, loading, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and use worship lists
|
||||
* @param {Object} options - { enabled: true, refetchOnMount: false }
|
||||
* @returns {{ lists: Array, loading: boolean, error: string|null, refetch: Function }}
|
||||
*/
|
||||
export function useLists(options = {}) {
|
||||
const { enabled = true, refetchOnMount = false } = options;
|
||||
|
||||
const lists = useDataStore((state) => state.lists);
|
||||
const loading = useDataStore((state) => state.listsLoading);
|
||||
const error = useDataStore((state) => state.listsError);
|
||||
const fetchLists = useDataStore((state) => state.fetchLists);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
fetchLists(refetchOnMount);
|
||||
}
|
||||
}, [enabled, refetchOnMount, fetchLists]);
|
||||
|
||||
const refetch = useCallback(() => fetchLists(true), [fetchLists]);
|
||||
|
||||
return { lists, loading, error, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and use songs in a specific list
|
||||
* @param {string} listId - List ID
|
||||
* @param {Object} options - { enabled: true }
|
||||
* @returns {{ songs: Array, loading: boolean, refetch: Function }}
|
||||
*/
|
||||
export function useListSongs(listId, options = {}) {
|
||||
const { enabled = true } = options;
|
||||
|
||||
const listDetails = useDataStore((state) => state.listDetails);
|
||||
const loadingMap = useDataStore((state) => state.listDetailsLoading);
|
||||
const fetchListDetail = useDataStore((state) => state.fetchListDetail);
|
||||
|
||||
const songs = listDetails[listId]?.songs || [];
|
||||
const loading = loadingMap[listId] || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && listId) {
|
||||
fetchListDetail(listId);
|
||||
}
|
||||
}, [enabled, listId, fetchListDetail]);
|
||||
|
||||
const refetch = useCallback(
|
||||
() => fetchListDetail(listId, true),
|
||||
[fetchListDetail, listId],
|
||||
);
|
||||
|
||||
return { songs, loading, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and use profiles
|
||||
* @param {Object} options - { enabled: true, refetchOnMount: false }
|
||||
* @returns {{ profiles: Array, loading: boolean, error: string|null, refetch: Function }}
|
||||
*/
|
||||
export function useProfiles(options = {}) {
|
||||
const { enabled = true, refetchOnMount = false } = options;
|
||||
|
||||
const profiles = useDataStore((state) => state.profiles);
|
||||
const loading = useDataStore((state) => state.profilesLoading);
|
||||
const error = useDataStore((state) => state.profilesError);
|
||||
const fetchProfiles = useDataStore((state) => state.fetchProfiles);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
fetchProfiles(refetchOnMount);
|
||||
}
|
||||
}, [enabled, refetchOnMount, fetchProfiles]);
|
||||
|
||||
const refetch = useCallback(() => fetchProfiles(true), [fetchProfiles]);
|
||||
|
||||
return { profiles, loading, error, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and use stats
|
||||
* @param {Object} options - { enabled: true, refetchOnMount: false }
|
||||
* @returns {{ stats: Object, loading: boolean, refetch: Function }}
|
||||
*/
|
||||
export function useStats(options = {}) {
|
||||
const { enabled = true, refetchOnMount = false } = options;
|
||||
|
||||
const stats = useDataStore((state) => state.stats);
|
||||
const loading = useDataStore((state) => state.statsLoading);
|
||||
const fetchStats = useDataStore((state) => state.fetchStats);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
fetchStats(refetchOnMount);
|
||||
}
|
||||
}, [enabled, refetchOnMount, fetchStats]);
|
||||
|
||||
const refetch = useCallback(() => fetchStats(true), [fetchStats]);
|
||||
|
||||
return { stats, loading, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for searching songs (with debouncing built-in)
|
||||
* @param {string} query - Search query
|
||||
* @param {Object} options - { debounceMs: 300 }
|
||||
* @returns {{ results: Array, loading: boolean }}
|
||||
*/
|
||||
export function useSearch(query, options = {}) {
|
||||
const { debounceMs = 300 } = options;
|
||||
|
||||
const searchSongs = useDataStore((state) => state.searchSongs);
|
||||
const searchCache = useDataStore((state) => state.searchCache);
|
||||
const loading = useDataStore((state) => state.searchLoading);
|
||||
|
||||
const trimmedQuery = query?.trim().toLowerCase() || "";
|
||||
const results = searchCache[trimmedQuery]?.results || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!trimmedQuery) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
searchSongs(trimmedQuery);
|
||||
}, debounceMs);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [trimmedQuery, debounceMs, searchSongs]);
|
||||
|
||||
return { results, loading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get cache mutation functions
|
||||
* @returns Object with cache mutation functions
|
||||
*/
|
||||
export function useDataMutations() {
|
||||
const updateSongInCache = useDataStore((state) => state.updateSongInCache);
|
||||
const addSongToCache = useDataStore((state) => state.addSongToCache);
|
||||
const removeSongFromCache = useDataStore(
|
||||
(state) => state.removeSongFromCache,
|
||||
);
|
||||
const invalidateSongs = useDataStore((state) => state.invalidateSongs);
|
||||
const invalidateSongDetail = useDataStore(
|
||||
(state) => state.invalidateSongDetail,
|
||||
);
|
||||
|
||||
const updateListInCache = useDataStore((state) => state.updateListInCache);
|
||||
const addListToCache = useDataStore((state) => state.addListToCache);
|
||||
const removeListFromCache = useDataStore(
|
||||
(state) => state.removeListFromCache,
|
||||
);
|
||||
const invalidateLists = useDataStore((state) => state.invalidateLists);
|
||||
const invalidateListDetail = useDataStore(
|
||||
(state) => state.invalidateListDetail,
|
||||
);
|
||||
|
||||
const invalidateProfiles = useDataStore((state) => state.invalidateProfiles);
|
||||
const invalidateAll = useDataStore((state) => state.invalidateAll);
|
||||
|
||||
return {
|
||||
// Songs
|
||||
updateSongInCache,
|
||||
addSongToCache,
|
||||
removeSongFromCache,
|
||||
invalidateSongs,
|
||||
invalidateSongDetail,
|
||||
// Lists
|
||||
updateListInCache,
|
||||
addListToCache,
|
||||
removeListFromCache,
|
||||
invalidateLists,
|
||||
invalidateListDetail,
|
||||
// General
|
||||
invalidateProfiles,
|
||||
invalidateAll,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to prefetch data on app mount
|
||||
*/
|
||||
export function usePrefetch() {
|
||||
const prefetch = useDataStore((state) => state.prefetch);
|
||||
|
||||
useEffect(() => {
|
||||
prefetch();
|
||||
}, [prefetch]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get cache status (for debugging)
|
||||
*/
|
||||
export function useCacheStatus() {
|
||||
const getCacheStatus = useDataStore((state) => state.getCacheStatus);
|
||||
return getCacheStatus();
|
||||
}
|
||||
43
new-site/frontend/src/hooks/useLocalStorage.js
Normal file
43
new-site/frontend/src/hooks/useLocalStorage.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
export function useLocalStorage(key, initialValue) {
|
||||
// Get stored value or use initial value
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
// Silent fail - return initial value
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Update localStorage when value changes
|
||||
const setValue = useCallback(
|
||||
(value) => {
|
||||
try {
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
// Silent fail - localStorage might be full or disabled
|
||||
}
|
||||
},
|
||||
[key, storedValue],
|
||||
);
|
||||
|
||||
// Remove from localStorage
|
||||
const removeValue = useCallback(() => {
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
setStoredValue(initialValue);
|
||||
} catch (error) {
|
||||
// Silent fail
|
||||
}
|
||||
}, [key, initialValue]);
|
||||
|
||||
return [storedValue, setValue, removeValue];
|
||||
}
|
||||
|
||||
export default useLocalStorage;
|
||||
64
new-site/frontend/src/hooks/useMediaQuery.js
Normal file
64
new-site/frontend/src/hooks/useMediaQuery.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useMediaQuery(query) {
|
||||
const [matches, setMatches] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(query);
|
||||
|
||||
const handleChange = (event) => {
|
||||
setMatches(event.matches);
|
||||
};
|
||||
|
||||
// Add listener
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
mediaQuery.addListener(handleChange);
|
||||
}
|
||||
|
||||
// Set initial value
|
||||
setMatches(mediaQuery.matches);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (mediaQuery.removeEventListener) {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
} else {
|
||||
mediaQuery.removeListener(handleChange);
|
||||
}
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Predefined breakpoints
|
||||
export function useIsMobile() {
|
||||
return useMediaQuery("(max-width: 767px)");
|
||||
}
|
||||
|
||||
export function useIsTablet() {
|
||||
return useMediaQuery("(min-width: 768px) and (max-width: 1023px)");
|
||||
}
|
||||
|
||||
export function useIsDesktop() {
|
||||
return useMediaQuery("(min-width: 1024px)");
|
||||
}
|
||||
|
||||
export function useBreakpoint() {
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
const isTablet = useMediaQuery("(min-width: 768px) and (max-width: 1023px)");
|
||||
|
||||
if (isMobile) return "mobile";
|
||||
if (isTablet) return "tablet";
|
||||
return "desktop";
|
||||
}
|
||||
|
||||
export default useMediaQuery;
|
||||
Reference in New Issue
Block a user