diff --git a/cypress/component/DataTablePreview.cy.tsx b/cypress/component/DataTablePreview.cy.tsx new file mode 100644 index 0000000..0318627 --- /dev/null +++ b/cypress/component/DataTablePreview.cy.tsx @@ -0,0 +1,38 @@ +import DataTablePreview from '../../src/components/DataTablePreview'; +import { mockDataTable, mockInitialColumns } from '../../src/utils/mocks'; + +const props = { + dataTable: mockDataTable, + columns: mockInitialColumns, +}; + +describe('DataTablePreview', () => { + it('should render the component correctly', () => { + cy.mount(); + cy.get('[data-cy="data-table-preview"]').should('be.visible'); + }); + it('verifies the data table preview content', () => { + cy.mount(); + cy.get('[data-cy="data-table-preview"] th:nth-child(2)').should('contain', 'age'); + + // Check the value of the 3rd column in the 3rd row + cy.get('[data-cy="data-table-preview"] tr:nth-child(3) td:nth-child(3)').should('contain', 'M'); + }); + it('should navigate to the 3rd page of the table and verify the content', () => { + cy.mount(); + + // Go the 3rd page of the table + cy.get('[data-cy="datatable-preview-pagination"]').within(() => { + cy.get('button[aria-label="Go to next page"]').click(); + cy.get('button[aria-label="Go to next page"]').click(); + }); + + cy.get('[data-cy="data-table-preview"] th:nth-child(3)').should('contain', 'sex'); + + // Check the value of the 2nd column in the 2nd row + cy.get('[data-cy="data-table-preview"] tr:nth-child(2) td:nth-child(2)').should( + 'contain', + '65' + ); + }); +}); diff --git a/cypress/component/FileUploader.cy.tsx b/cypress/component/FileUploader.cy.tsx new file mode 100644 index 0000000..2929d0d --- /dev/null +++ b/cypress/component/FileUploader.cy.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import FileUploader from '../../src/components/FileUploader'; + +const props = { + displayText: 'Upload your file (.csv)', + handleClickToUpload: () => {}, + handleDrop: () => {}, + handleDragOver: () => {}, + handleFileUpload: () => {}, + fileInputRef: React.createRef(), + allowedFileType: '.csv', +}; + +describe('FileUploader', () => { + it('should render the FileUploader component', () => { + cy.mount( + + ); + cy.get('[data-cy="upload-area"]').should('be.visible'); + cy.get('[data-cy="upload-area"]').should('contain', 'Upload your file (.csv)'); + }); +}); diff --git a/cypress/component/NavigationButton.cy.tsx b/cypress/component/NavigationButton.cy.tsx index 8ef0d80..322cb3f 100644 --- a/cypress/component/NavigationButton.cy.tsx +++ b/cypress/component/NavigationButton.cy.tsx @@ -1,5 +1,5 @@ import NavigationButton from '../../src/components/NavigationButton'; -import useStore from '../../src/stores/store'; +import useStore from '../../src/stores/view'; const props = { label: 'Next', diff --git a/cypress/component/UploadCard.cy.tsx b/cypress/component/UploadCard.cy.tsx new file mode 100644 index 0000000..39a3eba --- /dev/null +++ b/cypress/component/UploadCard.cy.tsx @@ -0,0 +1,75 @@ +import UploadCard from '../../src/components/UploadCard'; + +const exampleFileName = 'ds003653_participant.tsv'; +const exampleFilePath = `examples/${exampleFileName}`; +function MockPreviewComponent() { + return
Preview Component
; +} + +const props = { + title: 'some title', + allowedFileType: '.tsv', + uploadedFileName: exampleFileName, + onFileUpload: () => {}, + previewComponent: , +}; + +describe('UploadCard', () => { + it('should render the component correctly', () => { + cy.mount( + + ); + cy.get('[data-cy="some title-upload-card"]').should('be.visible'); + cy.get('[data-cy="some title-upload-card"]').should('contain', 'some title'); + }); + beforeEach(() => { + cy.mount( + + ); + + // Load the file from the fixtures folder + cy.fixture(exampleFilePath, 'binary').then((fileContent) => { + // Convert the binary content to a Blob + const blob = Cypress.Blob.binaryStringToBlob(fileContent, 'text/tab-separated-values'); + + // Create a File object from the Blob + const testFile = new File([blob], exampleFileName, { type: 'text/tab-separated-values' }); + + // Simulate clicking the upload area + cy.get('[data-cy="upload-area"]').click(); + + // Trigger the file upload by selecting the file + cy.get('input[type="file"]').then((input) => { + // Cast input[0] to HTMLInputElement to access the files property + const fileInput = input[0] as HTMLInputElement; + + // Create a DataTransfer object to simulate the file drop + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(testFile); + fileInput.files = dataTransfer.files; + + // Trigger the change event manually + fileInput.dispatchEvent(new Event('change', { bubbles: true })); + }); + }); + }); + it('should open the preview, and verify the data table preview component is rendered', () => { + cy.get('[data-cy="some title-card-uploaded-file-name"]').should('contain', exampleFileName); + + cy.get('[data-cy="toggle-some title-card-preview"]').click(); + + cy.get('[data-cy="toggle-some title-card-preview"]').should('be.visible'); + }); +}); diff --git a/cypress/fixtures/examples/ds003653_participant.tsv b/cypress/fixtures/examples/ds003653_participant.tsv new file mode 100644 index 0000000..a2d2640 --- /dev/null +++ b/cypress/fixtures/examples/ds003653_participant.tsv @@ -0,0 +1,88 @@ +participant_id age sex group group_dx number_comorbid_dx medload iq session +sub-718211 28.4 M UD MDD 0 0 117.66 1 +sub-718213 24.6 F UD MDD 0 0 109.08 1 +sub-718216 43.6 M UD PDD 1 2 112.98 1 +sub-718217 19.1 F UD PDD 3 0 114.54 1 +sub-718220 38.9 F UD PDD 1 6 107.52 1 +sub-718221 32.5 F UD PDD 4 0 99.72 1 +sub-718315 19.7 F HC HC 0 0 104.4 1 +sub-718318 23 F HC HC 0 0 98.16 1 +sub-718320 25.9 M HC HC 0 0 111.42 1 +sub-718321 31.2 F HC HC 0 0 108.3 1 +sub-718322 38.2 F HC HC 0 0 109.86 1 +sub-718323 36.3 F HC HC 0 0 111.42 1 +sub-718518 33.8 F UD MDD 1 0 108.3 1 +sub-719211 28.3 M UD MDD 1 1 105.96 1 +sub-719215 29.1 F UD MDD 1 1 121.56 1 +sub-719222 25.6 F UD PDD 3 0 110.64 1 +sub-719224 43.2 F UD MDD 3 0 113.76 1 +sub-719225 24.4 F UD PDD 5 1 111.42 1 +sub-719226 36.1 F UD MDD 1 4 114.54 1 +sub-719231 21.8 F UD PDD 0 2 119.22 1 +sub-719232 26.5 F UD MDD 3 3 109.86 1 +sub-719238 20.1 F UD MDD 2 3 109.08 1 +sub-719241 32.2 F UD MDD 4 1 121.56 1 +sub-719245 44 M UD PDD 1 0 117.66 1 +sub-719247 24.1 F UD MDD 2 2 110.64 1 +sub-719311 26 M HC HC 0 0 104.4 1 +sub-719312 36.4 F HC HC 0 0 100.5 1 +sub-719313 25.5 M HCconvertedMDD HCconvertedMDD 0 0 111.42 1 +sub-719315 26.9 F HC HC 0 0 107.52 1 +sub-719316 36.8 F HC HC 0 0 111.42 1 +sub-719318 23.4 F HC HC 0 0 109.08 1 +sub-719319 30.4 M HC HC 0 0 107.52 1 +sub-719322 22.2 M HC HC 0 0 108.3 1 +sub-719326 27.6 F HC HC 0 0 113.76 1 +sub-719327 29.3 F HC HC 0 0 112.98 1 +sub-719329 19.3 F HC HC 0 0 93.48 1 +sub-719330 41.2 F HC HC 0 0 101.28 1 +sub-719331 21.4 F HC HC 0 0 96.6 1 +sub-719332 26.7 M HC HC 0 0 115.32 1 +sub-719334 22.9 F HC HC 0 0 103.62 1 +sub-719335 39.5 F HC HC 0 0 102.84 1 +sub-719337 21.3 F HC HC 0 0 95.82 1 +sub-719339 22.4 F HC HC 0 0 105.18 1 +sub-719341 34.1 F HC HC 0 0 102.06 1 +sub-719345 27.5 F HC HC 0 0 98.16 1 +sub-719348 33.6 F HC HC 0 0 102.06 1 +sub-719349 26.7 M HC HC 0 0 105.96 1 +sub-719350 27 F HC HC 0 0 91.92 1 +sub-719351 32.8 M HC HC 0 0 114.54 1 +sub-719354 26.9 F HC HC 0 0 105.96 1 +sub-719355 31.4 M HC HC 0 0 109.86 1 +sub-719356 36.8 F HC HC 0 0 103.62 1 +sub-719358 22.6 F HC HC 0 0 107.52 1 +sub-719360 28.2 F HC HC 0 0 102.84 1 +sub-719362 31.4 M HC HC 0 0 105.96 1 +sub-719364 19.6 F HC HC 0 0 111.42 1 +sub-719369 25.5 F HC HC 0 0 108.3 1 +sub-719370 44.3 F HC HC 0 0 108.3 1 +sub-719371 30 F HC HC 0 0 113.76 1 +sub-719511 31 F UD MDD 1 1 109.08 1 +sub-719515 31.1 F UD MDD 1 2 120.78 1 +sub-719518 27.7 F UD MDD 2 3 118.44 1 +sub-719522 32.1 F UD MDD 1 2 119.22 1 +sub-719523 21.1 F UD PDD 2 0 100.5 1 +sub-719524 22.7 M UD MDD 0 0 108.3 1 +sub-719525 23.6 F UD MDD 0 1 115.32 1 +sub-719526 24.4 F UD MDD 1 0 102.84 1 +sub-719527 38.9 F UD PDD 0 3 116.88 1 +sub-719528 21.6 M UD MDD 0 0 109.86 1 +sub-719530 33.6 F UD MDD 0 0 118.44 1 +sub-719531 34.8 F UD PDD 2 0 102.06 1 +sub-719535 29.1 M UD MDD 0 0 107.52 1 +sub-719536 23.7 M UD MDD 0 1 88.8 1 +sub-720219 28.4 M UD PDD 2 3 110.64 1 +sub-720220 22.3 F UD PDD 3 4 99.72 1 +sub-720311 25.1 F HC HC 0 0 114.54 1 +sub-720312 26.8 M HC HC 0 0 122.34 1 +sub-720314 20.4 F HC HC 0 0 112.98 1 +sub-720316 21.4 M HC HC 0 0 106.74 1 +sub-720317 33.1 F HC HC 0 0 102.06 1 +sub-720318 30.3 F HC HC 0 0 110.64 1 +sub-720319 28.8 F HC HC 0 0 105.96 1 +sub-720320 31.1 F HC HC 0 0 115.32 1 +sub-720511 21.9 M UD MDD 0 1 102.84 1 +sub-720515 22 F UD MDD 2 1 109.86 1 +sub-720516 38.8 F UD MDD 1 4 105.18 1 +sub-720517 29.8 F UD MDD 1 0 95.82 1 diff --git a/cypress/fixtures/examples/mock.tsv b/cypress/fixtures/examples/mock.tsv new file mode 100644 index 0000000..f8d6713 --- /dev/null +++ b/cypress/fixtures/examples/mock.tsv @@ -0,0 +1,13 @@ +participant_id age sex +sub-718211 28.4 M +sub-718213 24.6 F +sub-718216 43.6 M +sub-718217 28.4 F +sub-718218 72.1 M +sub-718219 56.2 M +sub-718220 23 M +sub-718221 22 F +sub-718222 21 M +sub-718223 45 F +sub-718224 34 M +sub-718225 65 M \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 215f36e..5efa6ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.1.1", + "@mui/icons-material": "^6.4.4", "@mui/material": "^6.1.10", "react": "^19.0.0", "react-dom": "^19.0.0", + "uuid": "^11.0.5", "zustand": "^5.0.3" }, "devDependencies": { @@ -1990,6 +1992,15 @@ "node": ">= 6" } }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@cypress/webpack-preprocessor": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@cypress/webpack-preprocessor/-/webpack-preprocessor-6.0.2.tgz", @@ -3088,6 +3099,31 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.4.tgz", + "integrity": "sha512-uF1chGaoFmYdRUomK6f8kgJfWosk9A3HXWiVD0vQm+2mE7f25eTQ1E8RRO11LXpnUBqu8Rbv/uGlpnjT/u1Ksg==", + "dependencies": { + "@babel/runtime": "^7.26.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.4.4", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.4.tgz", @@ -8969,6 +9005,15 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -13169,12 +13214,15 @@ "dev": true }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/verror": { diff --git a/package.json b/package.json index ee3b4fe..016e519 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.1.1", + "@mui/icons-material": "^6.4.4", "@mui/material": "^6.1.10", "react": "^19.0.0", "react-dom": "^19.0.0", + "uuid": "^11.0.5", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index d19a656..9e4f749 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import useStore from './stores/store'; +import useViewStore from './stores/view'; import Landing from './components/Landing'; import Upload from './components/Upload'; import ColumnAnnotation from './components/ColumnAnnotation'; @@ -6,11 +6,11 @@ import ValueAnnotation from './components/ValueAnnotation'; import Download from './components/Download'; function App() { - const currentView = useStore((state) => state.currentView); + const currentView = useViewStore((state) => state.currentView); const renderView = () => { switch (currentView) { - case 'ladning': + case 'landing': return ; case 'upload': return ; diff --git a/src/components/DataTablePreview.tsx b/src/components/DataTablePreview.tsx new file mode 100644 index 0000000..18f269f --- /dev/null +++ b/src/components/DataTablePreview.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, +} from '@mui/material'; +import { v4 as uuidv4 } from 'uuid'; +import { DataTable, Columns, Column } from '../utils/types'; + +function DataTablePreview({ dataTable, columns }: { dataTable: DataTable; columns: Columns }) { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const headers = Object.values(columns).map((column: Column) => column.header); + + // Transform column-based data into row-based data for rendering + const rowData = + Object.keys(dataTable).length > 0 + ? Object.values(dataTable)[0].map((_, rowIndex) => + Object.keys(dataTable).map((colKey) => dataTable[Number(colKey)][rowIndex]) + ) + : []; + + const slicedRows = rowData.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + const handleChangePage = (_: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + return ( +
+ + + + + {headers.map((header) => ( + + {header} + + ))} + + + + {slicedRows.map((row) => { + const rowId = uuidv4(); + return ( + + {row.map((cell) => { + const cellId = uuidv4(); + return ( + + {cell} + + ); + })} + + ); + })} + +
+ +
+
+ ); +} + +export default DataTablePreview; diff --git a/src/components/FileUploader.tsx b/src/components/FileUploader.tsx new file mode 100644 index 0000000..3069406 --- /dev/null +++ b/src/components/FileUploader.tsx @@ -0,0 +1,69 @@ +import { Card, Typography, useTheme } from '@mui/material'; +import { CloudUpload } from '@mui/icons-material'; + +interface FileUploaderProps { + displayText: string; + handleClickToUpload: () => void; + handleDrop: (event: React.DragEvent) => void; + handleDragOver: (event: React.DragEvent) => void; + handleFileUpload: (event: React.ChangeEvent) => void; + fileInputRef: React.RefObject; + allowedFileType: string; +} + +function FileUploader({ + displayText, + handleClickToUpload, + handleDrop, + handleDragOver, + handleFileUpload, + fileInputRef, + allowedFileType, +}: FileUploaderProps) { + const theme = useTheme(); + + return ( + + + + {displayText} + + + + Click to upload + {' '} + or drag and drop + + + + ); +} + +export default FileUploader; diff --git a/src/components/NavigationButton.tsx b/src/components/NavigationButton.tsx index 45a00bb..fdf1326 100644 --- a/src/components/NavigationButton.tsx +++ b/src/components/NavigationButton.tsx @@ -1,5 +1,5 @@ import { Button } from '@mui/material'; -import useStore from '../stores/store'; +import useViewStore from '../stores/view'; function NavigationButton({ label, @@ -8,7 +8,7 @@ function NavigationButton({ label: string; viewToNavigateTo: string; }) { - const setCurrentView = useStore((state) => state.setCurrentView); + const setCurrentView = useViewStore((state) => state.setCurrentView); const handleClick = () => { setCurrentView(viewToNavigateTo); diff --git a/src/components/Upload.tsx b/src/components/Upload.tsx index a3c1059..c078fbc 100644 --- a/src/components/Upload.tsx +++ b/src/components/Upload.tsx @@ -1,9 +1,30 @@ +import UploadCard from './UploadCard'; +import DataTablePreview from './DataTablePreview'; +import useDataStore from '../stores/data'; import NavigationButton from './NavigationButton'; function Upload() { + const processFile = useDataStore((state) => state.processFile); + const dataTable = useDataStore((state) => state.dataTable); + const columns = useDataStore((state) => state.columns); + const uploadedDataTableFileName = useDataStore((state) => state.uploadedDataTableFileName); + const setUploadedDataTableFileName = useDataStore((state) => state.setUploadedDataTableFileName); + + const handleFileUpload = (file: File) => { + setUploadedDataTableFileName(file.name); + processFile(file); + }; + return (

Upload

+ } + />
diff --git a/src/components/UploadCard.tsx b/src/components/UploadCard.tsx new file mode 100644 index 0000000..bb993b5 --- /dev/null +++ b/src/components/UploadCard.tsx @@ -0,0 +1,101 @@ +import { useRef, useState } from 'react'; +import { Button, Card, Typography, Collapse } from '@mui/material'; +import { ExpandMore, ExpandLess } from '@mui/icons-material'; +import FileUploader from './FileUploader'; + +interface UploadCardProps { + title: string; + allowedFileType: string; + uploadedFileName: string | null; + onFileUpload: (file: File) => void; + previewComponent: React.ReactNode; +} + +function UploadCard({ + title, + allowedFileType, + uploadedFileName, + onFileUpload, + previewComponent, +}: UploadCardProps) { + const fileInputRef = useRef(null); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + + const isFileUploaded = uploadedFileName !== null; + + const handleFileUpload = (event: React.ChangeEvent) => { + const uploadedFile = event.target.files?.[0]; + if (uploadedFile) { + onFileUpload(uploadedFile); + } + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + const droppedFile = event.dataTransfer.files?.[0]; + if (droppedFile) { + onFileUpload(droppedFile); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + const togglePreview = () => { + setIsPreviewOpen(!isPreviewOpen); + }; + + const handleClickToUpload = () => { + fileInputRef.current?.click(); + }; + + return ( +
+ + + {title} + + + + + {isFileUploaded && ( +
+ + {uploadedFileName} + + +
+ )} +
+ + + {isFileUploaded && previewComponent} + +
+ ); +} + +export default UploadCard; diff --git a/src/stores/data.test.ts b/src/stores/data.test.ts new file mode 100644 index 0000000..bee7552 --- /dev/null +++ b/src/stores/data.test.ts @@ -0,0 +1,24 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { mockDataTable, mockInitialColumns } from '~/utils/mocks'; +import fs from 'fs'; +import path from 'path'; +import useDataStore from './data'; + +describe('store actions', () => { + it('should process a file and update dataTable, columns, and uploadedDataTableFileName', async () => { + const { result } = renderHook(() => useDataStore()); + + const filePath = path.resolve(__dirname, '../../cypress/fixtures/examples/mock.tsv'); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const file = new File([fileContent], 'mock.tsv', { type: 'text/tab-separated-values' }); + + await act(async () => { + await result.current.processFile(file); + }); + + expect(result.current.dataTable).toEqual(mockDataTable); + expect(result.current.columns).toEqual(mockInitialColumns); + expect(result.current.uploadedDataTableFileName).toEqual('mock.tsv'); + }); +}); diff --git a/src/stores/data.ts b/src/stores/data.ts new file mode 100644 index 0000000..d0c7e1c --- /dev/null +++ b/src/stores/data.ts @@ -0,0 +1,60 @@ +import { create } from 'zustand'; +import { DataTable, Columns } from '../utils/types'; + +type DataStore = { + dataTable: DataTable; + columns: Columns; + uploadedDataTableFileName: string | null; + setDataTable: (data: DataTable) => void; + initializeColumns: (data: Columns) => void; + setUploadedDataTableFileName: (fileName: string | null) => void; + processFile: (file: File) => Promise; +}; + +const useDataStore = create((set) => ({ + dataTable: {}, + columns: {}, + uploadedDataTableFileName: null, + setDataTable: (data: DataTable) => set({ dataTable: data }), + initializeColumns: (data: Columns) => set({ columns: data }), + setUploadedDataTableFileName: (fileName: string | null) => + set({ uploadedDataTableFileName: fileName }), + processFile: async (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + const content = e.target?.result as string; + const rows = content.split('\n').map((row) => row.split('\t')); + const headers = rows[0]; + const data = rows.slice(1); + + // Transform data into column-based structure + const columnData: DataTable = {}; + headers.forEach((_, columnIndex) => { + columnData[columnIndex + 1] = data.map((row) => row[columnIndex]); + }); + + const columns: Columns = headers.reduce((acc, header, index) => { + acc[index + 1] = { header }; + return acc; + }, {} as Columns); + + set({ + dataTable: columnData, + columns, + uploadedDataTableFileName: file.name, + }); + + resolve(); + }; + + reader.onerror = (error) => { + reject(error); + }; + + reader.readAsText(file); + }), +})); + +export default useDataStore; diff --git a/src/stores/store.test.ts b/src/stores/view.test.ts similarity index 77% rename from src/stores/store.test.ts rename to src/stores/view.test.ts index 24a5d57..cebfb28 100644 --- a/src/stores/store.test.ts +++ b/src/stores/view.test.ts @@ -1,10 +1,10 @@ import { act, renderHook } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; -import useStore from './store'; +import useViewStore from './view'; describe('store actions', () => { it('should set currentView', () => { - const { result } = renderHook(() => useStore()); + const { result } = renderHook(() => useViewStore()); act(() => { result.current.setCurrentView('upload'); }); diff --git a/src/stores/store.ts b/src/stores/view.ts similarity index 70% rename from src/stores/store.ts rename to src/stores/view.ts index 3dedec2..9ce4ec7 100644 --- a/src/stores/store.ts +++ b/src/stores/view.ts @@ -1,23 +1,22 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; -type Store = { +type ViewStore = { currentView: string; setCurrentView: (view: string) => void; }; -const useStore = create()( +const useViewStore = create()( persist( (set) => ({ currentView: 'landing', setCurrentView: (view: string) => set({ currentView: view }), }), { - name: 'store', - partialize: (state) => ({ currentView: state.currentView }), + name: 'view-store', storage: createJSONStorage(() => sessionStorage), } ) ); -export default useStore; +export default useViewStore; diff --git a/src/utils/mocks.ts b/src/utils/mocks.ts new file mode 100644 index 0000000..14d437c --- /dev/null +++ b/src/utils/mocks.ts @@ -0,0 +1,30 @@ +export const mockDataTable = { + 1: [ + 'sub-718211', + 'sub-718213', + 'sub-718216', + 'sub-718217', + 'sub-718218', + 'sub-718219', + 'sub-718220', + 'sub-718221', + 'sub-718222', + 'sub-718223', + 'sub-718224', + 'sub-718225', + ], + 2: ['28.4', '24.6', '43.6', '28.4', '72.1', '56.2', '23', '22', '21', '45', '34', '65'], + 3: ['M', 'F', 'M', 'F', 'M', 'M', 'M', 'F', 'M', 'F', 'M', 'M'], +}; + +export const mockInitialColumns = { + 1: { + header: 'participant_id', + }, + 2: { + header: 'age', + }, + 3: { + header: 'sex', + }, +}; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..0cb0bf4 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,3 @@ +export type DataTable = { [key: number]: string[] }; +export type Column = { header: string }; +export type Columns = { [key: number]: Column };