appdev_with_generative_ai/src/knowledge-drive/components/NewButton.tsx (190 lines of code) (raw):
"use client";
import { AiOutlinePlus } from "react-icons/ai";
import { Button } from "./ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
} from "./ui/dropdown-menu";
import { AiOutlineFolderAdd } from "react-icons/ai";
import { GrDocumentUpload } from "react-icons/gr";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Input } from "./ui/input";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
import { useState } from "react";
import { usePathname } from "next/navigation";
import { createFile, createFolder } from "@/lib/firebase/firestore";
import { User } from "firebase/auth";
import { getFolderId } from "@/lib/utils";
import { Label } from "./ui/label";
import { alphanumeric } from "nanoid-dictionary";
import { customAlphabet } from "nanoid";
import {
getDownloadURL,
getStorage,
ref,
uploadBytesResumable,
} from "firebase/storage";
import firebaseClientApp from "@/lib/firebase/client";
import { toast } from "react-hot-toast";
const storage = getStorage(firebaseClientApp);
const formSchema = z.object({
folderName: z.string().min(3).max(20),
});
type NewButtonProps = {
user: User;
};
const NewButton = ({ user }: NewButtonProps) => {
const pathname = usePathname();
const [dialogOpen, setDialogOpen] = useState(false);
const [dropdownMenuOpen, setDropdownMenuOpen] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
folderName: "",
},
});
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) return;
const file = e.target.files[0];
const fileId = customAlphabet(alphanumeric, 20)();
var fileExtension = file.name.split(".").pop();
const filePath = `/files/${user.uid}/${fileId}.${fileExtension}`;
const storageRef = ref(storage, filePath);
const uploadTask = uploadBytesResumable(storageRef, file, {
contentType: file.type,
});
setDropdownMenuOpen(false);
uploadTask.on(
"state_changed",
(snapshot) => {
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log("upload is " + progress + "% done");
switch (snapshot.state) {
case "paused":
console.log("upload is paused");
break;
case "running":
console.log("upload is running");
break;
}
},
(error) => {
switch (error.code) {
case "storage/unauthorized":
console.log("NO PERMISSION");
break;
case "storage/canceled":
break;
case "storage/unknown":
break;
}
toast.error("ファイルのアップロードに失敗しました");
},
() => {
console.log("upload finished!!");
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
createFile({
id: fileId,
name: file.name,
parent: getFolderId(pathname),
uid: user.uid,
size: file.size,
url: downloadURL,
});
});
toast.success("ファイルのアップロードが完了しました");
}
);
};
const onSubmit = async (values: z.infer<typeof formSchema>) => {
form.reset();
setDialogOpen(false);
await createFolder({
name: values.folderName,
parent: getFolderId(pathname),
uid: user.uid,
});
};
return (
<div className="grid-in-newb max-w-[256px] px-4 pt-2 pb-4">
<div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DropdownMenu
open={dropdownMenuOpen}
onOpenChange={setDropdownMenuOpen}
>
<DropdownMenuTrigger>
<div className="items-center flex gap-3 bg-white h-14 text-black cursor-pointer min-w-[100px] rounded-xl drop-shadow-md py-[18px] pl-4 pr-5 hover:bg-[#edf2fc]">
<AiOutlinePlus size={24} />
<span>新規</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={-40} className="ml-4">
<div className="h-22 w-[240px] flex-col flex text-[14px] gap-2">
<DialogTrigger>
<div
className="mt-2 px-2 h-8 hover:bg-slate-100 flex items-center gap-3"
onClick={() => setDropdownMenuOpen(false)}
>
<AiOutlineFolderAdd size={24} />
<span>新しいフォルダ</span>
</div>
</DialogTrigger>
<Label
htmlFor="file"
className="mb-2 pl-[10px] h-8 hover:bg-slate-100 flex items-center gap-3"
>
<Input
type="file"
className="opacity-0 absolute left-[-9999px]"
onChange={handleUpload}
id="file"
/>
<GrDocumentUpload size={20} />
<span>ファイルのアップロード</span>
</Label>
</div>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-[24px]">新しいフォルダ</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
>
<FormField
control={form.control}
name="folderName"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="無題のフォルダ" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="reset"
variant="ghost"
onClick={() => form.reset()}
>
リセット
</Button>
<Button type="submit" variant="ghost">
作成
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
</div>
);
};
export default NewButton;