Initial commit - Church Music Database

This commit is contained in:
2026-01-27 18:04:50 -06:00
commit d367261867
336 changed files with 103545 additions and 0 deletions

View 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 };

View 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();
}

View 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;

View 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;