Skip to content

Commit

Permalink
feat(fe): add problem uploading ui with excel (#1468)
Browse files Browse the repository at this point in the history
* feat: add file upload dialog

* feat: configure apollo-upload-client
  • Loading branch information
dotoleeoak authored Feb 22, 2024
1 parent e52cc56 commit 17aaf13
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 27 deletions.
2 changes: 1 addition & 1 deletion Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
header Access-Control-Allow-Credentials true
header Access-Control-Allow-Origin "{args[0]}"
header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE"
header Access-Control-Allow-Headers "Content-Type, Authorization, Email-Auth"
header Access-Control-Allow-Headers "Content-Type, Authorization, Email-Auth, Apollo-Require-Preflight"
respond "" 204
}

Expand Down
11 changes: 7 additions & 4 deletions frontend-client/app/admin/_components/ApolloProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import {
ApolloClient,
ApolloLink,
ApolloProvider,
InMemoryCache,
createHttpLink
InMemoryCache
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs'

interface Props {
children: React.ReactNode
}

export default function ClientApolloProvider({ children }: Props) {
const httpLink = createHttpLink({
uri: adminBaseUrl
const httpLink = createUploadLink({
uri: adminBaseUrl,
headers: {
'Apollo-Require-Preflight': 'true'
}
})
const authLink = setContext(async (_, { headers }) => {
const session = await auth()
Expand Down
164 changes: 164 additions & 0 deletions frontend-client/app/admin/problem/_components/UploadDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { gql } from '@generated'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogTrigger
} from '@/components/ui/dialog'
import { useMutation } from '@apollo/client'
import { UploadIcon, UploadCloudIcon } from 'lucide-react'
import { useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { RiFileExcel2Fill } from 'react-icons/ri'
import { useDrop } from 'react-use'
import { toast } from 'sonner'

interface Props {
refetch: () => Promise<unknown>
}

const UPLOAD_PROBLEMS = gql(`
mutation uploadProblems ($groupId: Int!, $input: UploadFileInput!) {
uploadProblems(groupId: $groupId, input: $input) {
id
}
}
`)

export default function UploadDialog({ refetch }: Props) {
const [file, setFile] = useState<File | null>(null)
const fileRef = useRef<HTMLInputElement | null>(null)

const state = useDrop({
onFiles: (files) => {
if (files.length > 1) {
toast.error('Only one file is allowed')
}
const file = files[0]
if (file.name.endsWith('.xlsx')) {
setFile(files[0])
} else {
toast.error('Only .xlsx files are allowed')
}
}
})

const openFileBrowser = () => {
fileRef.current?.click()
}

const setFileFromInput = () => {
if (fileRef.current?.files) {
setFile(fileRef.current.files[0])
}
}

const resetFile = () => {
setFile(null)
if (fileRef.current) fileRef.current.value = ''
}

const [uploadProblems, { loading }] = useMutation(UPLOAD_PROBLEMS)
const uploadFile = async () => {
try {
await uploadProblems({
variables: {
groupId: 1,
input: {
file
}
}
})
} catch (error) {
toast.error('Failed to upload file')
return
}
toast.success('File uploaded successfully')
document.getElementById('closeDialog')?.click()
resetFile()
await refetch()
}

return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<UploadIcon className="mr-2 h-4 w-4" />
Upload
</Button>
</DialogTrigger>
<DialogContent className="flex h-[24rem] w-[32rem] flex-col">
<h1 className="my-2 text-2xl font-bold">Upload Problem</h1>
<p className="text-sm">
Please upload Excel file containing problem data. If you are looking
for the required schema, you can download{' '}
<a href="/sample.xlsx" download className="text-primary underline">
the sample file here.
</a>
</p>
<input
hidden
type="file"
ref={fileRef}
accept=".xlsx"
onChange={setFileFromInput}
/>
<DialogClose />
{file ? (
<section className="relative flex h-full w-full flex-col items-center justify-center gap-4">
<div className="flex min-w-60 items-center justify-center gap-2 border-4 border-dotted p-8 text-sm">
<RiFileExcel2Fill size={20} className="text-[#1D6F42]" />
{file.name}
</div>
<div className="flex gap-4">
<Button
variant="ghost"
size="sm"
onClick={resetFile}
disabled={loading}
>
Reset
</Button>
<Button
variant="default"
size="sm"
onClick={uploadFile}
disabled={loading}
>
Upload
</Button>
</div>
{loading && (
<div className="absolute left-0 top-0 h-full w-full bg-white/20">
<div className="" /> {/* spinner */}
</div>
)}
</section>
) : (
<section className="flex h-full w-full flex-col items-center justify-center rounded-lg bg-slate-100">
<UploadCloudIcon className="h-16 w-16 text-slate-800" />
<p className="text-sm font-semibold">Drag and Drop</p>
<p className="text-sm">or</p>
<Button
variant="outline"
className="mt-2 text-sm"
size="sm"
onClick={openFileBrowser}
>
Browse
</Button>
</section>
)}
{state.over &&
typeof window === 'object' &&
createPortal(
<div className="fixed left-0 top-0 z-50 grid h-dvh w-dvw place-items-center bg-slate-500/50 text-5xl font-bold backdrop-blur">
Drop file here
</div>,
document.body
)}
</DialogContent>
</Dialog>
)
}
18 changes: 11 additions & 7 deletions frontend-client/app/admin/problem/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PlusCircleIcon } from 'lucide-react'
import Link from 'next/link'
import * as React from 'react'
import { columns } from './_components/Columns'
import UploadDialog from './_components/UploadDialog'

const GET_PROBLEMS = gql(`
query GetProblems(
Expand Down Expand Up @@ -47,7 +48,7 @@ const GET_PROBLEMS = gql(`
export const dynamic = 'force-dynamic'

export default function Page() {
const { data, loading } = useQuery(GET_PROBLEMS, {
const { data, loading, refetch } = useQuery(GET_PROBLEMS, {
variables: {
groupId: 1,
take: 100,
Expand Down Expand Up @@ -88,12 +89,15 @@ export default function Page() {
Here&apos;s a list you made
</p>
</div>
<Link href="/admin/problem/create">
<Button variant="default">
<PlusCircleIcon className="mr-2 h-4 w-4" />
Create
</Button>
</Link>
<div className="flex gap-2">
<UploadDialog refetch={refetch} />
<Link href="/admin/problem/create">
<Button variant="default">
<PlusCircleIcon className="mr-2 h-4 w-4" />
Create
</Button>
</Link>
</div>
</div>
{loading ? (
<>
Expand Down
2 changes: 2 additions & 0 deletions frontend-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@uiw/codemirror-extensions-langs": "^4.21.22",
"@uiw/codemirror-themes": "^4.21.22",
"@uiw/react-codemirror": "^4.21.22",
"apollo-upload-client": "^18.0.1",
"cmdk": "^0.2.1",
"dayjs": "^1.11.10",
"embla-carousel-react": "8.0.0-rc22",
Expand All @@ -70,6 +71,7 @@
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/client-preset": "^4.2.2",
"@graphql-typed-document-node/core": "^3.2.0",
"@types/apollo-upload-client": "^18.0.0",
"@types/node": "^20.11.19",
"@types/react": "^18.2.56",
"@types/react-copy-to-clipboard": "^5.0.7",
Expand Down
Binary file added frontend-client/public/sample.xlsx
Binary file not shown.
Loading

0 comments on commit 17aaf13

Please sign in to comment.