diff --git a/bun.lockb b/bun.lockb index 588c8de..88f2bbc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index cb0850d..91f72d6 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toggle": "^1.0.3", @@ -25,6 +29,7 @@ "clsx": "^2.1.1", "d3": "^7.9.0", "html2canvas": "^1.4.1", + "joi": "^17.13.1", "lodash": "^4.17.21", "lucide-react": "^0.378.0", "papaparse": "^5.4.1", diff --git a/src/App.tsx b/src/App.tsx index 6a263d5..67fd05e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,73 +1,68 @@ -import './App.css' -import { - // useDataShopData, - useLocalSampleData, - usePathAnalysisData -} from '@/lib/dataFetchingHooks'; -import { useEffect, useState } from 'react'; -import { convertDataTypesIncomingData, processPathAnalysisData, processDataShopData } from '@/lib/dataProcessingUtils'; +import './App.css'; +import { useContext, useEffect, useState } from 'react'; import { GlobalDataType, GraphData } from './lib/types'; import DirectedGraph from './components/DirectedGraph'; import DropZone from './components/DropZone'; +// import { NavBar } from './components/NavBar'; +import { Button } from './components/ui/button'; +import { Context } from './Context'; function App() { - const sectionId = "area_perimeter_mix_rectangle_derived"; - const problemId = "area_perimeter_mix_rectangle_derived-020"; + const { resetData, setGraphData, setLoading, data, setData, graphData, loading } = useContext(Context) - // const { pathAnalysisData, isPathAnalysisDataLoading } = usePathAnalysisData(sectionId, problemId); - // const { dataShopData, isDataShopDataLoading } = useDataShopData(); - const filePath = "analyzing_different_forms_of_expressions.json" - // const { localSampleData, isLocalSampleDataLoading } = useLocalSampleData(filePath) - const [processedPathAnalysisData, setProcessedPathAnalysisData] = useState(null); - const [processedShopData, setProcessedShopData] = useState(null); + const handleData = (data: GlobalDataType[]) => { + setData(data) + } - // useEffect( () => { - // if (isDataShopDataLoading) { - // return; - // } - // else{ - // console.log("dataShopData: ", dataShopData); - // } - // }, [isDataShopDataLoading, dataShopData]) + const handleLoading = (loading: boolean) => { + setLoading(loading) + } - // useEffect(() => { - // if (isLocalSampleDataLoading) { - // return; - // } - // if (localSampleData) { - // const _processData = async () => { - // const processedData = await processDataShopData(localSampleData as GlobalDataType[]) - // setProcessedShopData(processedData); - // } - // _processData(); - // } - // }, [isLocalSampleDataLoading, localSampleData]) + useEffect(() => { + if (data) { + console.log(data); + } + }, [data]) + return ( + <> +
+ {/* */} + - // }, [isPathAnalysisDataLoading, pathAnalysisData]) +
- // if (isPathAnalysisDataLoading || !processedPathAnalysisData || !processedShopData) { - // return
Loading...
- // } + { + loading ? +
+
+

Loading...

