mirror of
https://github.com/hanzoai/captable.git
synced 2026-10-16 14:21:29 +00:00
feat: add basic document preview and update document schema (#120)
* feat: add react pdf * fix: upload provider enum * feat: add pdf viewer component * feat: add company url * feat: add get document procedure * feat: add get all documents procedure * feat: add create document procedure * fix: schema * feat; add all procedures * fix: remove explicit type casting * feat: update document schema * feat: make uploader component more generic * feat: use trpc and trpc types * feat: use company id * feat: add page layout component * feat: add equity document upload page * feat: add document detail page * featL add pdf worker * feat: rename props * feat: add templates page * feat : add bucket model * feat: add relation for bucket * feat: refactor document fields * feat: update schema * feat: add status for template * feat: add bucket router * feat: add create template procedure * fix: relation * feat: migration * feat: add get template procedure * feat: update document schema * feat: use bucket procedure for uploader * feat: use relavant procedures * feat: use template for query * feat: use data * fix: provider enum * feat: add order by * feat: add new fields * fix: eslint and build error and some minor changes --------- Co-authored-by: Puru D <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Puru D
parent
3e63fb1a97
commit
36b11e3606
@@ -0,0 +1 @@
|
||||
public/pdf.worker.min.js
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
||||
* for Docker builds.
|
||||
@@ -9,6 +11,15 @@ const config = {
|
||||
images: {
|
||||
domains: ["randomuser.me"],
|
||||
},
|
||||
webpack: (config) => {
|
||||
/**
|
||||
* Critical: prevents " ⨯ ./node_modules/canvas/build/Release/canvas.node
|
||||
* Module parse failed: Unexpected character '�' (1:0)" error
|
||||
*/
|
||||
config.resolve.alias.canvas = false;
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
async redirects() {
|
||||
return [
|
||||
|
||||
+4
-1
@@ -17,7 +17,8 @@
|
||||
"start": "next start",
|
||||
"format": "prettier --write \"**/*.{css,js,json,jsx,ts,tsx}\" && npx prisma format",
|
||||
"email:preview": "email preview ./src/emails",
|
||||
"smpt:dev": "mailpit"
|
||||
"smpt:dev": "mailpit",
|
||||
"copy:pdfjs": "node scripts/copy-pdfjs-worker.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/plots": "^2.1.12",
|
||||
@@ -50,6 +51,7 @@
|
||||
"@trpc/next": "^10.43.6",
|
||||
"@trpc/react-query": "^10.43.6",
|
||||
"@trpc/server": "^10.43.6",
|
||||
"@wojtekmaj/react-hooks": "^1.18.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
@@ -67,6 +69,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-pdf": "^7.7.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.33.2",
|
||||
"superjson": "^2.2.1",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `uploadedById` on the `Document` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[publicId]` on the table `Document` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `mimeType` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `publicId` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `uploadProvider` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `uploaderId` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||
- Changed the type of `type` on the `Document` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UploadProviders" AS ENUM ('S3', 'R2');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentType" AS ENUM ('SAFE', 'EQUITY', 'GENERIC');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentStatus" AS ENUM ('DRAFT', 'COMPLETED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentFieldTypes" AS ENUM ('TEXT', 'TEXT_AREA', 'RADIO', 'CHECK_BOX', 'SIGNATURE', 'DATE', 'DATE_TIME', 'EMAIL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DocumentSigningStatus" AS ENUM ('NOT_SIGNED', 'SIGNED');
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Document_uploadedById_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" DROP COLUMN "uploadedById",
|
||||
ADD COLUMN "mimeType" TEXT NOT NULL,
|
||||
ADD COLUMN "publicId" TEXT NOT NULL,
|
||||
ADD COLUMN "status" "DocumentStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
ADD COLUMN "uploadProvider" "UploadProviders" NOT NULL,
|
||||
ADD COLUMN "uploaderId" TEXT NOT NULL,
|
||||
DROP COLUMN "type",
|
||||
ADD COLUMN "type" "DocumentType" NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DocumentFields" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "DocumentFieldTypes" NOT NULL DEFAULT 'TEXT',
|
||||
"placeholder" TEXT NOT NULL DEFAULT '',
|
||||
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||
"documentId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DocumentFields_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DocumentRecepients" (
|
||||
"id" TEXT NOT NULL,
|
||||
"signedStatus" "DocumentSigningStatus" NOT NULL,
|
||||
"membershipId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DocumentRecepients_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DocumentFields_documentId_idx" ON "DocumentFields"("documentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DocumentRecepients_membershipId_idx" ON "DocumentRecepients"("membershipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Document_uploaderId_idx" ON "Document"("uploaderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Document_publicId_key" ON "Document"("publicId");
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `key` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `mimeType` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `size` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `status` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `type` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `uploadProvider` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the `DocumentFields` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `DocumentRecepients` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `bucketId` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FieldTypes" AS ENUM ('text', 'textArea', 'radio', 'checkBox', 'signature', 'date', 'dateTime', 'email');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TemplateStatus" AS ENUM ('draft', 'completed');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EsignRecipientStatus" AS ENUM ('notSigned', 'signed');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" DROP COLUMN "key",
|
||||
DROP COLUMN "mimeType",
|
||||
DROP COLUMN "size",
|
||||
DROP COLUMN "status",
|
||||
DROP COLUMN "type",
|
||||
DROP COLUMN "uploadProvider",
|
||||
ADD COLUMN "bucketId" TEXT NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "DocumentFields";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "DocumentRecepients";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "DocumentFieldTypes";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "DocumentSigningStatus";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "DocumentStatus";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "DocumentType";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "UploadProviders";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Bucket" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"mimeType" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Bucket_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TemplateField" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "FieldTypes" NOT NULL DEFAULT 'text',
|
||||
"placeholder" TEXT NOT NULL DEFAULT '',
|
||||
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||
"positionX" DECIMAL(65,30) NOT NULL,
|
||||
"positionY" DECIMAL(65,30) NOT NULL,
|
||||
"width" DECIMAL(65,30) NOT NULL,
|
||||
"height" DECIMAL(65,30) NOT NULL,
|
||||
"templateId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TemplateField_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Template" (
|
||||
"id" TEXT NOT NULL,
|
||||
"publicId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"status" "TemplateStatus" NOT NULL DEFAULT 'draft',
|
||||
"bucketId" TEXT NOT NULL,
|
||||
"uploaderId" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EsignRecipient" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"status" "EsignRecipientStatus" NOT NULL,
|
||||
"membershipId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "EsignRecipient_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TemplateField_templateId_idx" ON "TemplateField"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Template_bucketId_idx" ON "Template"("bucketId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Template_uploaderId_idx" ON "Template"("uploaderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Template_companyId_idx" ON "Template"("companyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "EsignRecipient_membershipId_idx" ON "EsignRecipient"("membershipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Document_bucketId_idx" ON "Document"("bucketId");
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `verificationTokenToken` to the `EsignRecipient` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `order` to the `TemplateField` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "EsignRecipient" ADD COLUMN "verificationTokenToken" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TemplateField" ADD COLUMN "order" INTEGER NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "EsignRecipient_verificationTokenToken_idx" ON "EsignRecipient"("verificationTokenToken");
|
||||
+124
-15
@@ -51,7 +51,6 @@ model User {
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
companies Membership[]
|
||||
documents Document[]
|
||||
}
|
||||
|
||||
model WaitlistUser {
|
||||
@@ -61,9 +60,10 @@ model WaitlistUser {
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
eSignRecipients EsignRecipient[]
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
@@ -89,6 +89,7 @@ model Company {
|
||||
shareClasses ShareClass[]
|
||||
equityPlans EquityPlan[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
|
||||
@@unique([publicId])
|
||||
}
|
||||
@@ -123,6 +124,10 @@ model Membership {
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
documentReceived EsignRecipient[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
|
||||
@@unique([companyId, userId])
|
||||
@@index([companyId])
|
||||
@@index([status])
|
||||
@@ -230,21 +235,125 @@ model EquityPlan {
|
||||
@@index([companyId])
|
||||
}
|
||||
|
||||
model Document {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
type String
|
||||
size Int
|
||||
key String
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
model Bucket {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
key String
|
||||
mimeType String
|
||||
size Int
|
||||
|
||||
uploadedById String?
|
||||
uploadedBy User? @relation(fields: [uploadedById], references: [id], onDelete: Cascade)
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Document {
|
||||
id String @id @default(cuid())
|
||||
publicId String
|
||||
name String
|
||||
|
||||
bucketId String
|
||||
bucket Bucket @relation(fields: [bucketId], references: [id])
|
||||
|
||||
uploaderId String
|
||||
uploader Membership @relation(fields: [uploaderId], references: [id])
|
||||
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([publicId])
|
||||
@@index([bucketId])
|
||||
@@index([uploaderId])
|
||||
@@index([companyId])
|
||||
@@index([uploadedById])
|
||||
}
|
||||
|
||||
enum FieldTypes {
|
||||
text
|
||||
textArea
|
||||
radio
|
||||
checkBox
|
||||
signature
|
||||
date
|
||||
dateTime
|
||||
email
|
||||
}
|
||||
|
||||
model TemplateField {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
type FieldTypes @default(text)
|
||||
placeholder String @default("")
|
||||
required Boolean @default(false)
|
||||
positionX Decimal
|
||||
positionY Decimal
|
||||
width Decimal
|
||||
height Decimal
|
||||
order Int
|
||||
Template Template @relation(fields: [templateId], references: [id])
|
||||
templateId String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([templateId])
|
||||
}
|
||||
|
||||
enum TemplateStatus {
|
||||
draft
|
||||
completed
|
||||
}
|
||||
|
||||
model Template {
|
||||
id String @id @default(cuid())
|
||||
publicId String
|
||||
name String
|
||||
status TemplateStatus @default(draft)
|
||||
|
||||
bucketId String
|
||||
bucket Bucket @relation(fields: [bucketId], references: [id])
|
||||
|
||||
uploaderId String
|
||||
uploader Membership @relation(fields: [uploaderId], references: [id])
|
||||
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
fields TemplateField[]
|
||||
|
||||
@@index([bucketId])
|
||||
@@index([uploaderId])
|
||||
@@index([companyId])
|
||||
}
|
||||
|
||||
enum EsignRecipientStatus {
|
||||
notSigned
|
||||
signed
|
||||
}
|
||||
|
||||
model EsignRecipient {
|
||||
id String @id @default(cuid())
|
||||
|
||||
email String?
|
||||
|
||||
status EsignRecipientStatus
|
||||
|
||||
membershipId String?
|
||||
membership Membership? @relation(fields: [membershipId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
token VerificationToken @relation(fields: [verificationTokenToken], references: [token])
|
||||
verificationTokenToken String
|
||||
|
||||
@@index([membershipId])
|
||||
@@index([verificationTokenToken])
|
||||
}
|
||||
|
||||
Vendored
+22
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const pdfjsDistPath = path.dirname(require.resolve("pdfjs-dist/package.json"));
|
||||
|
||||
const pdfWorkerPath = path.join(pdfjsDistPath, "build", "pdf.worker.min.js");
|
||||
|
||||
fs.copyFileSync(pdfWorkerPath, "./public/pdf.worker.min.js");
|
||||
@@ -0,0 +1,5 @@
|
||||
const DocumentDetailPage = async () => {
|
||||
return <div>document detail page</div>;
|
||||
};
|
||||
|
||||
export default DocumentDetailPage;
|
||||
@@ -3,14 +3,23 @@
|
||||
import { useState } from "react";
|
||||
import Modal from "@/components/shared/modal";
|
||||
import Uploader from "@/components/ui/uploader";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { api } from "@/trpc/react";
|
||||
|
||||
type DocumentUploadModalProps = {
|
||||
trigger: React.ReactNode;
|
||||
companyPublicId: string;
|
||||
};
|
||||
|
||||
const DocumentUploadModal = ({ trigger }: DocumentUploadModalProps) => {
|
||||
const DocumentUploadModal = ({
|
||||
trigger,
|
||||
companyPublicId,
|
||||
}: DocumentUploadModalProps) => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync } = api.document.create.useMutation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Upload a document"
|
||||
@@ -23,7 +32,19 @@ const DocumentUploadModal = ({ trigger }: DocumentUploadModalProps) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Uploader setOpen={setOpen} />
|
||||
<Uploader
|
||||
identifier={companyPublicId}
|
||||
keySuffix="generic-document"
|
||||
onSuccess={async (uploadedData) => {
|
||||
await mutateAsync({
|
||||
name: uploadedData.name,
|
||||
bucketId: uploadedData.id,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
import { db } from "@/server/db";
|
||||
import DocumentsTable from "./table";
|
||||
import DocumentUploadModal from "./modal";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { withServerSession } from "@/server/auth";
|
||||
import type { Document, User } from "@prisma/client";
|
||||
|
||||
import EmptyState from "@/components/shared/empty-state";
|
||||
import { RiUploadCloudLine, RiAddFill } from "@remixicon/react";
|
||||
|
||||
export interface DocumentType extends Document {
|
||||
uploadedBy: User | null;
|
||||
}
|
||||
|
||||
const getDocuments = async (companyId: string) => {
|
||||
return await db.document.findMany({
|
||||
where: { companyId },
|
||||
include: {
|
||||
uploadedBy: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
};
|
||||
import { api } from "@/trpc/server";
|
||||
import { withServerSession } from "@/server/auth";
|
||||
|
||||
const DocumentsPage = async () => {
|
||||
const documents = await api.document.getAll.query();
|
||||
const session = await withServerSession();
|
||||
const companyId = session?.user?.companyId;
|
||||
const documents: DocumentType[] = await getDocuments(companyId);
|
||||
|
||||
if (documents.length === 0) {
|
||||
return (
|
||||
@@ -37,6 +20,7 @@ const DocumentsPage = async () => {
|
||||
subtitle="Please click the button below to upload a new document."
|
||||
>
|
||||
<DocumentUploadModal
|
||||
companyPublicId={session.user.companyPublicId}
|
||||
trigger={
|
||||
<Button size="lg">
|
||||
<RiAddFill className="mr-2 h-5 w-5" />
|
||||
@@ -60,6 +44,7 @@ const DocumentsPage = async () => {
|
||||
|
||||
<div>
|
||||
<DocumentUploadModal
|
||||
companyPublicId={session.user.companyPublicId}
|
||||
trigger={
|
||||
<Button>
|
||||
<RiAddFill className="mr-2 h-5 w-5" />
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { dayjsExt } from "@/common/dayjs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import type { Document, User } from "@prisma/client";
|
||||
import FileIcon from "@/components/shared/file-icon";
|
||||
import { RiFileDownloadLine } from "@remixicon/react";
|
||||
import { getPresignedGetUrl } from "@/server/file-uploads";
|
||||
@@ -15,13 +14,12 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
} from "@/components/ui/table";
|
||||
import { type RouterOutputs } from "@/trpc/shared";
|
||||
|
||||
interface DocumentType extends Document {
|
||||
uploadedBy: User | null;
|
||||
}
|
||||
type DocumentType = RouterOutputs["document"]["getAll"];
|
||||
|
||||
type DocumentTableProps = {
|
||||
documents: DocumentType[];
|
||||
documents: DocumentType;
|
||||
};
|
||||
|
||||
const DocumentsTable = ({ documents }: DocumentTableProps) => {
|
||||
@@ -46,15 +44,15 @@ const DocumentsTable = ({ documents }: DocumentTableProps) => {
|
||||
{documents.map((document) => (
|
||||
<TableRow key={document.id}>
|
||||
<TableCell className="flex items-center ">
|
||||
<FileIcon type={document.type} />
|
||||
<FileIcon type={document.bucket.mimeType} />
|
||||
<span className="flex">{document.name}</span>
|
||||
</TableCell>
|
||||
<TableCell>{document.uploadedBy?.name}</TableCell>
|
||||
<TableCell>{document.uploader.user.name}</TableCell>
|
||||
<TableCell>{dayjsExt().to(document.createdAt)}</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await openFileOnTab(document.key);
|
||||
await openFileOnTab(document.bucket.key);
|
||||
}}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary/80"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
const EquityPage = () => {
|
||||
return <div>Equity page</div>;
|
||||
import { PageLayout } from "@/components/dashboard/page-layout";
|
||||
import { EquityDocumentUpload } from "@/components/equity/equity-document-upload";
|
||||
import { withServerSession } from "@/server/auth";
|
||||
|
||||
const EquityPage = async () => {
|
||||
const session = await withServerSession();
|
||||
const user = session.user;
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="Equity"
|
||||
action={<EquityDocumentUpload companyPublicId={user.companyPublicId} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EquityPage;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { api } from "@/trpc/server";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import PdfViewer from "@/components/ui/pdf-viewer";
|
||||
|
||||
const TemplateDetailPage = async ({
|
||||
params,
|
||||
}: {
|
||||
params: { templatePublicId: string };
|
||||
}) => {
|
||||
const { url } = await api.template.get.query({
|
||||
publicId: params.templatePublicId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="relative col-span-12 h-full lg:col-span-6 xl:col-span-4">
|
||||
<Card className="fixed h-fit w-full max-w-[300px]">
|
||||
{/* Input fields */}
|
||||
<div className="h-[500px]">Sidebar</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-8">
|
||||
<PdfViewer file={url} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateDetailPage;
|
||||
+13
-5
@@ -3,8 +3,6 @@ import {
|
||||
type getPresignedUrlOptions,
|
||||
} from "@/server/file-uploads";
|
||||
|
||||
import { type DocumentMutationType } from "@/trpc/routers/document-router/schema";
|
||||
|
||||
/**
|
||||
* usage
|
||||
* ```js
|
||||
@@ -21,12 +19,15 @@ import { type DocumentMutationType } from "@/trpc/routers/document-router/schema
|
||||
*/
|
||||
export const uploadFile = async (
|
||||
file: File,
|
||||
options?: Pick<getPresignedUrlOptions, "expiresIn" | "keyPrefix">,
|
||||
options: Pick<
|
||||
getPresignedUrlOptions,
|
||||
"expiresIn" | "keySuffix" | "identifier"
|
||||
>,
|
||||
) => {
|
||||
const { url, key } = await getPresignedPutUrl({
|
||||
contentType: file.type,
|
||||
fileName: file.name,
|
||||
...(options && { ...options }),
|
||||
...options,
|
||||
});
|
||||
|
||||
const body = await file.arrayBuffer();
|
||||
@@ -46,5 +47,12 @@ export const uploadFile = async (
|
||||
}
|
||||
|
||||
const { name, type, size } = file;
|
||||
return { key, name, type, size } as DocumentMutationType;
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
mimeType: type,
|
||||
size,
|
||||
};
|
||||
};
|
||||
|
||||
export type TUploadFile = Awaited<ReturnType<typeof uploadFile>>;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
interface PageLayoutProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function PageLayout({
|
||||
action,
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
}: PageLayoutProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<div className="flex items-center justify-between gap-y-3 ">
|
||||
<div className="gap-y-3">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>{action}</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import Modal from "../shared/modal";
|
||||
import { Button } from "../ui/button";
|
||||
import { Uploader } from "../ui/uploader";
|
||||
import { api } from "@/trpc/react";
|
||||
|
||||
interface EquityDocumentUploadProps {
|
||||
companyPublicId: string;
|
||||
}
|
||||
|
||||
export function EquityDocumentUpload({
|
||||
companyPublicId,
|
||||
}: EquityDocumentUploadProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync } = api.template.create.useMutation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Upload equity document"
|
||||
subtitle=""
|
||||
trigger={<Button>Upload Document</Button>}
|
||||
>
|
||||
<Uploader
|
||||
identifier={companyPublicId}
|
||||
keySuffix="equity-doc-uploads"
|
||||
onSuccess={async (bucketData) => {
|
||||
const equityTemplate = await mutateAsync({
|
||||
bucketId: bucketData.id,
|
||||
name: bucketData.name,
|
||||
});
|
||||
|
||||
router.push(
|
||||
`/${companyPublicId}/templates/${equityTemplate.publicId}`,
|
||||
);
|
||||
}}
|
||||
accept={{
|
||||
"application/pdf": [".pdf"],
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { pdfjs, Document, Page } from "react-pdf";
|
||||
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
|
||||
import "react-pdf/dist/esm/Page/TextLayer.css";
|
||||
import { useResizeObserver } from "@wojtekmaj/react-hooks";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `/pdf.worker.min.js`;
|
||||
|
||||
const options = {
|
||||
cMapUrl: "/cmaps/",
|
||||
standardFontDataUrl: "/standard_fonts/",
|
||||
};
|
||||
|
||||
const resizeObserverOptions = {};
|
||||
|
||||
const maxWidth = 800;
|
||||
|
||||
interface PdfViewerProps {
|
||||
file: string | File | null;
|
||||
}
|
||||
|
||||
const PdfViewer = ({ file }: PdfViewerProps) => {
|
||||
const [numPages, setNumPages] = useState<number>();
|
||||
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
|
||||
const [containerWidth, setContainerWidth] = useState<number>();
|
||||
|
||||
const onResize = useCallback<ResizeObserverCallback>((entries) => {
|
||||
const [entry] = entries;
|
||||
|
||||
if (entry) {
|
||||
setContainerWidth(entry.contentRect.width);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useResizeObserver(containerRef, resizeObserverOptions, onResize);
|
||||
|
||||
const onDocumentLoadSuccess = (event: {
|
||||
numPages?: number;
|
||||
document?: {
|
||||
numPages: number;
|
||||
};
|
||||
}): void => {
|
||||
const nextNumPages = event.numPages ?? event.document?.numPages;
|
||||
setNumPages(nextNumPages);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[calc(100%-2em)]" ref={setContainerRef}>
|
||||
<Document
|
||||
file={file}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
options={options}
|
||||
>
|
||||
{Array.from(new Array(numPages), (el, index) => (
|
||||
<Page
|
||||
key={`page_${index + 1}`}
|
||||
pageNumber={index + 1}
|
||||
width={
|
||||
containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PdfViewer;
|
||||
@@ -1,45 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "./button";
|
||||
import { api } from "@/trpc/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
import { uploadFile } from "@/common/uploads";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useDropzone, type FileWithPath } from "react-dropzone";
|
||||
import {
|
||||
useDropzone,
|
||||
type FileWithPath,
|
||||
type DropzoneOptions,
|
||||
} from "react-dropzone";
|
||||
|
||||
type UploadProps = {
|
||||
import { type RouterOutputs } from "@/trpc/shared";
|
||||
|
||||
type UploadReturn = RouterOutputs["bucket"]["create"];
|
||||
|
||||
type DocumentUploadDropzone = Omit<
|
||||
DropzoneOptions,
|
||||
"noClick" | "noKeyboard" | "onDrop"
|
||||
>;
|
||||
|
||||
type UploaderProps = {
|
||||
header?: React.ReactNode;
|
||||
setOpen?: (val: boolean) => void;
|
||||
uploadType?: "avatar" | "document";
|
||||
};
|
||||
|
||||
function Uploader({ header, setOpen }: UploadProps) {
|
||||
const router = useRouter();
|
||||
// should be companyPublicId or membershipId or userId
|
||||
identifier: string;
|
||||
|
||||
keySuffix: string;
|
||||
|
||||
onSuccess?: (data: UploadReturn) => void | Promise<void>;
|
||||
} & DocumentUploadDropzone;
|
||||
|
||||
export function Uploader({
|
||||
header,
|
||||
identifier,
|
||||
keySuffix,
|
||||
onSuccess,
|
||||
...rest
|
||||
}: UploaderProps) {
|
||||
const { toast } = useToast();
|
||||
const { data: session } = useSession();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const publicId = session?.user.companyPublicId;
|
||||
const uploader = api.document.create.useMutation();
|
||||
const { mutateAsync } = api.bucket.create.useMutation();
|
||||
|
||||
const processFiles = async (acceptedFiles: FileWithPath[]) => {
|
||||
for (const file of acceptedFiles) {
|
||||
const upload = await uploadFile(file, { keyPrefix: publicId });
|
||||
await uploader.mutateAsync(upload);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadFiles = async (acceptedFiles: FileWithPath[]) => {
|
||||
const onDrop = useCallback(async (acceptedFiles: FileWithPath[]) => {
|
||||
try {
|
||||
setUploading(true);
|
||||
await processFiles(acceptedFiles);
|
||||
router.refresh();
|
||||
setOpen && setOpen(false);
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "🎉 Successfully uploaded",
|
||||
description: "Your document(s) has been uploaded.",
|
||||
});
|
||||
for (const file of acceptedFiles) {
|
||||
const upload = await uploadFile(file, { identifier, keySuffix });
|
||||
|
||||
const data = await mutateAsync({ ...upload });
|
||||
|
||||
if (onSuccess) {
|
||||
await onSuccess(data);
|
||||
}
|
||||
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "🎉 Successfully uploaded",
|
||||
description: "Your document(s) has been uploaded.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
toast({
|
||||
@@ -50,16 +72,15 @@ function Uploader({ header, setOpen }: UploadProps) {
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: FileWithPath[]) => {
|
||||
await uploadFiles(acceptedFiles);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
...rest,
|
||||
// Disable click and keydown behavior
|
||||
noClick: true,
|
||||
noKeyboard: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onDrop,
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ export interface getPresignedUrlOptions {
|
||||
contentType: string;
|
||||
expiresIn?: number;
|
||||
fileName: string;
|
||||
keyPrefix?: string;
|
||||
keySuffix: string;
|
||||
// should be companyPublicId or membershipId or userId
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
const TEN_MINUTES_IN_SECONDS = 10 * 60;
|
||||
@@ -45,12 +47,13 @@ export const getPresignedPutUrl = async ({
|
||||
contentType,
|
||||
expiresIn,
|
||||
fileName,
|
||||
keyPrefix,
|
||||
keySuffix,
|
||||
identifier,
|
||||
}: getPresignedUrlOptions) => {
|
||||
const { name, ext } = path.parse(fileName);
|
||||
|
||||
const Key = `${keyPrefix ?? customId(12)}/${slugify(name)}-${customId(
|
||||
6,
|
||||
const Key = `${identifier}/${keySuffix}/${customId(12)}/${slugify(
|
||||
name,
|
||||
)}${ext}`;
|
||||
|
||||
const putObjectCommand = new PutObjectCommand({
|
||||
|
||||
@@ -7,6 +7,8 @@ import { documentRouter } from "../routers/document-router/router";
|
||||
import { waitListRouter } from "../routers/waitlist-router/router";
|
||||
import { onboardingRouter } from "../routers/onboarding-router/router";
|
||||
import { stakeholderRouter } from "../routers/stakeholder-router/router";
|
||||
import { bucketRouter } from "../routers/bucket-router/router";
|
||||
import { templateRouter } from "../routers/template-router/router";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -22,6 +24,8 @@ export const appRouter = createTRPCRouter({
|
||||
shareClass: shareClassRouter,
|
||||
equityPlan: equityPlanRouter,
|
||||
stakeholder: stakeholderRouter,
|
||||
bucket: bucketRouter,
|
||||
template: templateRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { protectedProcedure } from "@/trpc/api/trpc";
|
||||
import { ZodCreateBucketMutationSchema } from "../schema";
|
||||
|
||||
export const createBucketProcedure = protectedProcedure
|
||||
.input(ZodCreateBucketMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.db.bucket.create({ data: input });
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createTRPCRouter } from "@/trpc/api/trpc";
|
||||
import { createBucketProcedure } from "./procedures/create-bucket";
|
||||
|
||||
export const bucketRouter = createTRPCRouter({
|
||||
create: createBucketProcedure,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZodCreateBucketMutationSchema = z.object({
|
||||
name: z.string(),
|
||||
key: z.string(),
|
||||
mimeType: z.string(),
|
||||
size: z.number(),
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { protectedProcedure } from "@/trpc/api/trpc";
|
||||
import { ZodCreateDocumentMutationSchema } from "../schema";
|
||||
import { generatePublicId } from "@/common/id";
|
||||
|
||||
export const createDocumentProcedure = protectedProcedure
|
||||
.input(ZodCreateDocumentMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = ctx.session.user;
|
||||
const publicId = generatePublicId();
|
||||
|
||||
const document = await ctx.db.document.create({
|
||||
data: {
|
||||
companyId: user.companyId,
|
||||
uploaderId: user.membershipId,
|
||||
publicId,
|
||||
...input,
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { protectedProcedure } from "@/trpc/api/trpc";
|
||||
|
||||
export const getAllDocumentsProcedure = protectedProcedure.query(
|
||||
async ({ ctx }) => {
|
||||
const user = ctx.session.user;
|
||||
|
||||
const data = await ctx.db.document.findMany({
|
||||
where: {
|
||||
companyId: user.companyId,
|
||||
},
|
||||
include: {
|
||||
uploader: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bucket: {
|
||||
select: {
|
||||
key: true,
|
||||
mimeType: true,
|
||||
size: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { protectedProcedure } from "@/trpc/api/trpc";
|
||||
import { ZodGetDocumentQuerySchema } from "../schema";
|
||||
import { getPresignedGetUrl } from "@/server/file-uploads";
|
||||
|
||||
export const getDocumentProcedure = protectedProcedure
|
||||
.input(ZodGetDocumentQuerySchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const user = ctx.session.user;
|
||||
|
||||
const data = await ctx.db.document.findFirstOrThrow({
|
||||
where: {
|
||||
publicId: input.publicId,
|
||||
companyId: user.companyId,
|
||||
},
|
||||
select: {
|
||||
bucket: {
|
||||
select: {
|
||||
key: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return getPresignedGetUrl(data.bucket.key);
|
||||
});
|
||||
@@ -1,22 +1,10 @@
|
||||
import { createTRPCRouter, adminOnlyProcedure } from "@/trpc/api/trpc";
|
||||
import { DocumentMutationSchema } from "./schema";
|
||||
import { createTRPCRouter } from "@/trpc/api/trpc";
|
||||
import { createDocumentProcedure } from "./procedures/create-document";
|
||||
import { getDocumentProcedure } from "./procedures/get-document";
|
||||
import { getAllDocumentsProcedure } from "./procedures/get-all-documents";
|
||||
|
||||
export const documentRouter = createTRPCRouter({
|
||||
create: adminOnlyProcedure
|
||||
.input(DocumentMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { db, session } = ctx;
|
||||
const uploadedById = session.user.id;
|
||||
const companyId = session.user.companyId;
|
||||
|
||||
await db.document.create({
|
||||
data: {
|
||||
...input,
|
||||
uploadedById,
|
||||
companyId,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
create: createDocumentProcedure,
|
||||
get: getDocumentProcedure,
|
||||
getAll: getAllDocumentsProcedure,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const DocumentMutationSchema = z.object({
|
||||
key: z.string(),
|
||||
export const ZodCreateDocumentMutationSchema = z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
size: z.number(),
|
||||
bucketId: z.string(),
|
||||
});
|
||||
|
||||
export type DocumentMutationType = z.infer<typeof DocumentMutationSchema>;
|
||||
export type TypeZodCreateDocumentMutationSchema = z.infer<
|
||||
typeof ZodCreateDocumentMutationSchema
|
||||
>;
|
||||
|
||||
export const ZodGetDocumentQuerySchema = z.object({
|
||||
publicId: z.string(),
|
||||
});
|
||||
|
||||
export type TypeZodGetDocumentQuerySchema = z.infer<
|
||||
typeof ZodGetDocumentQuerySchema
|
||||
>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { protectedProcedure } from "@/trpc/api/trpc";
|
||||
import { ZodCreateTemplateMutationSchema } from "../schema";
|
||||
import { generatePublicId } from "@/common/id";
|
||||
|
||||
export const createTemplateProcedure = protectedProcedure
|
||||
.input(ZodCreateTemplateMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const publicId = generatePublicId();
|
||||
const user = ctx.session.user;
|
||||
|
||||
return ctx.db.template.create({
|
||||
data: {
|
||||
status: "draft",
|
||||
publicId,
|
||||
companyId: user.companyId,
|
||||
uploaderId: user.id,
|
||||
...input,
|
||||
},
|
||||
select: {
|
||||
publicId: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { protectedProcedure } from "@/trpc/api/trpc";
|
||||
import { ZodGetTemplateQuerySchema } from "../schema";
|
||||
import { getPresignedGetUrl } from "@/server/file-uploads";
|
||||
|
||||
export const getTemplateProcedure = protectedProcedure
|
||||
.input(ZodGetTemplateQuerySchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const user = ctx.session.user;
|
||||
|
||||
const data = await ctx.db.template.findFirstOrThrow({
|
||||
where: {
|
||||
publicId: input.publicId,
|
||||
companyId: user.companyId,
|
||||
},
|
||||
select: {
|
||||
bucket: {
|
||||
select: {
|
||||
key: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return getPresignedGetUrl(data.bucket.key);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createTRPCRouter } from "@/trpc/api/trpc";
|
||||
import { createTemplateProcedure } from "./procedures/create-template";
|
||||
import { getTemplateProcedure } from "./procedures/get-template";
|
||||
|
||||
export const templateRouter = createTRPCRouter({
|
||||
create: createTemplateProcedure,
|
||||
get: getTemplateProcedure,
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZodCreateTemplateMutationSchema = z.object({
|
||||
name: z.string(),
|
||||
bucketId: z.string(),
|
||||
});
|
||||
|
||||
export const ZodGetTemplateQuerySchema = z.object({
|
||||
publicId: z.string(),
|
||||
});
|
||||
Reference in New Issue
Block a user