Skip to content

Commit

Permalink
Merge pull request #67 from aleinin/improve-csv-export
Browse files Browse the repository at this point in the history
Improve csv export
  • Loading branch information
aleinin authored Nov 16, 2023
2 parents 9020d0a + 3392715 commit 35d2652
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 69 deletions.
2 changes: 1 addition & 1 deletion goty-client/src/components/controls/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const TabButtons = ({
key={tab}
onClick={onChangeCallback(tab)}
>
{tab}
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</Button>
)
})}
Expand Down
29 changes: 29 additions & 0 deletions goty-client/src/components/results/ExportButton/ExportButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import styles from '../ResultsPage.module.scss'
import { Export } from '../../../icons/export/Export'
import { Button } from '../../controls/Button/Button'
import React from 'react'
import { downloadCSV } from '../downloadCSV'
import { useSelector } from 'react-redux'
import {
selectResults,
selectSubmissions,
} from '../../../state/results/selectors'
import { selectProperties } from '../../../state/properties/selectors'
import { createExportData } from './createExportData'

export const ExportButton = () => {
const results = useSelector(selectResults)
const submissions = useSelector(selectSubmissions)
const properties = useSelector(selectProperties)
const handleExport = () => {
downloadCSV(
`goty-${properties.year}-results.csv`,
createExportData(results, submissions, properties),
)
}
return (
<Button isIcon className={styles.exportButton} onClick={handleExport}>
<Export />
</Button>
)
}
138 changes: 138 additions & 0 deletions goty-client/src/components/results/ExportButton/createExportData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { GameResult } from '../../../models/gameResult'
import { GameOfTheYearResult } from '../../../models/gameOfTheYearResult'
import { Game } from '../../../models/game'
import { Results } from '../../../models/results'
import { Submission } from '../../../models/submission'
import { Properties } from '../../../models/properties'
import { indexToOrdinal } from '../../../util/index-to-ordinal'
import { Column, Section } from '../downloadCSV'

const gameColumns: Column<GameResult>[] = [
{
label: 'Rank',
accessorFn: (game: GameResult) => game.rank + 1,
},
{
label: 'Title',
accessorKey: 'title',
},
{
label: 'Votes',
accessorKey: 'votes',
},
]
const gotyColumns: Column<GameOfTheYearResult>[] = [
...gameColumns,
{
label: 'Points',
accessorKey: 'points',
},
]
const gameAccessor = (game: Game | null) => game?.title ?? ''

const createTextSection = (label: string, data: any[]): Section<any> => ({
columns: [{ label }],
data,
})

const createGameSection = (
title: string,
data: GameResult[],
): Section<GameResult> => ({
title,
columns: gameColumns,
data,
})

const createGotySection = (
title: string,
data: GameOfTheYearResult[],
): Section<GameOfTheYearResult> => ({
title,
columns: gotyColumns,
data,
})

const createPropertiesSection = (
title: string,
data: Properties,
): Section<Properties> => ({
title,
data,
columns: [
{
label: 'gotyYear',
accessorKey: 'year',
},
{
label: 'deadline',
accessorKey: 'deadline',
},
{
label: 'hasGiveaway',
accessorKey: 'hasGiveaway',
},
{
label: 'giveawayAmountUSD',
accessorKey: 'giveawayAmountUSD',
},
],
})

const createSubmissionsSection = (
title: string,
data: Submission[],
maxGamesOfTheYear: number,
): Section<Submission> => ({
title,
data,
columns: [
{
label: 'ID',
accessorKey: 'submissionUUID',
},
{
label: 'Name',
accessorKey: 'name',
},
{
label: 'Best Old Game',
accessorFn: (submission: Submission) =>
gameAccessor(submission.bestOldGame),
},
{
label: 'Most Anticipated',
accessorFn: (submission: Submission) =>
gameAccessor(submission.mostAnticipated),
},
{
label: 'Entered Giveaway',
accessorFn: (submission: Submission) =>
submission.enteredGiveaway ? 'Yes' : 'No',
},
...[...Array(maxGamesOfTheYear)].map((_, index) => ({
label: indexToOrdinal(index),
accessorFn: (submission: Submission) =>
submission.gamesOfTheYear[index]?.title ?? '',
})),
],
})

export const createExportData = (
results: Results,
submissions: Submission[],
properties: Properties,
) => [
createTextSection('Respondents', results.participants),
createGotySection('Games of the Year', results.gamesOfTheYear),
createGameSection('Old Game', results.bestOldGames),
createGameSection('Most Anticipated', results.mostAnticipated),
createTextSection('Giveaway Entries', results.giveawayParticipants),
createSubmissionsSection(
'Submissions',
submissions,
properties.maxGamesOfTheYear,
),
createPropertiesSection('Properties', properties),
createTextSection('Tie points', properties.tiePoints),
]
23 changes: 6 additions & 17 deletions goty-client/src/components/results/ResultsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import React, { useEffect } from 'react'
import { useSelector, useStore } from 'react-redux'
import { useStore } from 'react-redux'
import { useNavigate } from 'react-router'
import { Outlet, useLocation } from 'react-router-dom'
import { loadResultsAndSubmissions } from '../../state/results/middleware'
import { Card } from '../controls/Card/Card'
import { TabButtons } from '../controls/Tabs/Tabs'
import { Button } from '../controls/Button/Button'
import styles from './ResultsPage.module.scss'
import { Export } from '../../icons/export/Export'
import { downloadCSV } from './downloadCSV'
import { selectProperties } from '../../state/properties/selectors'
import { selectSubmissions } from '../../state/results/selectors'
import { ExportButton } from './ExportButton/ExportButton'

