Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions components/datarooms/template-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState } from "react";

import { DataroomTemplate } from "@/lib/swr/use-dataroom-templates";
import { DEFAULT_BANNER_IMAGE } from "@/lib/utils";

import { UseTemplateModal } from "@/components/datarooms/use-template-modal";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export function TemplateCard({
template,
onTemplateCreated,
}: {
template: DataroomTemplate;
onTemplateCreated?: (dataroomId: string) => void;
}) {
const [useTemplateModalOpen, setUseTemplateModalOpen] = useState(false);

return (
<Card className="group relative overflow-hidden duration-500 hover:border-primary/50">
<div className="aspect-[3/1.3] w-full">
<div
className="h-full w-full rounded-t bg-slate-100 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${template.brand?.banner || DEFAULT_BANNER_IMAGE})`,
}}
/>
</div>
<CardHeader className="pb-3">
<div className="flex min-w-0 flex-1 items-start justify-between">
<CardTitle className="line-clamp-2 text-lg leading-tight">
{template.name}
</CardTitle>
</div>
</CardHeader>
<CardContent>
<UseTemplateModal
open={useTemplateModalOpen}
setOpen={setUseTemplateModalOpen}
template={template}
onTemplateCreated={onTemplateCreated}
>
<Button className="w-full cursor-pointer" variant="outline">
Use Template
</Button>
</UseTemplateModal>
</CardContent>
</Card>
);
}
232 changes: 232 additions & 0 deletions components/datarooms/use-template-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { useRouter } from "next/router";

import { useState } from "react";

import { useTeam } from "@/context/team-context";
import {
ArrowLeftIcon,
CopyIcon,
FileTextIcon,
FolderIcon,
} from "lucide-react";
import { toast } from "sonner";
import { mutate } from "swr";

import useDataroomTemplates, {
DataroomTemplate,
} from "@/lib/swr/use-dataroom-templates";

import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";

export function UseTemplateModal({
children,
template,
open,
setOpen,
onTemplateCreated,
}: {
children?: React.ReactNode;
template?: DataroomTemplate;
open?: boolean;
setOpen?: (open: boolean) => void;
onTemplateCreated?: (dataroomId: string) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [selectedTemplate, setSelectedTemplate] =
useState<DataroomTemplate | null>(template || null);
const [dataroomName, setDataroomName] = useState("");

const { templates, loading: templatesLoading } = useDataroomTemplates();
const router = useRouter();
const teamInfo = useTeam();

const handleUseTemplate = async () => {
if (!selectedTemplate || !teamInfo?.currentTeam?.id) return;

setIsLoading(true);

try {
const response = await fetch(
`/api/teams/${teamInfo.currentTeam.id}/datarooms/duplicate-template`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: selectedTemplate.id,
name: dataroomName || `${selectedTemplate.name}`,
}),
},
);

if (!response.ok) {
const errorData = await response.json();
if (errorData.info === "trial_limit_reached") {
window.location.replace("/datarooms");
}
throw new Error(
errorData.message || "Failed to create dataroom from template",
);
}

const newDataroom = await response.json();
mutate(`/api/teams/${teamInfo.currentTeam.id}/datarooms`);

const modalOpen = open !== undefined ? open : isOpen;
if (open !== undefined && setOpen) {
setOpen(false);
} else {
setIsOpen(false);
}

toast.success("Dataroom created successfully from template!");

if (onTemplateCreated) {
onTemplateCreated(newDataroom.id);
} else {
router.push(`/datarooms/${newDataroom.id}/documents`);
}
} catch (error) {
console.error("Error creating dataroom from template:", error);
toast.error(
error instanceof Error
? error.message
: "Failed to create dataroom from template",
);
} finally {
setIsLoading(false);
}
};

const handleBackToSelection = () => {
setSelectedTemplate(null);
setDataroomName("");
};

const modalOpen = open !== undefined ? open : isOpen;
const handleOpenChange = open !== undefined && setOpen ? setOpen : setIsOpen;

return (
<Dialog open={modalOpen} onOpenChange={handleOpenChange}>
{children && <DialogTrigger asChild>{children}</DialogTrigger>}
<DialogContent className="max-h-[85vh] max-w-2xl">
<DialogHeader>
{selectedTemplate ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleBackToSelection}
className="h-8 min-w-8 p-0"
>
<ArrowLeftIcon className="h-4 w-4" />
</Button>
<DialogTitle className="line-clamp-1">{`Use ${selectedTemplate.name}`}</DialogTitle>
</div>
) : (
<DialogTitle>Use Template</DialogTitle>
)}
<DialogDescription>
{selectedTemplate
? "Configure your new dataroom settings"
: "Choose a template to quickly create your dataroom with pre-built structure and documents."}
</DialogDescription>
</DialogHeader>

