Skip to content

Commit

Permalink
feat: documents page on admin
Browse files Browse the repository at this point in the history
  • Loading branch information
mrevanzak committed May 22, 2024
1 parent d28129b commit ffb88ff
Show file tree
Hide file tree
Showing 19 changed files with 356 additions and 12 deletions.
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@auth/drizzle-adapter": "^1.1.0",
"@mantine/dropzone": "^7.9.2",
"@t3-oss/env-nextjs": "^0.10.0",
"@tanstack/react-query": "^5.25.0",
"@tanya.in/ui": "*",
Expand All @@ -29,6 +30,7 @@
"drizzle-orm": "^0.30.10",
"drizzle-zod": "^0.5.1",
"geist": "^1.3.0",
"moment": "^2.30.1",
"next": "^14.2.0",
"next-auth": "beta",
"react": "18.3.1",
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/app/(app)/@admin/documents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DropzoneContainer } from "@/components/dropzone-container";
import { api } from "@/trpc/server";

import "@mantine/dropzone/styles.css";

export default async function DocumentsPage() {
const documents = await api.documents.get();

return (
<div className="flex flex-1 flex-col gap-4">
<h3 className="text-xl font-semibold">All Files</h3>
<DropzoneContainer initialData={documents} />
</div>
);
}
8 changes: 7 additions & 1 deletion apps/web/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { SidebarWrapper } from "@/components/sidebar/sidebar";
import { auth } from "@/server/auth";
import { get } from "@vercel/edge-config";

import { cn } from "@tanya.in/ui";

