diff --git a/package-lock.json b/package-lock.json
index 83add13..7d100df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.0",
"recharts": "^2.14.1",
"sonner": "^1.7.0",
@@ -2978,6 +2979,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/attr-accept": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
+ "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -4432,6 +4441,17 @@
"node": ">=16.0.0"
}
},
+ "node_modules/file-selector": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
+ "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
+ "dependencies": {
+ "tslib": "^2.7.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -5994,6 +6014,22 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-dropzone": {
+ "version": "14.3.5",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz",
+ "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
+ "dependencies": {
+ "attr-accept": "^2.2.4",
+ "file-selector": "^2.1.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8 || 18.0.0"
+ }
+ },
"node_modules/react-hook-form": {
"version": "7.54.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz",
diff --git a/package.json b/package.json
index 643cc96..c096f09 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.0",
"recharts": "^2.14.1",
"sonner": "^1.7.0",
diff --git a/src/components/csv-upload-alert.tsx b/src/components/csv-upload-alert.tsx
new file mode 100644
index 0000000..73a888e
--- /dev/null
+++ b/src/components/csv-upload-alert.tsx
@@ -0,0 +1,11 @@
+import { cn } from "@/lib/utils"
+import { TriangleAlert } from "lucide-react"
+
+export function CSVUploadAlert({ className }: { className?: string }) {
+ return (
+
svg]:text-[#dc7609] [&>svg]:dark:text-[#f3cf58]", className)}>
+
+ Uploading CSV will replace all current data.
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/csv-uploader.tsx b/src/components/csv-uploader.tsx
new file mode 100644
index 0000000..3fa072a
--- /dev/null
+++ b/src/components/csv-uploader.tsx
@@ -0,0 +1,114 @@
+import * as React from "react"
+import { FileText, Upload, X } from "lucide-react"
+import Dropzone, { DropzoneRootProps, DropzoneInputProps } from "react-dropzone"
+import { toast } from "sonner"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+interface CSVUploaderProps extends React.HTMLAttributes {
+ onCSVUpload: (data: string) => void
+ disabled?: boolean
+ className?: string
+}
+
+export function CSVUploader({ onCSVUpload, disabled = false, className }: CSVUploaderProps) {
+ const [file, setFile] = React.useState(null)
+
+ const onDrop = React.useCallback(
+ (acceptedFiles: File[]) => {
+ if (acceptedFiles.length === 0) {
+ toast.error("Please upload a CSV file")
+ return
+ }
+
+ const csvFile = acceptedFiles[0]
+ setFile(csvFile)
+
+ // Read and process the CSV file
+ const reader = new FileReader()
+ reader.onload = (event) => {
+ if (event.target?.result) {
+ const csvData = event.target.result as string
+ onCSVUpload(csvData)
+ }
+ }
+ reader.readAsText(csvFile)
+ },
+ [onCSVUpload]
+ )
+
+ function onRemove() {
+ setFile(null)
+ }
+
+ return (
+
+
+ {({ getRootProps, getInputProps, isDragActive }: {
+ getRootProps: () => DropzoneRootProps;
+ getInputProps: () => DropzoneInputProps;
+ isDragActive: boolean;
+ }) => (
+
+
+
+
+
+
+ {isDragActive
+ ? "Drop the CSV file here"
+ : "Drag and drop a CSV file, or click to select"}
+
+
+ Click to select a CSV file
+
+ {/*
+ You can only upload one file
+
*/}
+
+
+
+ )}
+
+
+ {file && (
+
+
+
+
{file.name}
+
+ {(file.size / 1024).toFixed(2)} KB
+
+
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/results/radial-chart.tsx b/src/components/radial-chart.tsx
similarity index 100%
rename from src/components/results/radial-chart.tsx
rename to src/components/radial-chart.tsx
diff --git a/src/components/stat-card.tsx b/src/components/stat-card.tsx
new file mode 100644
index 0000000..6f33913
--- /dev/null
+++ b/src/components/stat-card.tsx
@@ -0,0 +1,53 @@
+import { useBreakpoint } from '@/hooks/use-breakpoint'
+import { calculateColor } from "@/lib/calculate"
+import { RadialChart } from "@/components/radial-chart"
+import { useTheme } from "@/components/theme/theme-provider"
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+
+type StatCardProps = {
+ title: string
+ subtitle: string
+ value: number
+ maxValue: number
+}
+
+export function StatCard({ title, subtitle, value, maxValue }: StatCardProps) {
+ const { theme } = useTheme()
+ const isDarkMode = theme === "dark"
+ const color = calculateColor(value, maxValue, isDarkMode)
+ const { isMobile } = useBreakpoint()
+
+ return (
+
+
+
+ {title} ({subtitle})
+
+
+
+ {!isMobile && (
+
+ )}
+ {isMobile && (
+
+ {value}
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx
index 0ea9484..e554788 100644
--- a/src/routes/index.lazy.tsx
+++ b/src/routes/index.lazy.tsx
@@ -1,23 +1,20 @@
import { useMemo, useState } from 'react'
-import { Info, TriangleAlert, Upload } from "lucide-react"
+import { Info, Upload } from "lucide-react"
import { createLazyFileRoute } from '@tanstack/react-router'
import { useLocalStorage } from '@/hooks/use-local-storage'
-import { useBreakpoint } from '@/hooks/use-breakpoint'
import { STORAGE_KEYS } from '@/constants/storage-keys'
-import { calculateWAM, calculateGPA, calculateTotalCredits, calculateColor } from "@/lib/calculate"
-import { cn } from "@/lib/utils"
+import { calculateWAM, calculateGPA, calculateTotalCredits } from "@/lib/calculate"
import { Result } from "@/schemas/result-schema"
import { toast } from "sonner"
-import { useTheme } from "@/components/theme/theme-provider"
+import { CSVUploadAlert } from "@/components/csv-upload-alert"
+import { CSVUploader } from "@/components/csv-uploader"
+import { StatCard } from "@/components/stat-card"
import { ResultTable } from "@/components/results/result-table"
-import { RadialChart } from "@/components/results/radial-chart"
import { Button } from "@/components/ui/button"
import {
Card,
- CardContent,
CardHeader,
- CardTitle,
} from "@/components/ui/card"
import {
Dialog,
@@ -63,57 +60,6 @@ const initialData: Result[] = [
},
]
-type StatCardProps = {
- title: string
- subtitle: string
- value: number
- maxValue: number
-}
-
-function StatCard({ title, subtitle, value, maxValue }: StatCardProps) {
- const { theme } = useTheme()
- const isDarkMode = theme === "dark"
- const color = calculateColor(value, maxValue, isDarkMode)
- const { isMobile } = useBreakpoint()
-
- return (
-
-
-
- {title} ({subtitle})
-
-
-
- {!isMobile && (
-
- )}
- {isMobile && (
-
- {value}
-
- )}
-
-
- )
-}
-
-function UploadCSVAlert({ className }: { className?: string }) {
- return (
- svg]:text-[#dc7609] [&>svg]:dark:text-[#f3cf58]", className)}>
-
- Uploading CSV will replace all current data.
-
- )
-}
-
function Index() {
const [data, setData] = useLocalStorage(STORAGE_KEYS.RESULTS, initialData)
const [, setDeletedItems] = useState