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}
+
+ : }
+ >
+ {isPreviewOpen ? 'Hide Preview' : 'Show Preview'}
+
+
+ )}
+
+
+
+ {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 };