export enum Tabs {
SUMMARY = 'Summary',
RESPONSES = 'Responses',
SUMMARY = 'summary',
RESPONSES = 'responses',
}

const tabs = [Tabs.SUMMARY, Tabs.RESPONSES]
Expand All @@ -38,21 +33,15 @@ export const ResultsPage = () => {
const navigate = useNavigate()
const { pathname } = useLocation()
const activeTab = getActiveTab(pathname)
const properties = useSelector(selectProperties)
const submissions = useSelector(selectSubmissions)
useEffect(() => {
document.title = `TMW GOTY - ${activeTab}`
}, [activeTab])
const handleTabChange = (tab: string) => navigate(`${tab}`)
const handleExport = () => {
downloadCSV(submissions, properties)
}

return (
<>
<Card style={{ position: 'relative' }}>
<Button isIcon className={styles.exportButton} onClick={handleExport}>
<Export />
</Button>
<ExportButton />
<TabButtons
tabs={tabs}
onChange={handleTabChange}
Expand Down
100 changes: 49 additions & 51 deletions goty-client/src/components/results/downloadCSV.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Submission } from '../../models/submission'
import { Properties } from '../../models/properties'
import { indexToOrdinal } from '../../util/index-to-ordinal'
export interface Column<T> {
label: string
accessorFn?: (obj: T) => any
accessorKey?: keyof T
}
export interface Section<T> {
title?: string
columns: Column<T>[]
data: T[] | T
}

const TEXT_CSV = 'text/csv'
export const downloadCSV = (
submissions: Submission[],
properties: Properties,
) => {
const fileName = `goty-${properties.year}-results.csv`
const csvString = getCSVData(submissions, properties)
type CSV = any[][]
export const downloadCSV = (fileName: string, sections: Section<any>[]) => {
const csvString = getCSVData(sections)
const blob = new Blob([csvString], {
type: TEXT_CSV,
})
Expand All @@ -24,53 +28,47 @@ export const downloadCSV = (
aTag.remove()
}

const submissionsHeaders = [
'ID',
'Name',
'Best Old Game',
'Most Anticipated',
'Entered Giveaway',
]
const propertiesHeaders = 'Properties'

const getCSVData = (
submissions: Submission[],
properties: Properties,
): string => {
// Submissions
const firstRow = [
...submissionsHeaders,
...[...Array(properties.maxGamesOfTheYear)].map((_, index) =>
indexToOrdinal(index),
),
]
const rows = []
rows.push(firstRow)
submissions.forEach((submission) => {
const row = []
row.push(submission.submissionUUID)
row.push(submission.name)
row.push(submission.bestOldGame?.title ?? '')
row.push(submission.mostAnticipated?.title ?? '')
row.push(submission.enteredGiveaway ? 'Yes' : 'No')
submission.gamesOfTheYear.forEach((game) => {
row.push(game.title)
})
rows.push(row)
const getCSVData = (sections: Section<any>[]): string => {
const rows: CSV = []
sections.forEach((section) => {
addSection(rows, section.columns, section.data, section.title)
addEmptyRow(rows)
})
rows.push([]) // intentional space
// Properties
rows.push([propertiesHeaders])
rows.push(['gotyYear', properties.year])
rows.push(['deadline', properties.deadline])
rows.push(['hasGiveaway', properties.hasGiveaway])
rows.push(['giveawayAmountUSD', properties.giveawayAmountUSD])
rows.push(['tiePoints', ...properties.tiePoints])
padRowsWithCorrectNumberOfColumns(rows)
return rows.map((row) => row.join(',')).join('\n')
}

const padRowsWithCorrectNumberOfColumns = (rows: any[][]) => {
const addSection = <T>(
rows: CSV,
columns: Column<T>[],
data: T[] | T,
title?: string,
) => {
title && rows.push([title])
rows.push(columns.map((column) => column.label))
const addRow = (dataRow: T) => {
const row: any[] = []
columns.forEach((column) => {
if (column.accessorKey != null) {
row.push(dataRow[column.accessorKey])
} else if (column.accessorFn != null) {
row.push(column.accessorFn(dataRow))
} else {
row.push(dataRow)
}
})
rows.push(row)
}
if (Array.isArray(data)) {
data.forEach(addRow)
} else {
addRow(data)
}
}

const addEmptyRow = (rows: CSV) => rows.push([])

const padRowsWithCorrectNumberOfColumns = (rows: CSV) => {
const numberOfColumns = rows.reduce(
(mostColumns: number, columns: any[]) =>
columns.length > mostColumns ? columns.length : mostColumns,
Expand Down
1 change: 1 addition & 0 deletions goty-client/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default defineConfig(() => {
return {
build: {
outDir: 'build',
target: 'esnext',
},
plugins: [
react(),
Expand Down

0 comments on commit 35d2652

Please sign in to comment.