Skip to content

Commit

Permalink
feat: screen for empty observation and observation list (#84)
Browse files Browse the repository at this point in the history
* Adds button in settings tab to create test data.
  • Loading branch information
cimigree authored Jan 22, 2025
1 parent 9d90a5e commit 5999020
Show file tree
Hide file tree
Showing 38 changed files with 1,002 additions and 69 deletions.
28 changes: 28 additions & 0 deletions messages/renderer/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@
"components.OnboardingTopMenu.step": {
"message": "Step {number}"
},
"emptyState.inviteDevices": {
"message": "Invite Devices"
},
"emptyState.noObservationsFound": {
"message": "No Observations Found"
},
"mapMain.errorLoading": {
"message": "Oops! Error loading data."
},
"mapMain.loading": {
"message": "Loading..."
},
"mapMain.unnamedProject": {
"message": "Unnamed Project"
},
"observationListView.button.exchange": {
"message": "View Exchange"
},
"observationListView.button.team": {
"message": "View Team"
},
"screens.CreateJoinProjectScreen.askToJoin": {
"message": "Ask a monitoring coordinator to join their Project."
},
Expand Down Expand Up @@ -98,6 +119,13 @@
"screens.JoinProjectScreen.title": {
"message": "Join"
},
"screens.Observation.ObservationView.observation": {
"description": "Default name of observation with no matching preset",
"message": "Observation"
},
"screens.ObservationList.TrackListItem.Track": {
"message": "Track"
},
"screens.OnboardingPrivacyPolicy.permissionsTitle": {
"message": "Current Permissions"
},
Expand Down
29 changes: 29 additions & 0 deletions src/renderer/src/Theme.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { createTheme } from '@mui/material/styles'

import {
ALMOST_BLACK,
COMAPEO_BLUE,
DARKER_ORANGE,
DARK_COMAPEO_BLUE,
DARK_GREY,
DARK_ORANGE,
GREEN,
LIGHT_COMAPEO_BLUE,
ORANGE,
RED,
WHITE,
} from './colors'

declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
darkOrange: true
}
}

