Skip to content

Commit

Permalink
Activity page
Browse files Browse the repository at this point in the history
- improvements to the original version
- allow going back from location and user page
  • Loading branch information
wbazant committed Dec 10, 2024
1 parent a0639f5 commit 9ed1b97
Show file tree
Hide file tree
Showing 24 changed files with 523 additions and 464 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"eslint-plugin-simple-import-sort": "^7.0.0",
"husky": "^4.3.8",
"lint-staged": "^10.5.3",
"prettier": "^3.1.0"
"prettier": "^3.4.1"
},
"scripts": {
"start": "react-scripts start",
Expand Down
14 changes: 10 additions & 4 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@
"zoom_in_to_add_location": "Zoom in to add location"
},
"pages": {
"project": "The project",
"data": "The data",
"sharing": "Sharing the harvest",
"press": "In the press",
"about": {
"ff_disclaimer_html": "Falling Fruit is not associated with Fallen Fruit. Fallen Fruit can be found at <a href=\"http://fallenfruit.org\">fallenfruit.org</a>.",
"give_us_money": "We are a 501(c)(3) nonprofit and rely on donations to operate. If you are willing and able, please consider making a financial contribution. Donations within the United States are tax deductible.",
Expand Down Expand Up @@ -132,5 +128,15 @@
"no_token": "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided.",
"updated_not_active": "Your password has been changed successfully."
}
},
"layouts": {
"application": {
"menu": {
"the_project": "The project",
"the_data": "The data",
"sharing_the_harvest": "Sharing the harvest",
"in_the_press": "In the press"
}
}
}
}
17 changes: 12 additions & 5 deletions public/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"send": "Envoyer",
"save_changes": "Enregistrer vos modifications",
"edit": "Modifier",
"donate": "Contribuer"
"donate": "Contribuer",
"activity": "Activité"
},
"side_menu": {
"regional": "Régional",
Expand All @@ -50,10 +51,6 @@
"add_location": "Ajouter un site"
},
"pages": {
"project": "Le projet",
"data": "Les données",
"sharing": "Partager la récolte",
"press": "Dans la presse",
"about": {
"ff_disclaimer_html": "Falling Fruit n'est pas lié à Fallen Fruit. Fallen Fruit peut être consulté à <a href=\"http://fallenfruit.org\">fallenfruit.org</a>.",
"give_us_money": "Nous sommes un organisme caritatif à but non lucratif 501(c)(3) basé à Boulder (Colorado, États-Unis). Les dons réalisés depuis les États-Unis sont donc déductibles des impôts.",
Expand Down Expand Up @@ -130,5 +127,15 @@
"no_token": "Vous ne pouvez pas accéder à cette page sans passer par un e-mail de réinitialisation de mot de passe. Si vous venez en effet d’un tel email, assurez-vous d’utiliser l’URL complète.",
"updated_not_active": "Votre mot de passe a été changé avec succès."
}
},
"layouts": {
"application": {
"menu": {
"the_project": "Le projet",
"the_data": "Les données",
"sharing_the_harvest": "Partager la récolte",
"in_the_press": "Dans la presse"
}
}
}
}
168 changes: 68 additions & 100 deletions src/components/activity/ActivityPage.js
Original file line number Diff line number Diff line change
@@ -1,129 +1,97 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useEffect, useRef } from 'react'
import Skeleton from 'react-loading-skeleton'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'

import { fetchLocationChanges } from '../../redux/locationSlice'
import { fetchAndLocalizeTypes } from '../../redux/typeSlice'
import { debounce } from '../../utils/debounce'
import { groupChangesByDate } from '../../utils/groupChangesByDate'
import {
fetchMoreLocationChanges,
setAnchorElementId,
} from '../../redux/activitySlice'
import { transformActivityData } from '../../utils/transformActivityData'
import { PageScrollWrapper, PageTemplate } from '../about/PageTemplate'
import SkeletonLoader from '../ui/SkeletonLoader'
import InfinityList from './InfinityList'

const MAX_RECORDS = 1000
import ChangesPeriod from './ChangesPeriod'

const SkeletonWrapper = styled.div`
margin-bottom: 16px;
`

const SkeletonGroup = styled.div`
margin-bottom: 20px;
`

