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

Add Reef check surveys in Site page #1064

Merged
merged 18 commits into from
Dec 20, 2024
Merged
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
4 changes: 4 additions & 0 deletions packages/website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `yarn scan`

Runs the app in development mode and scans the application for performance issues using `react-scan`.

### `yarn eject`

**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
Expand Down
5 changes: 3 additions & 2 deletions packages/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test": "react-scripts --expose-gc test --logHeapUsage --maxWorkers=4",
"eject": "react-scripts eject",
"lint": "eslint --ext .js,.jsx,.ts,.tsx ./src --ignore-path .gitignore --max-warnings 0",
"scan": "yarn start & npx react-scan@latest localhost:3000",
"precommit": "lint-staged",
"deploy:prod": "dotenv -e .env.prod yarn build && yarn run firebase deploy --only hosting:prod-target",
"deploy:staging": "dotenv -e .env.staging yarn build && yarn run firebase deploy --only hosting:staging-target",
Expand All @@ -26,7 +27,7 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/user-event": "^7.1.2",
"@types/leaflet.locatecontrol": "^0.74.4",
"axios": "^1.7.4",
"axios": "^1.7.8",
"axios-cache-interceptor": "^1.6.0",
"chart.js": "^4.4.3",
"chartjs-adapter-date-fns": "^3.0.0",
Expand All @@ -39,7 +40,7 @@
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"firebase": "^10.11.0",
"firebase-tools": "^13.11.3",
"firebase-tools": "^13.28.0",
"geolib": "^3.3.4",
"immutable": "^4.0.0-rc.12",
"leaflet": "^1.7.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { render } from '@testing-library/react';
import { mockReefCheckSurvey } from 'mocks/mockReefCheckSurvey';
import { ReefCheckSurvey } from 'store/ReefCheckSurveys';
import { BrowserRouter } from 'react-router-dom';
import { ReefCheckSurveyCard } from '.';