+
+
+ : +
+ +
+ } - return ( - <> -
-
- -
- {/* */} -
+ { + graphData && ( + + ) + } +
+
) } diff --git a/src/Context.tsx b/src/Context.tsx new file mode 100644 index 0000000..3e03b0f --- /dev/null +++ b/src/Context.tsx @@ -0,0 +1,50 @@ +import { createContext, useState } from 'react'; +import { GlobalDataType, GraphData } from './lib/types'; +interface ContextInterface { + data: GlobalDataType[] | null; + graphData: GraphData | null; + loading: boolean; + setLoading: (loading: boolean) => void; + setData: (data: GlobalDataType[] | null) => void; + setGraphData: (graphData: GraphData | null) => void; + resetData: () => void; + +} +export const Context = createContext({} as ContextInterface); +const initialState = { + data: null, + graphData: null, + loading: false +} + +interface ProviderProps { + children: React.ReactNode; +} +export const Provider = ({ children }: ProviderProps) => { + const [data, setData] = useState(initialState.data) + const [graphData, setGraphData] = useState(initialState.graphData) + const [loading, setLoading] = useState(initialState.loading) + + const resetData = () => { + setData(null) + setGraphData(null) + console.log("Data reset"); + + } + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/components/DirectedGraph.tsx b/src/components/DirectedGraph.tsx index 1ecd5ef..6ede644 100644 --- a/src/components/DirectedGraph.tsx +++ b/src/components/DirectedGraph.tsx @@ -17,7 +17,6 @@ interface DirectedGraphProps { } export default function DirectedGraph({ graphData }: DirectedGraphProps) { - console.log("graphData: ", graphData); const initialTooltip: ToolTip = { display: false, text: '', x: 0, y: 0, fx: undefined, fy: undefined }; const [tooltip, setTooltip] = useState(initialTooltip); diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx index 4df743b..092e4b0 100644 --- a/src/components/DropZone.tsx +++ b/src/components/DropZone.tsx @@ -1,10 +1,22 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Accept, useDropzone } from 'react-dropzone'; import { GlobalDataType } from '@/lib/types'; import { parseData } from '@/lib/utils'; +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" + +interface DropZoneProps { + afterDrop: (data: GlobalDataType[]) => void, + onLoadingChange: (loading: boolean) => void +} + +export default function DropZone({afterDrop, onLoadingChange}: DropZoneProps) { + const delimiters = ["tsv", "csv", "pipe"] + + const [fileType, setFileType] = useState(delimiters[0]) -export default function DropZone() { const onDrop = useCallback((acceptedFiles: File[]) => { + onLoadingChange(true); acceptedFiles.forEach((file: File) => { const reader = new FileReader() @@ -13,8 +25,31 @@ export default function DropZone() { reader.onload = () => { // Do whatever you want with the file contents after .readAsText() const textStr = reader.result - const array: GlobalDataType[] = parseData(textStr) - console.log(array) + // find file type and convert to delimeter + let delimeter: string; + switch (fileType) { + case 'tsv': + delimeter = '\t' + break; + case 'csv': + delimeter = ',' + break; + case 'pipe': + delimeter = '|' + break; + case 'json': + delimeter = '' + break; + default: + delimeter = '\t' + break; + } + const array: GlobalDataType[] | null = parseData(textStr, delimeter) + if (array) { + afterDrop(array); + } + onLoadingChange(false); + // console.log(array) } reader.readAsText(file) }) @@ -30,22 +65,65 @@ export default function DropZone() { accept: acceptedFileTypes, }); + const fileTypeOptions = [ + { + label: 'Tab Separated', + value: delimiters.find((delimiter) => delimiter === 'tsv') as string + }, + { + label: 'Comma Separated', + value: delimiters.find((delimiter) => delimiter === 'csv') as string + }, + { + label: 'Pipe Separated', + value: delimiters.find((delimiter) => delimiter === 'pipe') as string + }, + // { + // label: 'JSON', + // value: delimiters.find((delimiter) => delimiter === 'json') as string + // } + ] return ( -
- - { - (isDragActive && !isDragReject) ? -

Drop em here

: -

Drag 'n' drop some files here, or click to select files

- } - { - isDragReject && -

File type not accepted, please try again

+ <> +
+
+ File Type +
+ { + setFileType(e) - } -
+ }}> + {fileTypeOptions.map((option, index) => ( +
+ + +
+ ))} + +
+
+ + { + !isDragActive ? +
+

Drag 'n' drop some files here, or click to select files

+
+ : +
+

Drag 'n' drop some files here, or click to select files

+
+ } + { + isDragReject && +

File type not accepted, please try again

+ + } +
+ + + ); } \ No newline at end of file diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx new file mode 100644 index 0000000..cdfb9e4 --- /dev/null +++ b/src/components/NavBar.tsx @@ -0,0 +1,74 @@ +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuIndicator, + NavigationMenuItem, + // NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + // NavigationMenuViewport, +} from "@/components/ui/navigation-menu" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { useContext } from "react" +import { Context } from "@/Context" +import DropZone from "./DropZone" +import { GlobalDataType } from "@/lib/types" + +export function NavBar() { + + return ( +
+ + + + Upload + + + +
+ Upload and Process Data +
+
+ + + + Upload Data + + + + {/* { }} + onLoadingChange={(loading) => { + setLoading(loading) + }} + /> */} + + + + Cancel + Continue + + +
+ +
+
+ +
+
+ +
+ ) +} \ No newline at end of file diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..8722561 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..1419f56 --- /dev/null +++ b/src/components/ui/navigation-menu.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" +import { cva } from "class-variance-authority" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)) +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName + +const NavigationMenuItem = NavigationMenuPrimitive.Item + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" +) + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children}{" "} + +)) +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName + +const NavigationMenuLink = NavigationMenuPrimitive.Link + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ +
+)) +NavigationMenuViewport.displayName = + NavigationMenuPrimitive.Viewport.displayName + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
+ +)) +NavigationMenuIndicator.displayName = + NavigationMenuPrimitive.Indicator.displayName + +export { + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, +} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..43b43b4 --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/src/lib/types.ts b/src/lib/types.ts index 114a05f..a2116c7 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -6,7 +6,7 @@ export type DataSet1 = { "Transaction Id": string; "Anon Student Id": string; "Session Id": "no_session_tracking"; - Time: string; + Time: string; // Assume data is sorted by time "Time Zone": boolean | null; "Duration (sec)": number | null; "Student Response Type": boolean | null; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index aa44d82..98f4be0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,18 +2,38 @@ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" import { GlobalDataType } from "./types" import Papa from "papaparse" +import Joi from "joi" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } +const validation = Joi.array().items( + Joi.object({ + 'Problem Name': Joi.string().required(), + 'Step Name': Joi.string().allow('', null), + 'Outcome': Joi.string().valid('OK', 'BUG', 'INITIAL_HINT', 'HINT_LEVEL_CHANGE', 'ERROR').required(), + }).unknown() +); -export function parseData(readerResult: string | ArrayBuffer | null, delimiter: "\t" | "," | "|" = "\t"): GlobalDataType[] { +export function parseData(readerResult: string | ArrayBuffer | null, delimiter: string = "\t"): GlobalDataType[] | null { const textStr = readerResult const results = Papa.parse(textStr as string, { - header: true, - delimiter: delimiter + header: true, + delimiter: delimiter }) + if (results.errors.length > 0) { + console.error("error during parsing: ", results.errors) + return null; + + } + const array: GlobalDataType[] = results.data as GlobalDataType[] + const { error } = validation.validate(array); + if (error) { + console.error(error) + return null; + } + return array } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 2b1a1aa..cb22913 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,13 +7,18 @@ import { QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { Provider } from './Context.tsx' const queryClient = new QueryClient() ReactDOM.createRoot(document.getElementById('root')!).render( - + + + + + ,