Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LIBSEARCH-957] Shelf Carousel implementation #454

Merged
merged 43 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d283930
Creating `BrowseLink` component to be used for both `Metadata` and so…
erinesullivan Apr 4, 2024
df8f65f
Creating and setting up `ShelfBrowse`.
erinesullivan Apr 4, 2024
ea462d0
Adding working scripts for carousel.
erinesullivan Apr 5, 2024
5650805
..
erinesullivan Apr 17, 2024
f847309
Removing the need for `createSelector`.
erinesullivan Apr 17, 2024
fd27d70
Styling container structure.
erinesullivan Apr 18, 2024
9e967da
Simplifying `Metadata`.
erinesullivan Apr 18, 2024
c0d958b
Simplifying `Metadata`.
erinesullivan Apr 19, 2024
b0c813a
Undoing expandable count.
erinesullivan Apr 19, 2024
88f41d8
Setting `immutableCheck` to `false` as it slows performance during de…
erinesullivan Apr 19, 2024
4d696a9
Merge branch 'master' into nearby-on-shelf-carousel
erinesullivan Apr 19, 2024
d3569e9
Adding and applying test data.
erinesullivan Apr 23, 2024
3b5956f
Adding `Noto Sans SemiCondensed`. Styling for `THIS ITEM`.
erinesullivan Apr 24, 2024
118adbe
Adding animations and adjusting styles.
erinesullivan Apr 25, 2024
5a5ecb5
Disabling buttons if on first/last page.
erinesullivan Apr 25, 2024
5e6ff15
Creating `useWindowWidth` as a reusable function.
erinesullivan Apr 25, 2024
414dafe
Updating `Browse in call number list` link, updating card links to po…
erinesullivan Apr 26, 2024
395a2ca
Turning off animation for users who prefer reduced motion, adding ani…
erinesullivan Apr 26, 2024
07ed6f9
Adding `Continue browse...` cards at the beginning and end of the car…
erinesullivan Apr 26, 2024
5e5d314
Separate `Current Record` into own div. Change `display` to `grid` so…
erinesullivan Apr 29, 2024
6b00b1e
Update mobile design to show blank cards. Hide left/right blank card …
erinesullivan Apr 30, 2024
9d0e36c
Recenter slideshow on resize.
erinesullivan Apr 30, 2024
03cb5b9
Merge branch 'master' into nearby-on-shelf-carousel
erinesullivan May 8, 2024
f876d76
Created a function that checks which page the current record is in, a…
erinesullivan May 9, 2024
5c1eec3
Merge branch 'master' into nearby-on-shelf-carousel
erinesullivan May 9, 2024
0069ed4
Mobile: Moving arrows to sides and removing blank cards.
erinesullivan May 9, 2024
e7a6d7b
Making button labels singular or plural depending on number of items …
erinesullivan May 9, 2024
16208be
Closing annoying gap between `CURRENT RECORD` banner and border.
erinesullivan May 9, 2024
f3091b7
Merge branch 'master' into nearby-on-shelf-carousel
erinesullivan May 31, 2024
c4c7f9b
Merge branch 'master' into nearby-on-shelf-carousel
erinesullivan Jun 5, 2024
8d1e733
Breaking up carousel and fetching logic.
erinesullivan Jun 7, 2024
d29c7b9
Adding logic to find `browse` descriptor information.
erinesullivan Jun 7, 2024
355ad64
Updating `test-data` with a final structure. Updating the carousel to…
erinesullivan Jun 7, 2024
441fb6e
Adding `fetch` functionality to `ShelfBrowse`.
erinesullivan Jun 7, 2024
af8b5a6
Removing the need for test data.
erinesullivan Jun 26, 2024
6c43d99
Adding record `uid` check for the current record in case other items …
erinesullivan Jun 26, 2024
051459d
Adding logic to test against `uid` for `currentItem`.
erinesullivan Jun 26, 2024
6302d54
Making all cards point to full records, except for first and last non…
erinesullivan Jun 26, 2024
881bdda
bug: Trimming `callNumber` to eliminate surrounding whitespace for co…
erinesullivan Jun 26, 2024
5e025a6
Changing `Return to current item` to `Return to current record`.
erinesullivan Jul 15, 2024
bb1f31d
Adding production URL for fetching.
erinesullivan Jul 22, 2024
a38c076
Replacing `item` with `record`.
erinesullivan Jul 22, 2024
4a26d7d
Merge branch 'master' into nearby-on-shelf-carousel
erinesullivan Jul 25, 2024
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
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
href="https://fonts.googleapis.com/css2?family=Crimson+Text&family=Muli:wght@400;600;700&family=Noto+Sans:wght@300;400;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="https://use.typekit.net/xst6ocg.css">
<link href="https://cdn.jsdelivr.net/npm/@umich-lib/web@latest/umich-lib.css" rel="stylesheet"/>
<script
type="module"
Expand Down
49 changes: 49 additions & 0 deletions src/modules/browse/components/ShelfBrowse/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react';
import ShelfBrowseCarousel from '../ShelfBrowseCarousel';
import { useSelector } from 'react-redux';

const ShelfBrowse = () => {
const [shelfData, setShelfData] = useState();
const { metadata, uid } = useSelector((state) => {
return state.records.record;
});

const callNumberBrowse = metadata.full
.flatMap((data) => {
return data.description;
})
.find((callNumber) => {
return callNumber.browse?.type === 'callnumber';
});

if (!callNumberBrowse) {
return null;
}

const callNumber = callNumberBrowse.browse.value.trim();

useEffect(() => {
const fetchShelfData = async () => {
try {
const response = await fetch(`https://search.lib.umich.edu/catalog/browse/carousel?query=${callNumber}`);
if (!response.ok) {
throw new Error(`HTTP Error! status: ${response.status}`);
}
const data = await response.json();
setShelfData(data);
} catch {
setShelfData();
}
};

fetchShelfData();
}, [callNumber]);

if (!shelfData) {
return null;
}

return <ShelfBrowseCarousel {...{ callNumber, items: shelfData, uid }} />;
};

