This commit is contained in:
Víctor Losada Hernández
2026-09-02 13:15:01 +02:00
parent 8f1c0eac92
commit 793a1a96d2
@@ -13,7 +13,7 @@ const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha'; const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc'; const DEFAULT_SORT_DIR = 'asc';
const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navItems = <></>, reportError = null, query }) => { const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navItems = <></>, reportError = null, query })=>{
const [filterString, setFilterString] = useState(query?.filter || ''); const [filterString, setFilterString] = useState(query?.filter || '');
const [filterTags, setFilterTags] = useState([]); const [filterTags, setFilterTags] = useState([]);
const [sortType, setSortType] = useState(query?.sort || null); const [sortType, setSortType] = useState(query?.sort || null);
@@ -24,27 +24,27 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
const sortTypeRef = useRef(sortType); const sortTypeRef = useRef(sortType);
const sortDirRef = useRef(sortDir); const sortDirRef = useRef(sortDir);
useEffect(() => { useEffect(()=>{
groupVisibilityRef.current = groupVisibility; groupVisibilityRef.current = groupVisibility;
}, [groupVisibility]); }, [groupVisibility]);
useEffect(() => { useEffect(()=>{
sortTypeRef.current = sortType; sortTypeRef.current = sortType;
}, [sortType]); }, [sortType]);
useEffect(() => { useEffect(()=>{
sortDirRef.current = sortDir; sortDirRef.current = sortDir;
}, [sortDir]); }, [sortDir]);
useEffect(() => { useEffect(()=>{
window.onbeforeunload = saveToLocalStorage; window.onbeforeunload = saveToLocalStorage;
if (typeof window !== 'undefined') { if(typeof window !== 'undefined') {
const newSortType = sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE); const newSortType = sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE);
const newSortDir = sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR); const newSortDir = sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR);
updateUrl(filterString, newSortType, newSortDir); updateUrl(filterString, newSortType, newSortDir);
const namedBrewCollection = brewCollection.reduce((visibility, brewGroup) => { const namedBrewCollection = brewCollection.reduce((visibility, brewGroup)=>{
visibility[brewGroup.class] = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true') == 'true'; visibility[brewGroup.class] = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true') == 'true';
return visibility; return visibility;
}, {}); }, {});
@@ -54,29 +54,29 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
setSortDir(newSortDir); setSortDir(newSortDir);
} }
return () => { return ()=>{
window.onbeforeunload = null; window.onbeforeunload = null;
}; };
}, []); }, []);
const saveToLocalStorage = () => { const saveToLocalStorage = ()=>{
brewCollection.forEach((brewGroup) => { brewCollection.forEach((brewGroup)=>{
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${groupVisibilityRef.current[brewGroup.class]}`); localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${groupVisibilityRef.current[brewGroup.class]}`);
}); });
localStorage.setItem(USERPAGE_SORT_TYPE, sortTypeRef.current); localStorage.setItem(USERPAGE_SORT_TYPE, sortTypeRef.current);
localStorage.setItem(USERPAGE_SORT_DIR, sortDirRef.current); localStorage.setItem(USERPAGE_SORT_DIR, sortDirRef.current);
}; };
const renderBrews = (brews) => { const renderBrews = (brews)=>{
if (!brews || !brews.length) return <div className='noBrews'>No Brews.</div>; if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
return _.map(brews, (brew, idx) => { return _.map(brews, (brew, idx)=>{
return ( return (
<BrewItem <BrewItem
brew={brew} brew={brew}
key={idx} key={idx}
reportError={reportError} reportError={reportError}
updateListFilter={(tag) => { updateListFilter={(tag)=>{
updateUrl(filterString, sortType, sortDir, tag); updateUrl(filterString, sortType, sortDir, tag);
}} }}
/> />
@@ -84,31 +84,31 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
}); });
}; };
const sortBrewOrder = (brew) => { const sortBrewOrder = (brew)=>{
const title = brew.title || 'No Title'; const title = brew.title || 'No Title';
const mapping = { const mapping = {
'alpha': _.deburr(title.trim().toLowerCase()), 'alpha' : _.deburr(title.trim().toLowerCase()),
'created': moment(brew.createdAt).format(), 'created' : moment(brew.createdAt).format(),
'updated': moment(brew.updatedAt).format(), 'updated' : moment(brew.updatedAt).format(),
'views': brew.views, 'views' : brew.views,
'latest': moment(brew.lastViewed).format(), 'latest' : moment(brew.lastViewed).format(),
}; };
return mapping[sortType]; return mapping[sortType];
}; };
const handleSortOptionChange = (event) => { const handleSortOptionChange = (event)=>{
updateUrl(filterString, event.target.value, sortDir); updateUrl(filterString, event.target.value, sortDir);
setSortType(event.target.value); setSortType(event.target.value);
}; };
const handleSortDirChange = (event) => { const handleSortDirChange = (event)=>{
const newDir = sortDir == 'asc' ? 'desc' : 'asc'; const newDir = sortDir == 'asc' ? 'desc' : 'asc';
updateUrl(filterString, sortType, newDir); updateUrl(filterString, sortType, newDir);
setSortDir(newDir); setSortDir(newDir);
}; };
const renderSortOption = (sortTitle, sortValue) => { const renderSortOption = (sortTitle, sortValue)=>{
return ( return (
<div className={`sort-option ${sortType == sortValue ? 'active' : ''}`}> <div className={`sort-option ${sortType == sortValue ? 'active' : ''}`}>
<button value={`${sortValue}`} onClick={sortType == sortValue ? handleSortDirChange : handleSortOptionChange}> <button value={`${sortValue}`} onClick={sortType == sortValue ? handleSortDirChange : handleSortOptionChange}>
@@ -119,13 +119,13 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
); );
}; };
const handleFilterTextChange = (e) => { const handleFilterTextChange = (e)=>{
setFilterString(e.target.value); setFilterString(e.target.value);
updateUrl(e.target.value, sortType, sortDir); updateUrl(e.target.value, sortType, sortDir);
return; return;
}; };
const updateUrl = (filterTerm, sortType, sortDir, filterTag = '') => { const updateUrl = (filterTerm, sortType, sortDir, filterTag = '')=>{
const url = new URL(window.location.href); const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search); const urlParams = new URLSearchParams(url.search);
@@ -133,39 +133,39 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
urlParams.set('dir', sortDir); urlParams.set('dir', sortDir);
let filterTags = urlParams.getAll('tag'); let filterTags = urlParams.getAll('tag');
if (filterTag != '') { if(filterTag != '') {
if ( if(
filterTags.findIndex((tag) => { filterTags.findIndex((tag)=>{
return tag.toLowerCase() == filterTag.toLowerCase(); return tag.toLowerCase() == filterTag.toLowerCase();
}) == -1 }) == -1
) { ) {
filterTags.push(filterTag); filterTags.push(filterTag);
} else { } else {
filterTags = filterTags.filter((tag) => { filterTags = filterTags.filter((tag)=>{
return tag.toLowerCase() != filterTag.toLowerCase(); return tag.toLowerCase() != filterTag.toLowerCase();
}); });
} }
} }
urlParams.delete('tag'); urlParams.delete('tag');
// Add tags to URL in the order they were clicked // Add tags to URL in the order they were clicked
filterTags.forEach((tag) => { filterTags.forEach((tag)=>{
urlParams.append('tag', tag); urlParams.append('tag', tag);
}); });
// Sort tags before updating state // Sort tags before updating state
filterTags.sort((a, b) => { filterTags.sort((a, b)=>{
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase()); return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
}); });
setFilterTags(filterTags); setFilterTags(filterTags);
if (!filterTerm) urlParams.delete('filter'); if(!filterTerm) urlParams.delete('filter');
else urlParams.set('filter', filterTerm); else urlParams.set('filter', filterTerm);
url.search = urlParams; url.search = urlParams;
window.history.replaceState(null, null, url); window.history.replaceState(null, null, url);
}; };
const renderFilterOption = () => { const renderFilterOption = ()=>{
return ( return (
<div className='filter-option'> <div className='filter-option'>
<label> <label>
@@ -176,17 +176,17 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
); );
}; };
const renderTagsOptions = () => { const renderTagsOptions = ()=>{
if (filterTags?.length == 0) return; if(filterTags?.length == 0) return;
return ( return (
<div className='tags-container'> <div className='tags-container'>
{_.map(filterTags, (tag, idx) => { {_.map(filterTags, (tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/); const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return ( return (
<span <span
key={idx} key={idx}
className={matches[1]} className={matches[1]}
onClick={() => { onClick={()=>{
updateUrl(filterString, sortType, sortDir, tag); updateUrl(filterString, sortType, sortDir, tag);
}}> }}>
{matches[2]} {matches[2]}
@@ -197,7 +197,7 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
); );
}; };
const renderSortOptions = () => { const renderSortOptions = ()=>{
return ( return (
<div className='sort-container'> <div className='sort-container'>
<h6>Sort by :</h6> <h6>Sort by :</h6>
@@ -212,10 +212,10 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
); );
}; };
const getSortedBrews = (brews) => { const getSortedBrews = (brews)=>{
const testString = _.deburr(filterString).toLowerCase(); const testString = _.deburr(filterString).toLowerCase();
brews = _.filter(brews, (brew) => { brews = _.filter(brews, (brew)=>{
// Filter by user entered text // Filter by user entered text
const brewStrings = _.deburr([brew.title, brew.description, brew.tags].join('\n').toLowerCase()); const brewStrings = _.deburr([brew.title, brew.description, brew.tags].join('\n').toLowerCase());
@@ -223,12 +223,12 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
// Filter by user selected tags // Filter by user selected tags
let filterTagTest = true; let filterTagTest = true;
if (filterTags.length > 0) { if(filterTags.length > 0) {
filterTagTest = filterTagTest =
Array.isArray(brew.tags) && Array.isArray(brew.tags) &&
filterTags?.every((tag) => { filterTags?.every((tag)=>{
return ( return (
brew.tags.findIndex((brewTag) => { brew.tags.findIndex((brewTag)=>{
return brewTag.toLowerCase() == tag.toLowerCase(); return brewTag.toLowerCase() == tag.toLowerCase();
}) >= 0 }) >= 0
); );
@@ -240,29 +240,29 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
return _.orderBy( return _.orderBy(
brews, brews,
(brew) => { (brew)=>{
return sortBrewOrder(brew); return sortBrewOrder(brew);
}, },
sortDir, sortDir,
); );
}; };
const sortedBrewCollection = useMemo(() => { const sortedBrewCollection = useMemo(()=>{
return brewCollection.map((brewGroup) => ({ ...brewGroup, brews: getSortedBrews(brewGroup.brews) })); return brewCollection.map((brewGroup)=>({ ...brewGroup, brews: getSortedBrews(brewGroup.brews) }));
}, [brewCollection, filterString, filterTags, sortType, sortDir]); }, [brewCollection, filterString, filterTags, sortType, sortDir]);
const toggleBrewCollectionState = (brewGroupClass) => { const toggleBrewCollectionState = (brewGroupClass)=>{
setGroupVisibility((prevVisibility) => ({ ...prevVisibility, [brewGroupClass]: !prevVisibility[brewGroupClass] })); setGroupVisibility((prevVisibility)=>({ ...prevVisibility, [brewGroupClass]: !prevVisibility[brewGroupClass] }));
}; };
const renderBrewCollection = (brewCollection) => { const renderBrewCollection = (brewCollection)=>{
if (brewCollection.length === 0) if(brewCollection.length === 0)
return ( return (
<div className='brewCollection'> <div className='brewCollection'>
<h1>No Brews</h1> <h1>No Brews</h1>
</div> </div>
); );
return _.map(brewCollection, (brewGroup, idx) => { return _.map(brewCollection, (brewGroup, idx)=>{
const sortedBrewGroup = sortedBrewCollection[idx]; const sortedBrewGroup = sortedBrewCollection[idx];
const visible = groupVisibility[brewGroup.class]; const visible = groupVisibility[brewGroup.class];
@@ -270,7 +270,7 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
<div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}> <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
<h1 <h1
className={visible ? 'active' : 'inactive'} className={visible ? 'active' : 'inactive'}
onClick={() => { onClick={()=>{
toggleBrewCollectionState(brewGroup.class); toggleBrewCollectionState(brewGroup.class);
}}> }}>
{brewGroup.title || 'No Title'} {brewGroup.title || 'No Title'}