Skip to content

Commit

Permalink
implement csv upload functionality and refactor index page
Browse files Browse the repository at this point in the history
  • Loading branch information
ligsnf committed Dec 17, 2024
1 parent de45151 commit 3bca8e1
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 63 deletions.
36 changes: 36 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src/components/csv-upload-alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { cn } from "@/lib/utils"
import { TriangleAlert } from "lucide-react"

export function CSVUploadAlert({ className }: { className?: string }) {
return (
<div className={cn("flex items-center gap-2 p-2 border rounded-md text-sm border-[#fdf5d3] dark:border-[#3d3d00] text-[#dc7609] dark:text-[#f3cf58] [&>svg]:text-[#dc7609] [&>svg]:dark:text-[#f3cf58]", className)}>
<TriangleAlert className="h-4 w-4 !top-auto" />
Uploading CSV will replace all current data.
</div>
)
}
114 changes: 114 additions & 0 deletions src/components/csv-uploader.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
onCSVUpload: (data: string) => void
disabled?: boolean
className?: string
}

export function CSVUploader({ onCSVUpload, disabled = false, className }: CSVUploaderProps) {
const [file, setFile] = React.useState<File | null>(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 (
<div className="relative flex flex-col gap-4">
<Dropzone
onDrop={onDrop}
accept={{ "text/csv": [".csv"] }}
maxFiles={1}
multiple={false}
disabled={disabled}
>
{({ getRootProps, getInputProps, isDragActive }: {
getRootProps: () => DropzoneRootProps;
getInputProps: () => DropzoneInputProps;
isDragActive: boolean;
}) => (
<div
{...getRootProps()}
className={cn(
"group relative grid h-40 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-muted-foreground/25 px-5 py-2.5 text-center transition hover:bg-muted/25",
isDragActive && "border-muted-foreground/50",
disabled && "pointer-events-none opacity-60",
className
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center justify-center gap-4">
<Upload
className="h-8 w-8 text-muted-foreground"
aria-hidden="true"
/>
<div className="flex flex-col gap-1">
<p className="hidden sm:inline font-medium text-muted-foreground">
{isDragActive
? "Drop the CSV file here"
: "Drag and drop a CSV file, or click to select"}
</p>
<p className="sm:hidden font-medium text-muted-foreground">
Click to select a CSV file
</p>
{/* <p className="text-sm text-muted-foreground/70">
You can only upload one file
</p> */}
</div>
</div>
</div>
)}
</Dropzone>

{file && (
<div className="flex items-center gap-2 rounded-lg border p-2">
<FileText className="h-6 w-6 text-muted-foreground" />
<div className="flex flex-1 flex-col">
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-muted-foreground">
{(file.size / 1024).toFixed(2)} KB
</p>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={onRemove}
>
<X className="h-4 w-4" />
<span className="sr-only">Remove file</span>
</Button>
</div>
)}
</div>
)
}
File renamed without changes.
53 changes: 53 additions & 0 deletions src/components/stat-card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<CardHeader>
<CardTitle className="md:text-center">
{title} <span className="hidden md:inline text-xl text-muted-foreground">({subtitle})</span>
</CardTitle>
</CardHeader>
<CardContent>
{!isMobile && (
<RadialChart
className="font-mono"
value={value}
maxValue={maxValue}
color={color}
/>
)}
{isMobile && (
<p
className="text-3xl sm:text-4xl font-bold font-mono"
style={{ color: color }}
>
{value}
</p>
)}
</CardContent>
</Card>
)
}
78 changes: 15 additions & 63 deletions src/routes/index.lazy.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 (
<Card>
<CardHeader>
<CardTitle className="md:text-center">
{title} <span className="hidden md:inline text-xl text-muted-foreground">({subtitle})</span>
</CardTitle>
</CardHeader>
<CardContent>
{!isMobile && (
<RadialChart
className="font-mono"
value={value}
maxValue={maxValue}
color={color}
/>
)}
{isMobile && (
<p
className="text-3xl sm:text-4xl font-bold font-mono"
style={{ color: color }}
>
{value}
</p>
)}
</CardContent>
</Card>
)
}

function UploadCSVAlert({ className }: { className?: string }) {
return (
<div className={cn("flex items-center gap-2 p-2 border rounded-md text-sm border-[#fdf5d3] dark:border-[#3d3d00] text-[#dc7609] dark:text-[#f3cf58] [&>svg]:text-[#dc7609] [&>svg]:dark:text-[#f3cf58]", className)}>
<TriangleAlert className="h-4 w-4 !top-auto" />
Uploading CSV will replace all current data.
</div>
)
}

function Index() {
const [data, setData] = useLocalStorage<Result[]>(STORAGE_KEYS.RESULTS, initialData)
const [, setDeletedItems] = useState<Map<number, Result>>(() => new Map<number, Result>())
Expand Down Expand Up @@ -225,7 +171,7 @@ function Index() {
<span className="font-mono font-semibold text-lg sm:text-xl">{totalCredits}</span>
</div>
<div className="flex items-center gap-2">
<UploadCSVAlert className="hidden md:flex" />
<CSVUploadAlert className="hidden md:flex" />
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
Expand All @@ -238,10 +184,16 @@ function Index() {
<DialogHeader>
<DialogTitle>Upload CSV</DialogTitle>
</DialogHeader>
<UploadCSVAlert />
<DialogDescription>
yeah mate
<CSVUploadAlert />
<DialogDescription className="hidden">
Upload a CSV file to replace all current data
</DialogDescription>
<CSVUploader
onCSVUpload={(csvData) => {
// Here you can process the CSV data
console.log(csvData)
}}
/>
</DialogContent>
</Dialog>
<Dialog>
Expand Down

0 comments on commit 3bca8e1

Please sign in to comment.