describe('ReefCheckSurveyCard', () => {
function renderReefCheckSurveyCard(overrides: Partial<ReefCheckSurvey> = {}) {
return render(
<BrowserRouter>
<ReefCheckSurveyCard
survey={{ ...mockReefCheckSurvey, ...overrides }}
/>
,
</BrowserRouter>,
);
}

it('should render date', () => {
const { getByText } = renderReefCheckSurveyCard();

expect(
getByText(`Date: ${new Date(mockReefCheckSurvey.date).toLocaleString()}`),
).toBeInTheDocument();
});

it('should render user if submittedBy is present', () => {
const { getByText } = renderReefCheckSurveyCard({
submittedBy: 'Test User',
});
expect(getByText('User: Test User')).toBeInTheDocument();
});

it('should render table with correct number of rows', () => {
const { container } = renderReefCheckSurveyCard();

expect(container.querySelectorAll('mock-tablerow').length).toBe(3);
});

it('should show correct counts in headers', () => {
const { container } = renderReefCheckSurveyCard();
const headers = [
...container.querySelectorAll('mock-tablehead mock-tablecell').values(),
].map((el) => el.textContent);
expect(headers).toEqual(
expect.arrayContaining([
'FISH (2)',
'Count',
'INVERTEBRATES (2)',
'Count',
'BLEACHING AND CORAL DIDEASES',
'YES/NO',
'IMPACT',
'YES/NO',
]),
);
});

it('should display link to survey details', () => {
const { getByRole } = renderReefCheckSurveyCard();

expect(
getByRole(
(role, element) =>
role === 'link' && element?.textContent === 'VIEW DETAILS',
),
).toHaveAttribute('href', `/reef_check_survey/${mockReefCheckSurvey.id}`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React from 'react';
import {
Box,
Button,
createStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Theme,
Typography,
WithStyles,
withStyles,
} from '@material-ui/core';
import { groupBy, times } from 'lodash';
import { Link } from 'react-router-dom';
import cls from 'classnames';
import { reefCheckImpactRows, ReefCheckSurvey } from 'store/ReefCheckSurveys';

type ReefCheckSurveyCardIncomingProps = {
survey: ReefCheckSurvey;
};

const ReefCheckSurveyCardComponent = ({
survey,
classes,
}: ReefCheckSurveyCardProps) => {
const stats = groupBy(
// eslint-disable-next-line fp/no-mutating-methods
survey.organisms
.map((organism) => ({
...organism,
count: organism.s1 + organism.s2 + organism.s3 + organism.s4,
}))
.filter(
({ count, type }) =>
// Filter out fish and invertebrates with no count
count > 0 || type === 'Impact' || type === 'Bleaching',
)
.sort((a, b) => b.count - a.count),
({ type, organism }) => {
if (type === 'Impact') {
return reefCheckImpactRows.includes(organism) ? 'Impact' : 'Bleaching';
}
return type;
},
);

const rowCount = Math.max(
stats.Fish?.length ?? 0,
stats.Invertebrate?.length ?? 0,
stats.Bleaching?.length ?? 0,
stats.Impact?.length ?? 0,
);

return (
<Paper className={classes.paper}>
<Box display="flex" justifyContent="space-between">
<Typography>Date: {new Date(survey.date).toLocaleString()}</Typography>
{survey.submittedBy && (
<Typography>User: {survey.submittedBy}</Typography>
)}
</Box>
<TableContainer className={classes.tableRoot}>
<Table size="small">
<TableHead>
<TableRow className={classes.header}>
<TableCell className={classes.label}>
FISH ({stats.Fish?.length ?? 0})
</TableCell>
<TableCell>Count</TableCell>
<TableCell className={cls(classes.label, classes.noWrap)}>
INVERTEBRATES ({stats.Invertebrate?.length ?? 0})
</TableCell>
<TableCell>Count</TableCell>
<TableCell className={classes.label}>
BLEACHING AND CORAL DIDEASES
</TableCell>
<TableCell>YES/NO</TableCell>
<TableCell className={classes.label}>IMPACT</TableCell>
<TableCell>YES/NO</TableCell>
</TableRow>
</TableHead>
<TableBody>
{times(rowCount).map((i) => (
<TableRow key={i}>
<TableCell className={classes.label}>
{stats.Fish?.[i]?.organism}
</TableCell>
<TableCell>{stats.Fish?.[i]?.count}</TableCell>
<TableCell className={classes.label}>
{stats.Invertebrate?.[i]?.organism}
</TableCell>
<TableCell>{stats.Invertebrate?.[i]?.count}</TableCell>
<TableCell className={classes.label}>
{stats.Bleaching?.[i]?.organism}
</TableCell>
<TableCell>
{formatImpactCount(stats.Bleaching?.[i]?.count)}
</TableCell>
<TableCell className={classes.label}>
{stats.Impact?.[i]?.organism}
</TableCell>
<TableCell>
{formatImpactCount(stats.Impact?.[i]?.count)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>

<Box marginTop={2}>
<Link to={`reef_check_survey/${survey.id}`}>
<Button size="small" variant="outlined" color="primary">
VIEW DETAILS
</Button>
</Link>
</Box>
</Paper>
);
};

const formatImpactCount = (count?: number) => {
if (count === undefined) {
return '';
}
return count > 0 ? 'YES' : 'NO';
};

const styles = (theme: Theme) =>
createStyles({
paper: {
padding: 16,
color: theme.palette.text.secondary,
maxWidth: '100%',
},
label: {
backgroundColor: '#FAFAFA',
},
tableRoot: {
maxHeight: 200,
},
header: {
'& th': {
borderBottom: '1px solid black',
},
},
noWrap: {
whiteSpace: 'nowrap',
},
});

type ReefCheckSurveyCardProps = ReefCheckSurveyCardIncomingProps &
WithStyles<typeof styles>;

export const ReefCheckSurveyCard = withStyles(styles)(
ReefCheckSurveyCardComponent,
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import classNames from 'classnames';
import grey from '@material-ui/core/colors/grey';

import { displayTimeInLocalTimezone } from 'helpers/dates';
import { times } from 'lodash';
import { SurveyListItem } from 'store/Survey/types';
import AddButton from '../AddButton';
import SurveyCard from '../SurveyCard';
import LoadingSkeleton from '../../../LoadingSkeleton';
import incomingStyles from '../styles';
import { TimelineProps } from './types';
import { ReefCheckSurveyCard } from '../ReefCheckSurveyCard';

const CONNECTOR_COLOR = grey[500];

Expand Down Expand Up @@ -48,7 +51,7 @@ const TimelineDesktop = ({
</TimelineContent>
</TimelineItem>
)}
{surveys.map((survey, index) => (
{(loading ? times(2, () => null) : surveys).map((survey, index) => (
<TimelineItem
key={survey?.id || `loading-survey-${index}`}
className={classes.timelineItem}
Expand All @@ -58,13 +61,13 @@ const TimelineDesktop = ({
className={classes.dateSkeleton}
loading={loading}
variant="text"
width="30%"
lines={1}
width={80}
>
{survey?.diveDate && (
{survey?.date && (
<Typography variant="h6" className={classes.dates}>
{displayTimeInLocalTimezone({
isoDate: survey.diveDate,
isoDate: survey.date,
format: 'LL/dd/yyyy',
displayTimezone: false,
timeZone,
Expand All @@ -79,14 +82,19 @@ const TimelineDesktop = ({
<hr className={classes.connector} />
</TimelineSeparator>
<TimelineContent className={classes.surveyCardWrapper}>
<SurveyCard
pointId={pointId}
pointName={pointName}
isAdmin={isAdmin}
siteId={siteId}
survey={survey}
loading={loading}
/>
{(survey?.type === 'survey' || loading) && (
<SurveyCard
pointId={pointId}
pointName={pointName}
isAdmin={isAdmin}
siteId={siteId}
survey={survey as SurveyListItem}
loading={loading}
/>
)}
{survey?.type === 'reefCheckSurvey' && (
<ReefCheckSurveyCard survey={survey} />
)}
</TimelineContent>
</TimelineItem>
))}
Expand All @@ -100,7 +108,7 @@ const useStyles = makeStyles((theme: Theme) => ({
alignItems: 'center',
},
timelineOppositeContent: {
flex: 0.5,
flex: '0 0 130px',
},
addNewButtonOpposite: {
padding: theme.spacing(0, 1.25),
Expand Down
Loading
Loading