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

Revisit review summary #659

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/components/entry/EntryOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ReportButton } from './overview/ReportButton'
import Tags from './overview/Tags'
import TypesHeader from './overview/TypesHeader'
import { ReviewButton } from './ReviewButton'
import ReviewSummary from './ReviewSummary'
import { formatISOString, formatMonth } from './textFormatters'

const hasSeasonality = (locationData) =>
Expand Down Expand Up @@ -72,6 +73,7 @@ const EntryOverview = () => {
locationId,
location: locationData,
pane,
reviews,
} = useSelector((state) => state.location)
const { locationsWithoutPanorama } = useSelector((state) => state.misc)
const dispatch = useDispatch()
Expand Down Expand Up @@ -200,6 +202,7 @@ const EntryOverview = () => {
</time>
</p>
</IconBesideText>
<ReviewSummary reviews={reviews} />
<div>
<ReviewButton />
<ReportButton />
Expand Down
2 changes: 0 additions & 2 deletions src/components/entry/EntryReviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useIsDesktop } from '../../utils/useBreakpoint'
import { ReviewForm } from '../form/ReviewForm'
import Review from './Review'
import { ReviewButton } from './ReviewButton'
import ReviewSummary from './ReviewSummary'

const EntryReviews = () => {
const isDesktop = useIsDesktop()
Expand All @@ -22,7 +21,6 @@ const EntryReviews = () => {
}
return (
<>
<ReviewSummary reviews={reviews} />
{!isDesktop && <ReviewButton />}
<h3>Reviews</h3>
{reviews.map((review) => {
Expand Down
245 changes: 116 additions & 129 deletions src/components/entry/ReviewSummary.js
Original file line number Diff line number Diff line change
@@ -1,167 +1,154 @@
import { Star as StarEmpty } from '@styled-icons/boxicons-regular'
import { Star, StarHalf } from '@styled-icons/boxicons-solid'
import { groupBy, prop as rProp } from 'ramda'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components/macro'

import { formatMonth } from './textFormatters'
import { createReviewSummary } from '../../utils/createReviewSummary'

const SummaryTable = styled.table`
border-spacing: 0;
width: 100%;
const StatsContainer = styled.div`
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.secondaryText};
margin-bottom: 1em;
gap: 4px;

svg {
width: 1.2em;
height: 1.2em;
margin: -0.3em 0 0 0.3em;
width: 1em;
height: 1em;
color: ${({ theme }) => theme.orange};
}
`

tbody {
line-height: 1.14;
}

td:nth-child(1) {
font-size: 1rem;
color: ${({ theme }) => theme.tertiaryText};
margin: 3px 0;
}

td:nth-child(2) {
font-size: 1.14rem;
text-align: right;
}

td > p {
margin-top: 0.5em;
font-size: 1rem;
}
const StatsRow = styled.div`
display: flex;
flex-wrap: wrap;
`

h3 {
margin-top: 0;
const Separator = styled.span`
margin: 0 0.5em;
&::before {
content: '·';
}
`

const FruitingSummaryRow = ({ reviews }) => {
const { t, i18n } = useTranslation()
if (!reviews?.length) {
const formatMonthList = (months) => {
if (!months.length) {
return null
}

const reviewsByMonth = reviews.reduce((monthToCount, review) => {
if (!review.observed_on) {
return monthToCount
}
const month = new Date(review.observed_on).getMonth()

monthToCount = {
...monthToCount,
[month]: (monthToCount[month] || 0) + 1,
}

return monthToCount
const monthCounts = months.reduce((acc, month) => {
acc[month] = (acc[month] || 0) + 1
return acc
}, {})

const reviewMonthPairs = Object.entries(reviewsByMonth)

return (
<tr>
<td colSpan={2}>
<p>
{t(`locations.infowindow.fruiting.${reviews[0].fruiting}`)}:{' '}
{reviewMonthPairs
.map(
([month, count]) =>
`${formatMonth(month, i18n.language)} (${count})`,
)
.join(', ')}
</p>
</td>
</tr>
)
const monthsStr = Object.entries(monthCounts)
.map(([month, count]) => {
const date = new Date(1, parseInt(month))
const monthStr = date.toLocaleDateString(undefined, {
month: 'long',
})
return `${monthStr} (${count})`
})
.join(', ')

return monthsStr
}

const FruitingSummary = ({ reviews }) => {
const {
0: flowerReviews,
1: unripeReviews,
2: ripeReviews,
} = groupBy(rProp('fruiting'), reviews)
const getStarRating = (score) => {
if (!score) {
return null
}

return (
<>
<tr>
<td colSpan={2}>Fruiting</td>
</tr>
<FruitingSummaryRow reviews={flowerReviews} />
<FruitingSummaryRow reviews={unripeReviews} />
<FruitingSummaryRow reviews={ripeReviews} />
</>
)
}
const stars = []
const remainder = score % 1
const fullStars = Math.floor(score) + 1

for (let i = 0; i < 5; i++) {
if (i < fullStars) {
stars.push(<Star key={i} />)
} else if (i === fullStars && remainder >= 0.25 && remainder <= 0.75) {
stars.push(<StarHalf key={i} />)
} else {
stars.push(<StarEmpty key={i} />)
}
}

const SummaryRow = ({ title, scores, total }) => {
const aggregateScore = scores.reduce((a, b) => a + b, 0) / scores.length
const percentScore = aggregateScore / total
return stars
}

let icon = <Star />
const ReviewSummary = ({ reviews }) => {
const { t } = useTranslation()
const summary = createReviewSummary(reviews)
const stats = []

if (summary.quality.average !== null) {
stats.push(
<span key="quality">
{t('glossary.quality')} {getStarRating(summary.quality.average)} (
{summary.quality.count})
</span>,
)
}

if (percentScore <= 0.25) {
icon = <StarEmpty />
} else if (percentScore <= 0.5) {
icon = <StarHalf />
if (summary.yield.average !== null) {
stats.push(
<span key="yield">
{t('glossary.yield')} {getStarRating(summary.yield.average)} (
{summary.yield.count})
</span>,
)
}

if (Number.isNaN(percentScore)) {
icon = null
const flowers = formatMonthList(summary.fruiting.flowers)
if (flowers) {
stats.push(
<span key="flowers">
{t('locations.infowindow.fruiting.0')} – {flowers}
</span>,
)
}

return (
<tr>
<td>{title}</td>
<td>
{scores.length > 0 ? aggregateScore.toFixed(1) : <>&mdash;</>}
<small>/{total}</small>
{icon}
</td>
</tr>
)
}
const unripe = formatMonthList(summary.fruiting.unripe)
if (unripe) {
stats.push(
<span key="unripe">
{t('locations.infowindow.fruiting.1')} – {unripe}
</span>,
)
}

const ReviewSummary = ({ reviews }) => {
const { t } = useTranslation()
const qualityScores = reviews.reduce((scores, review) => {
if (review.quality_rating) {
return [...scores, review.quality_rating]
}
return scores
}, [])
const ripe = formatMonthList(summary.fruiting.ripe)
if (ripe) {
stats.push(
<span key="ripe">
{t('locations.infowindow.fruiting.2')} – {ripe}
</span>,
)
}

const yieldScores = reviews.reduce((scores, review) => {
if (review.yield_rating) {
return [...scores, review.yield_rating]
}
return scores
}, [])
if (stats.length === 0) {
return null
}

return (
<SummaryTable>
<h3>Summary</h3>
<tbody>
<FruitingSummary reviews={reviews} />
<SummaryRow
title={t('glossary.quality')}
scores={qualityScores}
total={5}
/>
<SummaryRow
title={t('glossary.yield')}
scores={yieldScores}
total={5}
/>
</tbody>
</SummaryTable>
<StatsContainer>
<StatsRow>
{[stats[0], stats[1]].filter(Boolean).map((stat, i) => (
<>
{i > 0 && <Separator />}
{stat}
</>
))}
</StatsRow>
<StatsRow>
{stats.slice(2).map((stat, i) => (
<>
{i > 0 && <Separator />}
{stat}
</>
))}
</StatsRow>
</StatsContainer>
)
}

Expand Down
Loading
Loading