<div className="space-y-6">
{!selectedTemplate && (
<div>
<Label className="text-sm font-medium">Select Template</Label>
<ScrollArea className="mt-2 max-h-80 w-full rounded-md border p-4">
{templatesLoading ? (
<div className="flex h-32 items-center justify-center">
<div className="text-sm text-muted-foreground">
Loading templates...
</div>
</div>
) : (
<div className="grid gap-3">
{templates.map((tmpl) => (
<Card
key={tmpl.id}
className="cursor-pointer transition-all duration-200 hover:border-primary/50 hover:bg-muted/50"
onClick={() => setSelectedTemplate(tmpl)}
>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium">{tmpl.name}</h4>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</ScrollArea>
</div>
)}
{selectedTemplate && (
<>
<Card className="border-primary/50">
<CardHeader className="p-4">
<CardTitle className="line-clamp-2 text-lg">
{selectedTemplate.name}
</CardTitle>
</CardHeader>
</Card>
<div className="space-y-2">
<Label htmlFor="dataroom-name">Dataroom Name</Label>
<Input
id="dataroom-name"
placeholder={`${selectedTemplate.name}`}
value={dataroomName}
onChange={(e) => setDataroomName(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Leave empty to use default name: &quot;{selectedTemplate.name}
&quot;
</p>
</div>
</>
)}
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
{selectedTemplate && (
<Button
onClick={handleUseTemplate}
disabled={isLoading}
loading={isLoading}
className="flex items-center gap-2"
>
Create Dataroom
</Button>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}
4 changes: 1 addition & 3 deletions components/view/dataroom/nav-dataroom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DataroomBrand } from "@prisma/client";
import { BadgeInfoIcon, Download, MessagesSquareIcon } from "lucide-react";
import { toast } from "sonner";

import { formatDate } from "@/lib/utils";
import { DEFAULT_BANNER_IMAGE, formatDate } from "@/lib/utils";

import {
ButtonTooltip,
Expand All @@ -19,8 +19,6 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { Button } from "../../ui/button";
import { ConversationSidebar } from "../conversations/sidebar";

const DEFAULT_BANNER_IMAGE = "/_static/papermark-banner.png";

export default function DataroomNav({
allowDownload,
brand,
Expand Down
88 changes: 88 additions & 0 deletions components/welcome/browse-templates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useRouter } from "next/router";

import { motion } from "motion/react";

import { STAGGER_CHILD_VARIANTS } from "@/lib/constants";
import useDataroomTemplates from "@/lib/swr/use-dataroom-templates";

import { TemplateCard } from "@/components/datarooms/template-card";
import { Skeleton } from "@/components/ui/skeleton";

export default function BrowseTemplates() {
const { templates, loading: templatesLoading } = useDataroomTemplates();
const router = useRouter();

const handleTemplateCreated = (dataroomId: string) => {
router.push(`/welcome?type=dataroom-created&dataroomId=${dataroomId}`);
};

return (
<motion.div
className="z-10 mx-5 flex w-full flex-col items-center space-y-10 text-center sm:mx-auto"
variants={{
hidden: { opacity: 0, scale: 0.95 },
show: {
opacity: 1,
scale: 1,
transition: {
staggerChildren: 0.2,
},
},
}}
initial="hidden"
animate="show"
exit="hidden"
transition={{ duration: 0.3, type: "spring" }}
>
<motion.div
variants={STAGGER_CHILD_VARIANTS}
className="flex w-full flex-col items-center space-y-10 text-center"
>
<p className="text-2xl font-bold tracking-tighter text-foreground">
Papermark
</p>
<h1 className="font-display max-w-lg text-3xl font-semibold transition-colors sm:text-4xl">
Choose a Template
</h1>
<p className="max-w-md text-muted-foreground">
Select one of our professionally designed templates to get a head
start on your dataroom.
</p>
</motion.div>

<motion.div
variants={STAGGER_CHILD_VARIANTS}
className="w-full max-w-4xl"
>
{templatesLoading ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border border-border bg-card">
<div className="flex flex-col space-y-1.5 p-6">
<Skeleton className="aspect-[3/1] w-full rounded-md" />
<Skeleton className="h-6 w-3/4 pt-2" />
</div>
<div className="p-6 pt-0">
<Skeleton className="h-10 w-full" />
</div>
</div>
))}
</div>
) : (
templates &&
templates.length > 0 && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{templates.map((template) => (
<TemplateCard
key={template.id}
template={template}
onTemplateCreated={handleTemplateCreated}
/>
))}
</div>
)
)}
</motion.div>
</motion.div>
);
}
Loading