Skip to content

Commit

Permalink
Speakers company/geolocation/job title stats & fix dev log error for …
Browse files Browse the repository at this point in the history
…rerender (#90)

* Add speakers stats

* Remove unused imports

* Fix error in dev with rerender on Sessions and Speakers

* Improve stats sort and calc

* Remove unused improt
  • Loading branch information
HugoGresse authored Apr 5, 2024
1 parent 3d26437 commit 9327c98
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 16 deletions.
31 changes: 17 additions & 14 deletions src/events/page/sessions/list/EventSessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Typography,
} from '@mui/material'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useMemo, useState } from 'react'
import { Event, Session } from '../../../../types'
import { useSessions } from '../../../../services/hooks/useSessions'
import { FirestoreQueryLoaderAndErrorDisplay } from '../../../../components/FirestoreQueryLoaderAndErrorDisplay'
Expand All @@ -33,27 +33,25 @@ export const EventSessions = ({ event }: EventSessionsProps) => {
const [searchParams, setSearchParams] = useSearchParams()
const sessions = useSessions(event)
const [sessionsImportOpen, setSessionsImportOpen] = useState(false)
const [displayedSessions, setDisplayedSessions] = useState<Session[]>([])
const [search, setSearch] = useState<string>('')
const [selectedCategory, setSelectedCategory] = useState<string>(searchParams.get('category') || '')
const [selectedFormat, setSelectedFormat] = useState<string>(searchParams.get('format') || '')
const [onlyWithoutSpeaker, setOnlyWithoutSpeaker] = useState<boolean>(false)
const [generateDialogOpen, setGenerateDialogOpen] = useState(false)

const sessionsData = sessions.data || []
const isFiltered = displayedSessions.length !== sessionsData.length
const sessionsData = useMemo(() => sessions.data || [], [sessions.data])

useEffect(() => {
setDisplayedSessions(
filterSessions(sessionsData, {
search,
category: selectedCategory,
format: selectedFormat,
withoutSpeaker: onlyWithoutSpeaker,
})
)
const displayedSessions = useMemo(() => {
return filterSessions(sessionsData, {
search,
category: selectedCategory,
format: selectedFormat,
withoutSpeaker: onlyWithoutSpeaker,
})
}, [sessionsData, search, selectedCategory, selectedFormat, onlyWithoutSpeaker])

const isFiltered = displayedSessions.length !== sessionsData.length

if (sessions.isLoading) {
return <FirestoreQueryLoaderAndErrorDisplay hookResult={sessions} />
}
Expand Down Expand Up @@ -89,7 +87,12 @@ export const EventSessions = ({ event }: EventSessionsProps) => {
<InputAdornment position="start">
<IconButton
aria-label="Clear filters"
onClick={() => setDisplayedSessions(displayedSessions)}
onClick={() => {
setSearch('')
setSearchParams({})
setSelectedCategory('')
setSelectedFormat('')
}}
edge="end">
<Clear />
</IconButton>
Expand Down
16 changes: 14 additions & 2 deletions src/events/page/speakers/EventSpeakers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, Card, Container, Grid, IconButton, InputAdornment, TextField, Typography } from '@mui/material'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Event, Speaker } from '../../../types'
import { FirestoreQueryLoaderAndErrorDisplay } from '../../../components/FirestoreQueryLoaderAndErrorDisplay'
import { EventSpeakerItem } from './EventSpeakerItem'
Expand All @@ -9,6 +9,7 @@ import { RequireConferenceHallConnections } from '../../../components/RequireCon
import { SpeakersFromConferenceHallUpdaterDialog } from './components/SpeakersFromConferenceHallUpdaterDialog'
import { Clear } from '@mui/icons-material'
import { useSessionsRaw } from '../../../services/hooks/useSessions'
import { SpeakersStatsDialog } from './components/SpeakersStatsDialog'

export type EventSpeakersProps = {
event: Event
Expand All @@ -17,10 +18,11 @@ export const EventSpeakers = ({ event }: EventSpeakersProps) => {
const speakers = useSpeakers(event.id)
const sessions = useSessionsRaw(event.id)
const [updaterDialogOpen, setUpdaterDialogOpen] = useState(false)
const [speakersStatsOpen, setSpeakersStatsOpen] = useState(false)
const [displayedSpeakers, setDisplayedSpeakers] = useState<Speaker[]>([])
const [search, setSearch] = useState<string>('')

const speakersData = speakers.data || []
const speakersData = useMemo(() => speakers.data || [], [speakers.data])
const isFiltered = displayedSpeakers.length !== speakersData.length

useEffect(() => {
Expand Down Expand Up @@ -53,6 +55,7 @@ export const EventSpeakers = ({ event }: EventSpeakersProps) => {
Update speakers infos from ConferenceHall
</Button>
</RequireConferenceHallConnections>
<Button onClick={() => setSpeakersStatsOpen(true)}>Speakers stats</Button>
<Button href="/speakers/new" variant="contained">
Add speaker
</Button>
Expand Down Expand Up @@ -97,6 +100,15 @@ export const EventSpeakers = ({ event }: EventSpeakersProps) => {
}}
/>
)}
{speakersStatsOpen && (
<SpeakersStatsDialog
isOpen={speakersStatsOpen}
onClose={() => {
setSpeakersStatsOpen(false)
}}
speakers={speakers.data || []}
/>
)}
</Container>
)
}
86 changes: 86 additions & 0 deletions src/events/page/speakers/components/SpeakersStatsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Speaker } from '../../../../types'
import { Button, Dialog, DialogContent, Grid, Typography } from '@mui/material'
import * as React from 'react'
import { useMemo } from 'react'

const aggregateFields = <K extends keyof Speaker>(speakers: Speaker[], field: K) => {
const aggregate = speakers.reduce((acc: Record<string, number>, speaker) => {
const key = speaker[field] ? String(speaker[field]) : 'N/A'
if (key) {
acc[key] = acc[key] ? acc[key] + 1 : 1
}
return acc
}, {})

return Object.entries(aggregate)
.map(([key, value]) => ({
key,
value,
}))
.sort((a, b) => b.value - a.value)
}
export const SpeakersStatsDialog = ({
isOpen,
onClose,
speakers,
}: {
isOpen: boolean
onClose: () => void
speakers: Speaker[]
}) => {
const stats = useMemo(() => {
return {
companies: aggregateFields(speakers, 'company'),
jobTitles: aggregateFields(speakers, 'jobTitle'),
geolocations: aggregateFields(speakers, 'geolocation'),
}
}, [speakers])

return (
<Dialog open={isOpen} onClose={onClose} maxWidth="lg" fullWidth={true} scroll="body">
<DialogContent>
<Typography variant="h5">Speakers stats</Typography>

<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="h6">Companies ({Object.keys(stats.companies).length})</Typography>

<ul>
{stats.companies.map((company) => (
<li key={company.key}>
{company.key} ({company.value})
</li>
))}
</ul>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h6">Job titles ({Object.keys(stats.jobTitles).length})</Typography>

<ul>
{stats.jobTitles.map((jobTitle) => (
<li key={jobTitle.key}>
{jobTitle.key} ({jobTitle.value})
</li>
))}
</ul>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h6">Geolocations ({Object.keys(stats.geolocations).length})</Typography>

<ul>
{stats.geolocations.map((geolocation) => (
<li key={geolocation.key}>
{geolocation.key} ({geolocation.value})
</li>
))}
</ul>
</Grid>
</Grid>

<Button variant="outlined" onClick={onClose}>
Close
</Button>
</DialogContent>
</Dialog>
)
}

0 comments on commit 9327c98

Please sign in to comment.