const SkeletonLoader = ({ count = 3 }) => (
<SkeletonWrapper>
{Array.from({ length: count }).map((_, index) => (
<SkeletonGroup key={index}>
<Skeleton width="40%" height={20} style={{ marginBottom: 8 }} />
<Skeleton width="80%" height={20} style={{ marginBottom: 8 }} />
<Skeleton width="55%" height={20} style={{ marginBottom: 8 }} />
<Skeleton width="70%" height={20} />
</SkeletonGroup>
))}
</SkeletonWrapper>
)

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 [isInitialLoad, setIsInitialLoad] = useState(true)

const loadMoreRef = useRef()
const scrollPositionRef = useRef(0)

const { type, error } = useSelector((state) => ({
type: state.type.typesAccess.localizedTypes,
error: state.location.error,
}))
const { typesAccess } = useSelector((state) => state.type)
const { locationChanges, isLoading } = useSelector((state) => state.activity)
const { anchorElementId } = useSelector((state) => state.activity)

const loadMoreChanges = useCallback(async () => {
if (isLoading) {
return
}
const changesReady = !typesAccess.isEmpty

setIsLoading(true)
scrollPositionRef.current = window.scrollY

try {
const newChanges = await dispatch(
fetchLocationChanges({ offset }),
).unwrap()

if (newChanges.length > 0) {
setLocationChanges((prevChanges) => {
const updatedChanges = [...prevChanges, ...newChanges]
return updatedChanges.length > MAX_RECORDS
? updatedChanges.slice(-MAX_RECORDS)
: updatedChanges
})
setOffset((prevOffset) => prevOffset + newChanges.length)
useEffect(() => {
if (anchorElementId) {
const periodElement = document.getElementById(`${anchorElementId}`)
if (periodElement) {
periodElement.scrollIntoView()
dispatch(setAnchorElementId(null))
}
} finally {
setIsLoading(false)
setIsInitialLoad(false)
window.scrollTo({
top: scrollPositionRef.current,
behavior: 'smooth',
})
}
}, [dispatch, isLoading, offset])

// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedLoadMoreChanges = useCallback(debounce(loadMoreChanges, 300), [
loadMoreChanges,
])
}, [anchorElementId, dispatch])

useEffect(() => {
dispatch(fetchAndLocalizeTypes(language))
}, [dispatch, language])

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading) {
setIsLoading(true)
debouncedLoadMoreChanges()
}
},
{ threshold: 1.0 },
)

const currentRef = loadMoreRef.current
if (changesReady) {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
dispatch(fetchMoreLocationChanges())
}
},
{ threshold: 1.0 },
)

const currentRef = loadMoreRef.current

if (currentRef) {
observer.observe(currentRef)
}

return () => {
if (currentRef) {
observer.unobserve(currentRef)
observer.observe(currentRef)
}
}
}, [isLoading, debouncedLoadMoreChanges])

const getPlantName = (typeId) => {
const plant = type.find((t) => t.id === typeId)
return plant ? plant.commonName || plant.scientificName : 'Unknown Plant'
}
return () => {
if (currentRef) {
observer.unobserve(currentRef)
}
}
}
}, [dispatch, changesReady])

const groupedChanges = groupChangesByDate(locationChanges)
const groupedData = transformActivityData(locationChanges, typesAccess)

return (
<PageScrollWrapper>
<PageTemplate from="Settings">
<h1>Recent Changes</h1>
<p>
Explore the latest contributions from our community as they document
fruit-bearing trees and plants across different regions.
</p>
{error && (
<p>
Error fetching changes: {error.message || JSON.stringify(error)}
</p>
)}
{locationChanges.length > 0 && (
<InfinityList
groupedChanges={groupedChanges}
getPlantName={getPlantName}
/>
)}
{locationChanges.length > 0 &&
groupedData.map((period) => (
<ChangesPeriod key={period.periodName} period={period} />
))}
<div ref={loadMoreRef}></div>
{isLoading && <SkeletonLoader count={isInitialLoad ? 5 : 1} />}{' '}
{/* Render 3 skeletons on initial load */}
{isLoading && (
<SkeletonLoader count={locationChanges.length === 0 ? 5 : 1} />
)}
</PageTemplate>
</PageScrollWrapper>
)
Expand Down
42 changes: 0 additions & 42 deletions src/components/activity/ActivityPageStyles.js

This file was deleted.

Loading

0 comments on commit 9ed1b97

Please sign in to comment.