Mad/UI improvements in /explore (#127)

* feature: delete file

* feature: consume /explore/file_name to view details of an uploaded document

* feature: optimistic update when deleting file

* feature: Loading state for /explore

* style: Exit animation

* style: responsive card
This commit is contained in:
!MAD! 2023-05-23 11:45:13 +05:30 committed by GitHub
parent 9f926c2e2b
commit e4217fe15f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 179 additions and 54 deletions

View File

@ -19,6 +19,8 @@ const ButtonVariants = cva(
tertiary: "text-black dark:text-white bg-transparent py-2 px-4",
secondary:
"border border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white hover:bg-black dark:hover:bg-white hover:text-white dark:hover:text-black focus:text-white transition-colors py-2 px-4 shadow-none",
danger:
"border border-red-500 hover:bg-red-500 hover:text-white transition-colors",
},
brightness: {
dim: "",
@ -55,5 +57,5 @@ const Button: FC<ButtonProps> = forwardRef(
}
);
Button.displayName = 'Button';
Button.displayName = "Button";
export default Button;

View File

@ -10,9 +10,16 @@ interface ModalProps {
desc: string;
children?: ReactNode;
Trigger: ReactNode;
CloseTrigger?: ReactNode;
}
const Modal: FC<ModalProps> = ({ title, desc, children, Trigger }) => {
const Modal: FC<ModalProps> = ({
title,
desc,
children,
Trigger,
CloseTrigger,
}) => {
const [open, setOpen] = useState(false);
return (
<Dialog.Root onOpenChange={setOpen}>
@ -51,9 +58,13 @@ const Modal: FC<ModalProps> = ({ title, desc, children, Trigger }) => {
{children}
<Dialog.Close asChild>
<Button variant={"secondary"} className="self-end">
Done
</Button>
{CloseTrigger ? (
CloseTrigger
) : (
<Button variant={"secondary"} className="self-end">
Done
</Button>
)}
</Dialog.Close>
<Dialog.Close asChild>

View File

@ -0,0 +1,10 @@
import { FC } from "react";
import { FaSpinner } from "react-icons/fa";
interface SpinnerProps {}
const Spinner: FC<SpinnerProps> = ({}) => {
return <FaSpinner className="animate-spin m-5" />;
};
export default Spinner;

View File

@ -1,34 +0,0 @@
"use client";
import { FC } from "react";
import { Document } from "./types";
import Button from "../components/ui/Button";
import Modal from "../components/ui/Modal";
import { AnimatedCard } from "../components/ui/Card";
interface DocumentProps {
document: Document;
}
const DocumentItem: FC<DocumentProps> = ({ document }) => {
return (
<AnimatedCard
initial={{ x: -64, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="flex items-center justify-between w-full p-5 gap-10"
>
<p className="text-lg leading-tight max-w-sm">{document.name}</p>
<Modal
title={document.name}
desc={""}
Trigger={<Button className="">View</Button>}
>
<div className="bg-white py-10 w-full h-1/2 overflow-auto rounded-lg prose">
<pre>{JSON.stringify(document, null, 2)}</pre>
</div>
</Modal>
</AnimatedCard>
);
};
DocumentItem.displayName = "DocumentItem";
export default DocumentItem;

View File

@ -0,0 +1,37 @@
import axios from "axios";
import { Document } from "../types";
interface DocumentDataProps {
documentName: string;
}
const DocumentData = async ({ documentName }: DocumentDataProps) => {
const res = await axios.get(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${documentName}`
);
const documents = res.data.documents as any[];
const doc = documents[0];
return (
<div className="prose">
<p>No. of documents: {documents.length}</p>
{/* {documents.map((doc) => (
<pre key={doc.name}>{JSON.stringify(doc)}</pre>
))} */}
<div className="flex flex-col gap-2">
{documents[0] &&
Object.keys(documents[0]).map((k) => {
return (
<div className="grid grid-cols-2 border-b py-2" key={k}>
<span className="capitalize font-bold">
{k.replaceAll("_", " ")}
</span>
<span className="">{documents[0][k] || "Not Available"}</span>
</div>
);
})}
</div>
</div>
);
};
export default DocumentData;

View File

@ -0,0 +1,84 @@
"use client";
import { Document } from "../types";
import Button from "../../components/ui/Button";
import Modal from "../../components/ui/Modal";
import { AnimatedCard } from "../../components/ui/Card";
import { Dispatch, SetStateAction, Suspense, useState } from "react";
import axios from "axios";
import DocumentData from "./DocumentData";
import Spinner from "@/app/components/ui/Spinner";
interface DocumentProps {
document: Document;
setDocuments: Dispatch<SetStateAction<Document[]>>;
}
const DocumentItem = ({ document, setDocuments }: DocumentProps) => {
const [isDeleting, setIsDeleting] = useState(false);
const deleteDocument = async (name: string) => {
setIsDeleting(true);
try {
console.log(`Deleting Document ${name}`);
const response = await axios.delete(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`
);
setDocuments((docs) => docs.filter((doc) => doc.name !== name)); // Optimistic update
} catch (error) {
console.error(`Error deleting ${name}`, error);
}
setIsDeleting(false);
};
return (
<AnimatedCard
initial={{ x: -64, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 64, opacity: 0 }}
className="flex flex-col sm:flex-row sm:items-center justify-between w-full p-5 gap-5"
>
<p className="text-lg leading-tight max-w-sm">{document.name}</p>
<div className="flex gap-2 self-end">
{/* VIEW MODAL */}
<Modal
title={document.name}
desc={""}
Trigger={<Button className="">View</Button>}
>
<Suspense fallback={<Spinner />}>
{/* @ts-expect-error */}
<DocumentData documentName={document.name} />
</Suspense>
</Modal>
{/* DELETE MODAL */}
<Modal
title={"Confirm"}
desc={`Do you really want to delete?`}
Trigger={
<Button isLoading={isDeleting} variant={"danger"} className="">
Delete
</Button>
}
CloseTrigger={
<Button
variant={"danger"}
isLoading={isDeleting}
onClick={() => {
deleteDocument(document.name);
}}
className="self-end"
>
Delete forever
</Button>
}
>
<p>{document.name}</p>
</Modal>
</div>
</AnimatedCard>
);
};
DocumentItem.displayName = "DocumentItem";
export default DocumentItem;

View File

@ -5,15 +5,19 @@ import DocumentItem from "./DocumentItem";
import { Document } from "./types";
import Button from "../components/ui/Button";
import Link from "next/link";
import Spinner from "../components/ui/Spinner";
import { AnimatePresence } from "framer-motion";
export default function ExplorePage() {
const [documents, setDocuments] = useState<Document[]>([]);
const [isPending, setIsPending] = useState(true);
useEffect(() => {
fetchDocuments();
}, []);
const fetchDocuments = async () => {
setIsPending(true);
try {
console.log(
`Fetching documents from ${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`
@ -26,28 +30,39 @@ export default function ExplorePage() {
console.error("Error fetching documents", error);
setDocuments([]);
}
setIsPending(false);
};
return (
<div className="pt-20 flex flex-col items-center justify-center p-6">
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center justify-center my-10">
<h1 className="text-3xl font-bold text-center">Explore Your Brain</h1>
<h2 className="opacity-50">View what&rsquo;s in your second brain</h2>
</div>
<div className="w-full max-w-xl flex flex-col gap-5">
{documents.length !== 0 ? (
documents.map((document, index) => (
<DocumentItem key={index} document={document} />
))
) : (
<div className="flex flex-col items-center justify-center mt-10 gap-1">
<p className="text-center">Oh No, Your Brain is empty.</p>
<Link href="/upload">
<Button>Upload Files</Button>
</Link>
</div>
)}
</div>
{isPending ? (
<Spinner />
) : (
<div className="w-full max-w-xl flex flex-col gap-5">
{documents.length !== 0 ? (
<AnimatePresence>
{documents.map((document) => (
<DocumentItem
key={document.name}
document={document}
setDocuments={setDocuments}
/>
))}
</AnimatePresence>
) : (
<div className="flex flex-col items-center justify-center mt-10 gap-1">
<p className="text-center">Oh No, Your Brain is empty.</p>
<Link href="/upload">
<Button>Upload Files</Button>
</Link>
</div>
)}
</div>
)}
</div>
);
}