export default ShelfBrowse;
179 changes: 179 additions & 0 deletions src/modules/browse/components/ShelfBrowseCarousel/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import './styles.css';
import { Anchor, Icon, useWindowWidth } from '../../../reusable';
import React, { useEffect, useState } from 'react';
import BrowseLink from '../BrowseLink';
import PropTypes from 'prop-types';

const ShelfBrowseCarousel = ({ callNumber, items, uid }) => {
const currentItem = (item) => {
return item.call_number === callNumber && item.url.endsWith(uid);
};
const callNumberIndex = items.findIndex((item) => {
return currentItem(item);
});
const windowWidth = useWindowWidth();
let itemsPerPage = 5;
if (windowWidth < 820) {
itemsPerPage = 3;
}
if (windowWidth < 640) {
itemsPerPage = 1;
}
const middlePage = Math.floor(callNumberIndex / itemsPerPage);
const [currentPage, setCurrentPage] = useState(middlePage);
const [animationClass, setAnimationClass] = useState('');
const animationDuration = 250;
const debounce = (func) => {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, animationDuration);
};
};

useEffect(() => {
const handleResize = debounce(() => {
setCurrentPage(middlePage);
});

window.addEventListener('resize', handleResize);

return () => {
return window.removeEventListener('resize', handleResize);
};
}, [middlePage]);

const maxPages = Math.ceil(items.length / itemsPerPage);

const moveCarousel = (direction) => {
const getDirection = direction === 1 ? 'next' : 'previous';
setAnimationClass(`animation-out-${getDirection}`);
setTimeout(() => {
setAnimationClass(`animation-in-${getDirection}`);
setCurrentPage((prevPage) => {
let newPage = prevPage + direction;
if (newPage < 0) {
newPage = maxPages - 1;
}
if (newPage >= maxPages) {
newPage = 0;
}
return newPage;
});
}, animationDuration);
setTimeout(() => {
setAnimationClass('');
}, animationDuration * 2);
};

const startIndex = currentPage * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentItems = items.slice(startIndex, endIndex);
const firstPage = currentPage === 0;
const lastPage = currentPage === maxPages - 1;
const buttonLabel = `${itemsPerPage} record${itemsPerPage === 1 ? '' : 's'}`;

return (
<section className='shelf-browse container__rounded'>
<header className='flex__responsive'>
<h2 className='margin-y__none heading-medium'>Shelf browse</h2>
<BrowseLink value={callNumber}>
<Icon icon='list' size='24' className='margin-right__xs' />Browse in call number list
</BrowseLink>
</header>
<div className='shelf-browse-carousel' aria-label='Shelf browse carousel'>
<button
aria-label={`Previous ${buttonLabel}`}
title={`Previous ${buttonLabel}`}
disabled={firstPage}
onClick={() => {
return moveCarousel(-1);
}}
className='btn no-background'
>
<Icon icon='chevron_left' size='24' />
</button>
<ul
className='list__unstyled shelf-browse-items'
style={{
gridTemplateColumns: `repeat(${itemsPerPage}, 1fr)`
}}
>
{currentItems.map((item, index) => {
const isCurrentItem = currentItem(item);
const firstOrLastItem = !isCurrentItem && ((firstPage && index === 0) || (lastPage && currentItems.length - 1 === index));
const fields = firstOrLastItem ? ['call_number'] : Object.keys(item).slice(0, -1);
const basePath = 'https://search.lib.umich.edu';
const anchorAttributes = firstOrLastItem
? { href: `${basePath}/catalog/browse/callnumber?query=${item.call_number}` }
: { to: item.url.replace(basePath, '') + window.location.search };

return (
<li key={index} className={`shelf-browse-item ${(isCurrentItem || firstOrLastItem) ? 'shelf-browse-item-current' : ''} ${animationClass}`}>
<Anchor
{...anchorAttributes}
className={`underline__none container__rounded padding-x__s padding-bottom__xs padding-top__${isCurrentItem ? 'xs' : 's'}`}
>
<dl className='flex'>
{isCurrentItem && <p className='margin__none this-item'>Current Record</p>}
{firstOrLastItem && (
<>
<Icon icon='list' size='24' className='item-term-title' />
<span className='item-term-title'>Continue browsing in call number list</span>
</>
)}
{fields.map((key) => {
return item[key] && (
<React.Fragment key={key}>
<dt className='visually-hidden'>{key.replaceAll('_', ' ')}</dt>
<dd className={`item-term-${key}`}>{item[key]}</dd>
</React.Fragment>
);
})}
</dl>
</Anchor>
</li>
);
})}
</ul>
<button
aria-label={`Next ${buttonLabel}`}
title={`Next ${buttonLabel}`}
disabled={lastPage}
onClick={() => {
return moveCarousel(1);
}}
className='btn no-background'
>
<Icon icon='chevron_right' size='24' />
</button>
</div>
<button
className='btn btn--secondary btn--small shelf-browse-carousel-return'
onClick={() => {
setAnimationClass('animation-out');
setTimeout(() => {
setCurrentPage(middlePage);
setAnimationClass('animation-in');
}, animationDuration);
setTimeout(() => {
setAnimationClass('');
}, animationDuration * 2);
}}
disabled={currentPage === middlePage}
>
Return to current record
</button>
</section>
);
};

ShelfBrowseCarousel.propTypes = {
callNumber: PropTypes.string,
items: PropTypes.array,
uid: PropTypes.string
};

export default ShelfBrowseCarousel;
Loading