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

Issue 496/add recent activity page #537

Closed
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@styled-icons/boxicons-solid": "^10.23.0",
"axios": "^0.21.1",
"coordinate-parser": "^1.0.7",
"date-fns": "^4.1.0",
"debounce": "^1.2.1",
"formik": "^2.2.6",
"google-map-react": "^2.1.9",
Expand Down
4 changes: 3 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"imported_from": "Imported from {{name}}",
"edited_on": "Edited on {{date}}",
"glossary": {
"activity": "Activity",
"about": "About",
"list": "List",
"tree_inventory": "Tree inventory",
Expand Down Expand Up @@ -53,7 +54,8 @@
"project": "The project",
"data": "The data",
"sharing": "Sharing the harvest",
"press": "In the press"
"press": "In the press",
"last_activity": "Last activity"
},
"users": {
"sign_in": "Sign in",
Expand Down
155 changes: 155 additions & 0 deletions src/components/activity/ActivityPage.js
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
53 changes: 53 additions & 0 deletions src/components/activity/InfinityList.js
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
41 changes: 41 additions & 0 deletions src/components/activity/LazyLoader.js
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
15 changes: 15 additions & 0 deletions src/components/activity/activityRoutes.js
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
42 changes: 42 additions & 0 deletions src/components/activity/styles/ActivityPageStyles.js
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;
`
44 changes: 44 additions & 0 deletions src/components/activity/utils/listSortUtils.js
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
}
2 changes: 2 additions & 0 deletions src/components/desktop/DesktopLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import SplitPane from 'react-split-pane'
import styled from 'styled-components/macro'

import aboutRoutes from '../about/aboutRoutes'
import activityRoutes from '../activity/activityRoutes'
import authRoutes from '../auth/authRoutes'
import connectRoutes from '../connect/connectRoutes'
import MapPage from '../map/MapPage'
Expand Down Expand Up @@ -51,6 +52,7 @@ const DesktopLayout = () => (
<Header />
<Switch>
{aboutRoutes}
{activityRoutes}
{authRoutes}
<Route>
{connectRoutes}
Expand Down
Loading
Loading