const theme = createTheme({
typography: {
fontFamily: 'Rubik, sans-serif',
Expand Down Expand Up @@ -44,6 +54,10 @@ const theme = createTheme({
},
},
palette: {
text: {
primary: ALMOST_BLACK,
secondary: DARK_GREY,
},
primary: {
main: COMAPEO_BLUE,
dark: DARK_COMAPEO_BLUE,
Expand All @@ -59,9 +73,24 @@ const theme = createTheme({
error: {
main: RED,
},
background: {
default: WHITE,
},
},
components: {
MuiButton: {
variants: [
{
props: { variant: 'darkOrange' },
style: {
backgroundColor: DARK_ORANGE,
color: WHITE,
'&:hover': {
backgroundColor: DARKER_ORANGE,
},
},
},
],
defaultProps: {
variant: 'contained',
},
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ export const DARK_COMAPEO_BLUE = '#050F77'
export const LIGHT_COMAPEO_BLUE = '#CCE0FF'

export const CORNFLOWER_BLUE = '#80A0FF'
export const DARK_BLUE = '#000033'
export const BLUE_GREY = '#CCCCD6'
export const DARK_GREY = '#757575'
export const VERY_LIGHT_GREY = '#ededed'

export const ORANGE = '#FF9933'
export const DARK_ORANGE = '#E86826'
export const DARKER_ORANGE = '#D95F28'

export const GREEN = '#59A553'
export const RED = '#D92222'
export const BLACK = '#000000'
export const ALMOST_BLACK = '#333333'
export const WHITE = '#ffffff'
10 changes: 7 additions & 3 deletions src/renderer/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import {
type MouseEventHandler,
type PropsWithChildren,
} from 'react'
import { Button as MuiButton } from '@mui/material'
import { Button as MuiButton, type ButtonProps } from '@mui/material'

type CustomButtonProps = PropsWithChildren<{
name?: string
className?: string
color?: 'primary' | 'secondary' | 'success' | 'error'
color?: ButtonProps['color']
size?: 'medium' | 'large' | 'fullWidth'
testID?: string
variant?: 'contained' | 'outlined' | 'text'
variant?: 'contained' | 'outlined' | 'text' | 'darkOrange'
style?: CSSProperties
onClick?: MouseEventHandler<HTMLButtonElement>
disabled?: boolean
startIcon?: React.ReactNode
endIcon?: React.ReactNode
}>

export const Button = ({
Expand All @@ -26,11 +28,13 @@ export const Button = ({
style,
disabled,
className,
startIcon,
...props
}: CustomButtonProps) => {
const propsBasedOnSize = size === 'fullWidth' ? { fullWidth: true } : { size }
return (
<MuiButton
startIcon={startIcon}
className={className}
color={color}
variant={variant}
Expand Down
23 changes: 23 additions & 0 deletions src/renderer/src/components/FormattedData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react'
import type { Preset } from '@comapeo/schema'
import { defineMessages, useIntl } from 'react-intl'

const m = defineMessages({
observation: {
// Keep id stable for translations
id: 'screens.Observation.ObservationView.observation',
defaultMessage: 'Observation',
description: 'Default name of observation with no matching preset',
},
})

// Format the translated preset name, with a fallback to "Observation" if no
// preset is defined
export const FormattedPresetName = ({ preset }: { preset?: Preset }) => {
const { formatMessage: t } = useIntl()
const name = preset
? t({ id: `presets.${preset.docId}.name`, defaultMessage: preset.name })
: t(m.observation)

return <React.Fragment>{name}</React.Fragment>
}
88 changes: 88 additions & 0 deletions src/renderer/src/components/Observations/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { styled } from '@mui/material/styles'
import { defineMessages, useIntl } from 'react-intl'

import { ALMOST_BLACK, BLUE_GREY, VERY_LIGHT_GREY } from '../../colors'
import AddPersonIcon from '../../images/AddPerson.svg'
import EmptyStateImage from '../../images/empty_state.png'
import { Button } from '../Button'
import { Text } from '../Text'

const m = defineMessages({
inviteDevices: {
id: 'emptyState.inviteDevices',
defaultMessage: 'Invite Devices',
},
noObservationsFound: {
id: 'emptyState.noObservationsFound',
defaultMessage: 'No Observations Found',
},
})

const Container = styled('div')({
display: 'flex',
flexDirection: 'column',
padding: '25px 20px',
})

const DividerLine = styled('div')({
width: '100%',
height: 1,
backgroundColor: VERY_LIGHT_GREY,
})

const Circle = styled('div')({
width: 260,
height: 260,
borderRadius: '50%',
backgroundColor: 'rgba(0, 102, 255, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})

const StyledImage = styled('img')({
width: 120,
height: 120,
})

const LowerContainer = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '75%',
justifyContent: 'center',
gap: 18,
})

type EmptyStateProps = {
onInviteDevices?: () => void
}

export function EmptyState({ onInviteDevices }: EmptyStateProps) {
const { formatMessage } = useIntl()

return (
<>
<Container>
<Button
variant="outlined"
style={{
borderColor: BLUE_GREY,
color: ALMOST_BLACK,
}}
onClick={() => onInviteDevices?.()}
startIcon={<AddPersonIcon color={ALMOST_BLACK} />}
>
{formatMessage(m.inviteDevices)}
</Button>
</Container>
<DividerLine />
<LowerContainer>
<Circle>
<StyledImage src={EmptyStateImage} alt="Empty Observations List" />
</Circle>
<Text kind="body">{formatMessage(m.noObservationsFound)}</Text>
</LowerContainer>
</>
)
}
84 changes: 84 additions & 0 deletions src/renderer/src/components/Observations/ObservationListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Observation } from '@comapeo/schema'
import { styled } from '@mui/material/styles'
import { FormattedDate, FormattedTime } from 'react-intl'

import { VERY_LIGHT_GREY } from '../../colors'
import { useActiveProjectIdStoreState } from '../../contexts/ActiveProjectIdProvider'
import { useObservationWithPreset } from '../../hooks/useObservationWithPreset'
import { FormattedPresetName } from '../FormattedData'
import { PresetCircleIcon } from '../PresetCircleIcon'
import { Text } from '../Text'

type Props = {
observation: Observation
onClick?: () => void
}

const Container = styled('div')({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderBottom: `1px solid ${VERY_LIGHT_GREY}`,
padding: '10px 20px',
cursor: 'pointer',
width: '100%',
'&:hover': {
backgroundColor: '#f9f9f9',
},
})

const TextContainer = styled('div')({
flex: 1,
display: 'flex',
flexDirection: 'column',
})

const PhotoContainer = styled('img')({
width: 48,
height: 48,
borderRadius: 6,
objectFit: 'cover',
})

export function ObservationListItem({ observation, onClick }: Props) {
const projectId = useActiveProjectIdStoreState((s) => s.activeProjectId)
// TODO: Ideally, the fallback shouldn't be necessary
const preset = useObservationWithPreset(observation, projectId ?? '')
const createdAt = observation.createdAt
? new Date(observation.createdAt)
: new Date()

const photoAttachment = observation.attachments.find(
(att) => att.type === 'photo',
)

return (
<Container onClick={onClick}>
<TextContainer>
<Text bold>
<FormattedPresetName preset={preset} />
</Text>
<Text kind="caption">
<FormattedDate
value={createdAt}
month="short"
day="2-digit"
year="numeric"
/>
{', '}
<FormattedTime value={createdAt} hour="numeric" minute="2-digit" />
</Text>
</TextContainer>
{photoAttachment ? (
<PhotoContainer src="/path/to/mock/photo.jpg" alt="Observation photo" />
) : (
<PresetCircleIcon
projectId={projectId}
iconId={preset?.iconRef?.docId}
borderColor={preset?.color}
size="medium"
/>
)}
</Container>
)
}
Loading

0 comments on commit 5999020

Please sign in to comment.