export default async function AuthLayout(props: {
user: React.ReactNode;
admin: React.ReactNode;
Expand All @@ -23,7 +25,11 @@ export default async function AuthLayout(props: {
{session?.user.role === "admin" && <SidebarWrapper />}
<div className="flex-1">
<Navbar />
<main className="container flex min-h-[calc(100vh-8rem)]">
<main
className={cn("container flex min-h-[calc(100vh-8rem)]", {
"!px-4 pt-4 2xl:pt-8": isAdmin,
})}
>
{isAdmin ? props.admin : props.user}
</main>
<Footer />
Expand Down
180 changes: 180 additions & 0 deletions apps/web/src/components/documents-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"use client";

import type { Document } from "@/server/api/routers/documents/documents.schema";
import React from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { api } from "@/trpc/react";
import moment from "moment";
import { MdOutlineDownloadForOffline } from "react-icons/md";
import { RiDeleteBin2Line } from "react-icons/ri";

import { Button } from "@tanya.in/ui/button";
import { Chip } from "@tanya.in/ui/chip";
import {
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from "@tanya.in/ui/modal";
import { Spinner } from "@tanya.in/ui/spinner";
import {
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from "@tanya.in/ui/table";
import { Tooltip } from "@tanya.in/ui/tooltip";

const columns = [
{
key: "name" as const,
label: "NAME",
},
{
key: "createdAt" as const,
label: "CREATED AT",
},
{
key: "actions" as const,
label: "ACTIONS",
},
];
type ColumnKey = (typeof columns)[number]["key"];

export function DocumentsTable() {
const router = useRouter();
const searchParams = useSearchParams();
const showModal = searchParams.get("delete") === "true";

const [document, setDocument] = React.useState<Document>();

const { data, isLoading } = api.documents.get.useQuery();

const renderCell = React.useCallback(
(document: Document, columnKey: ColumnKey) => {
switch (columnKey) {
case "createdAt":
return (
<Chip
size="sm"
variant="flat"
color={!document[columnKey] ? "default" : "success"}
>
<span className="text-xs capitalize">
{document[columnKey]
? moment(document[columnKey]).fromNow()
: "Never"}
</span>
</Chip>
);
case "actions":
return (
<div className="flex items-center gap-4 ">
<Tooltip
content="Download document"
color="primary"
className="p-2"
>
<button className="pointer-events-auto text-primary">
<MdOutlineDownloadForOffline size={20} />
</button>
</Tooltip>
<Tooltip content="Delete document" color="danger" className="p-2">
<Link
href="?delete=true"
onClick={() => setDocument(document)}
className="pointer-events-auto"
>
<RiDeleteBin2Line size={20} color="#FF0080" />
</Link>
</Tooltip>
</div>
);
default:
return document[columnKey];
}
},
[],
);

return (
<>
<Table
aria-label="Documents Table"
classNames={{
wrapper: "min-h-[31rem] relative",
}}
>
<TableHeader columns={columns}>
{(column) => (
<TableColumn
key={column.key}
hideHeader={column.key === "actions"}
width={column.key === "actions" ? 80 : undefined}
>
{column.label}
</TableColumn>
)}
</TableHeader>
<TableBody
items={data ?? []}
emptyContent={
!isLoading && (
<p>
No files found <br /> Drag and drop a file to upload
</p>
)
}
// isLoading={isLoading || uploadDocument.isPending}
loadingContent={<Spinner />}
>
{(item) => (
<TableRow key={item.id}>
{(columnKey) => (
<TableCell>
{renderCell(item, columnKey as ColumnKey)}
</TableCell>
)}
</TableRow>
)}
</TableBody>
</Table>

<Modal isOpen={showModal} onClose={() => router.back()}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader>Delete Document</ModalHeader>
<ModalBody>
<p>Are you sure you want to delete {document?.name}?</p>
</ModalBody>
<ModalFooter>
<Button variant="light" onClick={onClose}>
Close
</Button>
<Button
color="danger"
// onPress={() => {
// if (document)
// mutate(document._id, {
// onSuccess: () => {
// onClose();
// },
// });
// }}
// isLoading={isPending}
>
Delete
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
}
64 changes: 64 additions & 0 deletions apps/web/src/components/dropzone-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import type { Document } from "@/server/api/routers/documents/documents.schema";
import { useRef } from "react";
import { DocumentsTable } from "@/components/documents-table";
import { api } from "@/trpc/react";
import { Dropzone, PDF_MIME_TYPE } from "@mantine/dropzone";
import { FaUpload, FaXmark } from "react-icons/fa6";

import { Button } from "@tanya.in/ui/button";
import { Input } from "@tanya.in/ui/form";

export function DropzoneContainer({
initialData,
}: {
initialData: Document[];
}) {
const openRef = useRef<() => void>(null);

api.documents.get.useQuery(undefined, { initialData });
// const uploadDocument = useUploadDocument();

return (
<>
<div className="flex flex-wrap items-center justify-between gap-4">
<Input
className="max-w-sm"
placeholder="Search files"
classNames={{ inputWrapper: "dark:bg-content2 bg-content1" }}
/>
{/* //TODO: fix full reload when clicking this button */}
<Button color="primary" onClick={() => openRef.current?.()}>
Upload New Files
</Button>
</div>
<div className="mx-auto w-full">
<Dropzone
onDrop={(file) => {
if (file[0]) {
// uploadDocument.mutate(file[0]);
}
}}
accept={PDF_MIME_TYPE}
openRef={openRef}
//10MB
maxSize={10 * 1024 ** 2}
activateOnClick={false}
className="group"
>
<div className="pointer-events-none absolute inset-0 z-10 flex h-full w-full items-center justify-center group-data-[accept]:bg-success group-data-[reject]:bg-danger group-data-[accept]:bg-opacity-10 group-data-[reject]:bg-opacity-10">
<Dropzone.Accept>
<FaUpload className="text-success" size={54} />
</Dropzone.Accept>
<Dropzone.Reject>
<FaXmark className="text-danger" size={54} />
</Dropzone.Reject>
</div>

<DocumentsTable />
</Dropzone>
</div>
</>
);
}
19 changes: 11 additions & 8 deletions apps/web/src/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as React from "react";
import { useLockedBody } from "@/lib/hooks/useBodyLock";
import { TRPCReactProvider } from "@/trpc/react";
import { MantineProvider } from "@mantine/core";
import { NextUIProvider } from "@nextui-org/system";

import { ThemeProvider } from "@tanya.in/ui/theme";
Expand All @@ -21,14 +22,16 @@ export function Providers({ children }: { children: React.ReactNode }) {
<NextUIProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TRPCReactProvider>
<SidebarContext.Provider
value={{
collapsed: sidebarOpen,
setCollapsed: handleToggleSidebar,
}}
>
{children}
</SidebarContext.Provider>
<MantineProvider>
<SidebarContext.Provider
value={{
collapsed: sidebarOpen,
setCollapsed: handleToggleSidebar,
}}
>
{children}
</SidebarContext.Provider>
</MantineProvider>
</TRPCReactProvider>
</ThemeProvider>
</NextUIProvider>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { analyticsRouter } from "@/server/api/routers/analytics/analytics.procedure";
import { authRouter } from "@/server/api/routers/auth/auth.procedure";
import { documentsRouter } from "@/server/api/routers/documents/documents.procedure";

import { createCallerFactory, createTRPCRouter } from "./trpc";

Expand All @@ -9,6 +11,8 @@ import { createCallerFactory, createTRPCRouter } from "./trpc";
*/
export const appRouter = createTRPCRouter({
auth: authRouter,
analytics: analyticsRouter,
documents: documentsRouter,
});

// export type definition of API
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/server/api/routers/documents/documents.procedure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Document } from "@/server/api/routers/documents/documents.schema";
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";

export const documentsRouter = createTRPCRouter({
get: adminProcedure.query(() => {
const documents: Document[] = [
{
id: "1",
name: "Document 1.pdf",
createdAt: new Date(),
uploadedBy: "3152ffe5-6496-4214-8fcd-b69fb4f70fd5",
},

{
id: "2",
name: "Document 2.pdf",
createdAt: new Date(),
uploadedBy: "3152ffe5-6496-4214-8fcd-b69fb4f70fd5",
},
];

return documents;
}),
});
6 changes: 6 additions & 0 deletions apps/web/src/server/api/routers/documents/documents.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { z } from "zod";
import { documents } from "@/server/db/schema";
import { createSelectSchema } from "drizzle-zod";

export const documentSchema = createSelectSchema(documents);
export type Document = z.infer<typeof documentSchema>;
10 changes: 10 additions & 0 deletions apps/web/src/server/api/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,13 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
},
});
});

export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You need an admin permission to access this resource",
});
}
return next();
});
Loading

0 comments on commit ffb88ff

Please sign in to comment.