forked from falling-fruit/falling-fruit-web
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
falling-fruit#496 Enhancement. Add page activity with infinity scroll
- Loading branch information
Vladyslav Moskalenko
committed
Oct 23, 2024
1 parent
86ccc9d
commit 81feba2
Showing
16 changed files
with
517 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import React, { useCallback, useEffect, useRef, useState } from 'react' | ||
import { useTranslation } from 'react-i18next' | ||
import { useDispatch, useSelector } from 'react-redux' | ||
|
||
import { fetchLocationChanges } from '../../redux/locationSlice' | ||
import { fetchAndLocalizeTypes } from '../../redux/typeSlice' | ||
import { PageScrollWrapper, PageTemplate } from '../about/PageTemplate' | ||
import InfinityList from './InfinityList' | ||
import LazyLoader from './LazyLoader' | ||
import { LazyLoaderWrapper } from './styles/ActivityPageStyles' | ||
import { groupChangesByDate, timePeriods } from './utils/listSortUtils' | ||
|
||
const MAX_RECORDS = 1000 | ||
|
||
const ActivityPage = () => { | ||
const dispatch = useDispatch() | ||
const { i18n } = useTranslation() | ||
const language = i18n.language | ||
|
||
const [locationChanges, setLocationChanges] = useState([]) | ||
const [isLoading, setIsLoading] = useState(false) | ||
const [offset, setOffset] = useState(0) | ||
|
||
const loadMoreRef = useRef() | ||
|
||
const { type, error } = useSelector((state) => ({ | ||
type: state.type.typesAccess.localizedTypes, | ||
error: state.location.error, | ||
})) | ||
|
||
const loadMoreChanges = useCallback(async () => { | ||
if (isLoading || locationChanges.length >= MAX_RECORDS) { | ||
return | ||
} | ||
|
||
setIsLoading(true) | ||
|
||
try { | ||
const newChanges = await dispatch( | ||
fetchLocationChanges({ offset }), | ||
).unwrap() | ||
|
||
if (newChanges.length > 0) { | ||
setLocationChanges((prevChanges) => [...prevChanges, ...newChanges]) | ||
setOffset((prevOffset) => prevOffset + newChanges.length) | ||
} | ||
} finally { | ||
setIsLoading(false) | ||
} | ||
}, [dispatch, isLoading, offset, locationChanges.length]) | ||
|
||
useEffect(() => { | ||
dispatch(fetchAndLocalizeTypes(language)) | ||
}, [dispatch, language]) | ||
|
||
useEffect(() => { | ||
const handleScroll = () => { | ||
if ( | ||
!isLoading && | ||
loadMoreRef.current && | ||
loadMoreRef.current.getBoundingClientRect().bottom <= window.innerHeight | ||
) { | ||
loadMoreChanges() | ||
} | ||
} | ||
|
||
window.addEventListener('scroll', handleScroll) | ||
return () => window.removeEventListener('scroll', handleScroll) | ||
}, [isLoading, loadMoreChanges]) | ||
|
||
useEffect(() => { | ||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
if (entries[0].isIntersecting && !isLoading) { | ||
loadMoreChanges() | ||
} | ||
}, | ||
{ threshold: 1.0 }, | ||
) | ||
|
||
const currentRef = loadMoreRef.current | ||
|
||
if (currentRef) { | ||
observer.observe(currentRef) | ||
} | ||
|
||
return () => { | ||
if (currentRef) { | ||
observer.unobserve(currentRef) | ||
} | ||
} | ||
}, [isLoading, loadMoreChanges]) | ||
|
||
const getPlantName = (typeId) => { | ||
const plant = type.find((t) => t.id === typeId) | ||
return plant ? plant.commonName || plant.scientificName : 'Unknown Plant' | ||
} | ||
|
||
const groupedChanges = groupChangesByDate(locationChanges) | ||
|
||
return ( | ||
<PageScrollWrapper> | ||
{/* eslint-disable-next-line react/style-prop-object */} | ||
<PageTemplate from="Settings"> | ||
<h1>Recent Activity</h1> | ||
<p> | ||
Explore the latest contributions from our community as they document | ||
fruit-bearing trees and plants across different regions. Your input | ||
helps make foraging and sustainable living accessible to everyone! | ||
</p> | ||
|
||
<p> | ||
Join the growing community of foragers and urban explorers by adding | ||
your own findings or discovering what’s nearby. Together, we can map | ||
the world’s! | ||
</p> | ||
|
||
<p> | ||
Browse through the latest additions to find trees near you, or sign up | ||
to add your own. Click on a tree name for more details about the | ||
location and type of fruit. | ||
</p> | ||
|
||
{error && ( | ||
<p> | ||
Error fetching changes: {error.message || JSON.stringify(error)} | ||
</p> | ||
)} | ||
|
||
{locationChanges.length > 0 && ( | ||
<InfinityList | ||
groupedChanges={groupedChanges} | ||
timePeriods={timePeriods} | ||
getPlantName={getPlantName} | ||
/> | ||
)} | ||
|
||
<div ref={loadMoreRef}></div> | ||
|
||
{isLoading && ( | ||
<LazyLoaderWrapper> | ||
<LazyLoader /> | ||
</LazyLoaderWrapper> | ||
)} | ||
{locationChanges.length >= MAX_RECORDS && ( | ||
<LazyLoaderWrapper> | ||
You have only viewed the first {MAX_RECORDS} activities! | ||
</LazyLoaderWrapper> | ||
)} | ||
</PageTemplate> | ||
</PageScrollWrapper> | ||
) | ||
} | ||
|
||
export default ActivityPage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React from 'react' | ||
|
||
import { | ||
ActivityText, | ||
AuthorName, | ||
List, | ||
ListItem, | ||
PlantLink, | ||
} from './styles/ActivityPageStyles' | ||
|
||
const InfinityList = ({ groupedChanges, timePeriods, getPlantName }) => { | ||
const renderGroup = (groupName, changes) => { | ||
if (changes.length === 0) { | ||
return null | ||
} | ||
|
||
return ( | ||
<div key={groupName}> | ||
<h3>{groupName.replace(/[A-Z]/g, (letter) => `${letter}`).trim()}</h3> | ||
<List> | ||
{changes.map((change, index) => ( | ||
<ListItem key={index}> | ||
{change.type_ids.map((typeId, idx) => ( | ||
<p key={`${index}${idx}`}> | ||
<PlantLink | ||
href={`/locations/${change.lat},${change.lng},15z`} | ||
> | ||
{getPlantName(typeId)} | ||
</PlantLink> | ||
<ActivityText> | ||
, {change.description} in {change.city}, {change.state},{' '} | ||
{change.country} —{' '} | ||
</ActivityText> | ||
<AuthorName>{change.author}</AuthorName> | ||
</p> | ||
))} | ||
</ListItem> | ||
))} | ||
</List> | ||
</div> | ||
) | ||
} | ||
|
||
return ( | ||
<> | ||
{timePeriods.map((period) => | ||
renderGroup(period.name, groupedChanges[period.name]), | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
export default InfinityList |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import React from 'react' | ||
import styled from 'styled-components' | ||
|
||
const Loader = styled.div` | ||
display: block; | ||
--height-of-loader: 4px; | ||
--loader-color: #0071e2; | ||
width: 130px; | ||
height: 0.1rem; | ||
border-radius: 30px; | ||
background-color: rgba(0, 0, 0, 0.2); | ||
position: relative; | ||
&::before { | ||
content: ''; | ||
position: absolute; | ||
background: orange; | ||
top: 0; | ||
left: 0; | ||
width: 0%; | ||
height: 100%; | ||
border-radius: 30px; | ||
animation: moving 1s ease-in-out infinite; | ||
} | ||
@keyframes moving { | ||
50% { | ||
width: 100%; | ||
} | ||
100% { | ||
width: 0; | ||
right: 0; | ||
left: unset; | ||
} | ||
} | ||
` | ||
|
||
const LazyLoader = () => <Loader /> | ||
|
||
export default LazyLoader |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Route } from 'react-router-dom' | ||
|
||
import ActivityPage from './ActivityPage' | ||
|
||
const pages = [ | ||
{ | ||
path: ['/activity'], | ||
component: ActivityPage, | ||
}, | ||
] | ||
|
||
const activityRoutes = pages.map((props) => ( | ||
<Route key={props.path[0]} {...props} /> | ||
)) | ||
export default activityRoutes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import styled from 'styled-components' | ||
|
||
export const PlantLink = styled.a` | ||
color: #007bff !important; | ||
font-weight: bold !important; | ||
font-size: 1rem; | ||
text-decoration: none; | ||
cursor: pointer; | ||
&:hover { | ||
text-decoration: underline; | ||
} | ||
` | ||
|
||
export const AuthorName = styled.span` | ||
color: grey; | ||
font-weight: bold; | ||
font-size: 1rem; | ||
` | ||
|
||
export const ActivityText = styled.span` | ||
font-size: 1rem; | ||
color: grey; | ||
` | ||
|
||
export const List = styled.ul` | ||
list-style-type: none; | ||
padding: 0; | ||
margin: 0; | ||
` | ||
|
||
export const ListItem = styled.li` | ||
margin-bottom: 1rem; | ||
` | ||
|
||
export const LazyLoaderWrapper = styled.div` | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
width: 100%; | ||
height: 5rem; | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
export const timePeriods = [ | ||
{ name: 'Today', condition: (daysAgo) => daysAgo === 0 }, | ||
{ name: 'Yesterday', condition: (daysAgo) => daysAgo === 1 }, | ||
{ name: '2 Days Ago', condition: (daysAgo) => daysAgo === 2 }, | ||
{ name: '3 Days Ago', condition: (daysAgo) => daysAgo === 3 }, | ||
{ name: 'This Week', condition: (daysAgo) => daysAgo <= 7 }, | ||
{ name: 'Last Week', condition: (daysAgo) => daysAgo <= 14 }, | ||
{ name: '2 Weeks Ago', condition: (daysAgo) => daysAgo <= 21 }, | ||
{ name: '3 Weeks Ago', condition: (daysAgo) => daysAgo <= 28 }, | ||
{ name: 'This Month', condition: (daysAgo) => daysAgo <= 30 }, | ||
{ name: 'Last Month', condition: (daysAgo) => daysAgo <= 60 }, | ||
{ name: 'Three Months Ago', condition: (daysAgo) => daysAgo <= 90 }, | ||
{ name: 'Six Months Ago', condition: (daysAgo) => daysAgo <= 180 }, | ||
{ name: 'One Year Ago', condition: (daysAgo) => daysAgo <= 365 }, | ||
{ name: 'More Than a Year', condition: (daysAgo) => daysAgo > 365 }, | ||
] | ||
|
||
const getDaysDifference = (date1, date2) => { | ||
const day1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) | ||
const day2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()) | ||
const timeDiff = day1.getTime() - day2.getTime() | ||
return Math.floor(timeDiff / (1000 * 3600 * 24)) | ||
} | ||
|
||
export const groupChangesByDate = (changes) => { | ||
const today = new Date() | ||
|
||
const groups = timePeriods.reduce((acc, period) => { | ||
acc[period.name] = [] | ||
return acc | ||
}, {}) | ||
|
||
changes.forEach((change) => { | ||
const changeDate = new Date(change.created_at) | ||
const daysAgo = getDaysDifference(today, changeDate) | ||
|
||
const period = timePeriods.find((period) => period.condition(daysAgo)) | ||
if (period) { | ||
groups[period.name].push(change) | ||
} | ||
}) | ||
|
||
return groups | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.