From 71a18a5d0fca5c3d39dd2c2a75433ad228563045 Mon Sep 17 00:00:00 2001 From: Liangdi Date: Tue, 17 Dec 2024 16:23:41 +1100 Subject: [PATCH] implement csv upload functionality and refactor index page https://github.com/sadmann7/file-uploader/ --- package-lock.json | 36 ++++++ package.json | 1 + src/components/csv/csv-information-dialog.tsx | 30 +++++ src/components/csv/csv-upload-alert.tsx | 11 ++ src/components/csv/csv-upload-dialog.tsx | 41 +++++++ src/components/csv/csv-uploader.tsx | 114 ++++++++++++++++++ src/components/{results => }/radial-chart.tsx | 0 src/components/stat-card.tsx | 53 ++++++++ src/routes/index.lazy.tsx | 109 ++--------------- 9 files changed, 294 insertions(+), 101 deletions(-) create mode 100644 src/components/csv/csv-information-dialog.tsx create mode 100644 src/components/csv/csv-upload-alert.tsx create mode 100644 src/components/csv/csv-upload-dialog.tsx create mode 100644 src/components/csv/csv-uploader.tsx rename src/components/{results => }/radial-chart.tsx (100%) create mode 100644 src/components/stat-card.tsx 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/csv-information-dialog.tsx b/src/components/csv/csv-information-dialog.tsx new file mode 100644 index 0000000..6536567 --- /dev/null +++ b/src/components/csv/csv-information-dialog.tsx @@ -0,0 +1,30 @@ +import { Info } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" + +export function CSVInformationDialog() { + return ( + + + + + + + Instructions for preparing CSV + + yeah mate + + + + + ) +} \ No newline at end of file diff --git a/src/components/csv/csv-upload-alert.tsx b/src/components/csv/csv-upload-alert.tsx new file mode 100644 index 0000000..73a888e --- /dev/null +++ b/src/components/csv/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/csv-upload-dialog.tsx b/src/components/csv/csv-upload-dialog.tsx new file mode 100644 index 0000000..ba7e761 --- /dev/null +++ b/src/components/csv/csv-upload-dialog.tsx @@ -0,0 +1,41 @@ +import { Upload } from "lucide-react" +import { CSVUploadAlert } from "@/components/csv/csv-upload-alert" +import { CSVUploader } from "@/components/csv/csv-uploader" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" + +export function CSVUploadDialog() { + return ( + + + + + + + Upload CSV + + + + Upload a CSV file to replace all current data + + { + // Here you can process the CSV data + console.log(csvData) + }} + /> + + + ) +} \ No newline at end of file diff --git a/src/components/csv/csv-uploader.tsx b/src/components/csv/csv-uploader.tsx new file mode 100644 index 0000000..3fa072a --- /dev/null +++ b/src/components/csv/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; + }) => ( +
+ +
+
+
+ )} +
+ + {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..6ac90d1 100644 --- a/src/routes/index.lazy.tsx +++ b/src/routes/index.lazy.tsx @@ -1,32 +1,21 @@ import { useMemo, useState } from 'react' -import { Info, TriangleAlert, 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 { StatCard } from "@/components/stat-card" +import { CSVUploadAlert } from "@/components/csv/csv-upload-alert" +import { CSVUploadDialog } from '@/components/csv/csv-upload-dialog' +import { CSVInformationDialog } from '@/components/csv/csv-information-dialog' 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, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" export const Route = createLazyFileRoute('/')({ component: Index, @@ -63,57 +52,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>(() => new Map()) @@ -225,40 +163,9 @@ function Index() { {totalCredits}
- - - - - - - - Upload CSV - - - - yeah mate - - - - - - - - - - Instructions for preparing CSV - - yeah mate - - - - + + +