diff --git a/app/(static)/investors/layout.tsx b/app/(static)/investors/layout.tsx index 6a19625a5..f40ac3b08 100644 --- a/app/(static)/investors/layout.tsx +++ b/app/(static)/investors/layout.tsx @@ -3,7 +3,7 @@ import Script from "next/script"; const data = { description: - "Open investor and vc funds list of over 10,000 investors based on stage, sector, or location. Powered by Papermark.", + "The largest investors database. This list of investors includes ten thousand of different venture funds based on stage, sector and location.", title: "Investor Search | Papermark", url: "/investors", }; diff --git a/app/(static)/investors/page.tsx b/app/(static)/investors/page.tsx index 96ccfc7f4..71128559f 100644 --- a/app/(static)/investors/page.tsx +++ b/app/(static)/investors/page.tsx @@ -12,11 +12,11 @@ export default async function HomePage() {

- Open Investor Database + Investors Database

- Find investors based on stage, sector, or location.
- Powered by Papermark. + List of investors where you can search by stage, sector, or + location.

diff --git a/app/(static)/open-source-investors/page.tsx b/app/(static)/open-source-investors/page.tsx index 33b57ed8f..21b148032 100644 --- a/app/(static)/open-source-investors/page.tsx +++ b/app/(static)/open-source-investors/page.tsx @@ -10,7 +10,7 @@ export const revalidate = 3600; // revalidate the data at most every 24 hours const data = { description: - "List of 100 open-source investors. Open-source VC, open-source angel investors. Share pitchdecks with your investors using Papermark, an open-source document infrastructure.", + "List of 100 open-source investors. Open-source VC, open-source angel investors, created and powered by Papermark", title: "Open Source Investors | Papermark", url: "/open-source-investors", }; @@ -43,12 +43,11 @@ export const metadata: Metadata = { }, }; - export type Investor = { id: string; createdTime: string; fields: Fields; -} +}; export type Fields = { name: string; @@ -60,7 +59,7 @@ export type Fields = { twitterImageUrl: string; openSourceInvestments: string; checkSize: "Unknown" | "$5k - $50k" | "$50k+" | "$100k+" | "$250k+"; -} +}; const getInvestors = async () => { const response = await fetch( @@ -79,14 +78,14 @@ const getInvestors = async () => { }; const checkSizes = [ - { id: "7", label: "All" }, - { id: "1", label: "$5k - $50k" }, - { id: "2", label: "$50k+" }, - { id: "3", label: "$100k+" }, - { id: "4", label: "$250k+" }, - ]; + { id: "7", label: "All" }, + { id: "1", label: "$5k - $50k" }, + { id: "2", label: "$50k+" }, + { id: "3", label: "$100k+" }, + { id: "4", label: "$250k+" }, +]; -const InvestorFallback = ({ allInvestors } : { allInvestors: Investor[] }) => { +const InvestorFallback = ({ allInvestors }: { allInvestors: Investor[] }) => { const category = "7"; return ( <> @@ -102,8 +101,7 @@ const InvestorFallback = ({ allInvestors } : { allInvestors: Investor[] }) => { } key={checkSize.id} className={cn( - category === checkSize.id || - (!category && checkSize.id === "7") + category === checkSize.id || (!category && checkSize.id === "7") ? "bg-gray-200" : "bg-white hover:bg-gray-50", "relative inline-flex items-center first-of-type:rounded-l-md last-of-type:rounded-r-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 focus:z-10 focus:outline-none focus:ring-gray-500 -ml-px first-of-type:-ml-0", @@ -125,7 +123,7 @@ const InvestorFallback = ({ allInvestors } : { allInvestors: Investor[] }) => { ); -} +}; export default async function HomePage() { const data = await getInvestors(); diff --git a/app/page.tsx b/app/page.tsx index ed61fc4ac..4830ec12e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -83,9 +83,7 @@ export default function Home() { - +
diff --git a/components/document-upload.tsx b/components/document-upload.tsx index c67faf2db..d628dde2f 100644 --- a/components/document-upload.tsx +++ b/components/document-upload.tsx @@ -10,6 +10,8 @@ import { Presentation as PresentationChartBarIcon, Image as PhotoIcon, } from "lucide-react"; +import { useTeam } from "@/context/team-context"; +import { usePlan } from "@/lib/swr/use-billing"; function fileIcon(fileType: string) { switch (fileType) { @@ -37,12 +39,14 @@ export default function DocumentUpload({ currentFile: File | null; setCurrentFile: React.Dispatch>; }) { + const { plan, loading } = usePlan(); + const maxSize = plan?.plan === "pro" ? 100 : 30; const { getRootProps, getInputProps } = useDropzone({ accept: { "application/pdf": [], // ".pdf" }, multiple: false, - maxSize: 30 * 1024 * 1024, // 30 MB + maxSize: maxSize * 1024 * 1024, // 30 MB onDropAccepted: (acceptedFiles) => { setCurrentFile(acceptedFiles[0]); }, @@ -50,7 +54,7 @@ export default function DocumentUpload({ const { errors } = fileRejections[0]; let message; if (errors[0].code === "file-too-large") { - message = "File size too big (max. 30 MB)"; + message = `File size too big (max. ${maxSize} MB)`; } else if (errors[0].code === "file-invalid-type") { message = "File type not supported (.pdf only)"; } else { @@ -103,7 +107,9 @@ export default function DocumentUpload({

- {currentFile ? "Replace file?" : "Only *.pdf & 30 MB limit"} + {currentFile + ? "Replace file?" + : `Only *.pdf & ${maxSize} MB limit`}

diff --git a/components/links/link-sheet/domain-section.tsx b/components/links/link-sheet/domain-section.tsx index 4fdeb00e4..7af41ecf3 100644 --- a/components/links/link-sheet/domain-section.tsx +++ b/components/links/link-sheet/domain-section.tsx @@ -74,10 +74,10 @@ export default function DomainSection({ name="key" required value={data.slug || ""} - pattern="[\p{L}\p{N}\p{Pd}\/]+" + pattern="[\p{L}\p{N}\p{Pd}]+" onInvalid={(e) => { e.currentTarget.setCustomValidity( - "Only letters, numbers, '-', and '/' are allowed.", + "Only letters, numbers, and '-' are allowed.", ); }} autoComplete="off" diff --git a/lib/constants.ts b/lib/constants.ts index 206635826..b90e2165d 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -35,3 +35,11 @@ export const REACTIONS = [ label: "down", }, ]; + +// growing list of blocked pathnames that lead to 404s +export const BLOCKED_PATHNAMES = [ + "/phpmyadmin", + "/server-status", + "/wordpress", + "/_all_dbs", +]; diff --git a/lib/middleware/domain.ts b/lib/middleware/domain.ts index 4f77db576..520851b89 100644 --- a/lib/middleware/domain.ts +++ b/lib/middleware/domain.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getToken } from "next-auth/jwt"; -import { PAPERMARK_HEADERS } from "../constants"; +import { BLOCKED_PATHNAMES, PAPERMARK_HEADERS } from "@/lib/constants"; export default async function DomainMiddleware(req: NextRequest) { const path = req.nextUrl.pathname; @@ -11,6 +10,10 @@ export default async function DomainMiddleware(req: NextRequest) { // if there's a path and it's not "/" then we need to check if it's a custom domain if (path !== "/") { + if (BLOCKED_PATHNAMES.includes(path) || path.includes(".")) { + url.pathname = "/404"; + return NextResponse.rewrite(url); + } // Subdomain available, rewriting // >>> Rewriting: ${path} to /view/domains/${host}${path}` url.pathname = `/view/domains/${host}${path}`; diff --git a/pages/alternatives/docsend.tsx b/pages/alternatives/docsend.tsx index 339183f59..f36c79349 100644 --- a/pages/alternatives/docsend.tsx +++ b/pages/alternatives/docsend.tsx @@ -1,6 +1,9 @@ import Head from "next/head"; import Footer from "@/components/web/footer"; import { Disclosure } from "@headlessui/react"; +import { CheckIcon } from "lucide-react"; +import { XIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; import { Plus as PlusSmallIcon, Minus as MinusSmallIcon, @@ -13,6 +16,18 @@ import { } from "lucide-react"; import Link from "next/link"; import { classNames } from "@/lib/utils"; +import { LogoCloud } from "@/components/web/landing-page/logo-cloud"; +import { Button } from "@/components/ui/button"; +import Navbar from "@/components/web/navbar"; + +const frequencies: { + value: "monthly" | "annually"; + label: "Monthly" | "Annually"; + priceSuffix: "/month" | "/year"; +}[] = [ + { value: "monthly", label: "Monthly", priceSuffix: "/month" }, + { value: "annually", label: "Annually", priceSuffix: "/year" }, +]; const features = [ { @@ -52,38 +67,97 @@ const features = [ }, ]; -const tiers = [ +// const tiers = [ +// { +// // name: "Hobby", +// id: "tier-hobby", +// href: "#", +// priceMonthly: "DocSend", +// description: "Free version is not available", +// features: [ +// "❌ Custom domain", +// "❌ Pitchdeck feedback", +// "❌ Hosting", +// "❌ AI-recommendations", +// "❌ Team access", +// "❌ Hosting", +// ], +// featured: false, +// }, +// { +// // name: "Enterprise", +// id: "tier-enterprise", +// href: "#", +// priceMonthly: "Papermark", +// description: "Free plan ", +// features: [ +// "✅ Open Source", +// "✅ Custom domain", +// "✅ Advanced tracking system", +// "✅ Work as a team", +// "✅ Host by yourself", +// "✅ Pitchdeck analytics", +// ], +// featured: true, +// }, +// ]; + +const tiers: { + name: string; + id: string; + href: string; + price: { + monthly: string; + annually: string; + }; + description: string; + features: string[]; + bgColor: string; + borderColor: string; + textColor: string; + buttonText: string; + mostPopular: boolean; +}[] = [ { - // name: "Hobby", - id: "tier-hobby", - href: "#", - priceMonthly: "DocSend", - description: "Free version is not available", + name: "Papermark", + id: "tier-free", + href: "/login", + price: { monthly: "$0", annually: "$144" }, + description: "Papermark plans start from freemium", features: [ - "❌ Custom domain", - "❌ Pitchdeck feedback", - "❌ Hosting", - "❌ AI-recommendations", - "❌ Team access", - "❌ Hosting", + "Open Source", + "Custom domain", + "Advanced tracking system", + "Work as a team", + "Host by yourself", + "Pitchdeck analytics", ], - featured: false, + + bgColor: "#fb7a00", + borderColor: "#fb7a00", + textColor: "#black", + buttonText: "Start for free", + mostPopular: false, }, { - // name: "Enterprise", - id: "tier-enterprise", - href: "#", - priceMonthly: "Papermark", - description: "Free plan ", + name: "Docsend", + id: "tier-freelancer", + href: "/login", + price: { monthly: "$15", annually: "$288" }, + description: "DocSend has no free plan available", features: [ - "✅ Open Source", - "✅ Custom domain", - "✅ Advanced tracking system", - "✅ Work as a team", - "✅ Host by yourself", - "✅ Pitchdeck analytics", + "Custom domain", + "Pitchdeck feedback", + "Hosting", + "AI-recommendations", + "Team access", + "Hosting", ], - featured: true, + bgColor: "bg-gray-100", + borderColor: "#bg-gray-800", + textColor: "#bg-gray-800", + buttonText: "Start with DocSend alternative", + mostPopular: false, }, ]; @@ -127,337 +201,357 @@ const faqs = [ // More questions... ]; -export default function Home() { +export default function DocsendPage() { + const frequency = frequencies[0]; return ( -
- - Papermark: Best Free & Open-Source Alternative to DocSend - - - - - - - + <> +
+ + + Papermark: Best Free & Open-Source Alternative to DocSend + + + + + + + + -
{/* Hero section */} -
-
-
+
+
+
+ ); } diff --git a/pages/api/file/browser-upload.ts b/pages/api/file/browser-upload.ts index 3a7101396..3d9bc5645 100644 --- a/pages/api/file/browser-upload.ts +++ b/pages/api/file/browser-upload.ts @@ -3,6 +3,7 @@ import type { NextApiResponse, NextApiRequest } from "next"; import { getServerSession } from "next-auth/next"; import { authOptions } from "../auth/[...nextauth]"; import { CustomUser } from "@/lib/types"; +import prisma from "@/lib/prisma"; export default async function handler( req: NextApiRequest, @@ -23,9 +24,28 @@ export default async function handler( throw new Error("Unauthorized"); } + const userId = (session.user as CustomUser).id; + const team = await prisma.team.findFirst({ + where: { + users: { + some: { + userId, + }, + }, + }, + select: { + plan: true, + }, + }); + + let maxSize = 30 * 1024 * 1024; // 30 MB + if (team?.plan === "pro") { + maxSize = 100 * 1024 * 1024; // 100 MB + } + return { allowedContentTypes: ["application/pdf"], - maximumSizeInBytes: 30 * 1024 * 1024, // 30 MB + maximumSizeInBytes: maxSize, // 30 MB metadata: JSON.stringify({ // optional, sent to your server on upload completion userId: (session.user as CustomUser).id, diff --git a/pages/pricing.tsx b/pages/pricing.tsx index 7a0f50987..9ac44d665 100644 --- a/pages/pricing.tsx +++ b/pages/pricing.tsx @@ -1,163 +1,271 @@ -import Footer from "@/components/web/footer"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { CheckIcon } from "lucide-react"; import Navbar from "@/components/web/navbar"; +import Footer from "@/components/web/footer"; import Link from "next/link"; -import Head from "next/head"; +import GitHubIcon from "@/components/shared/icons/github"; +import { usePlausible } from "next-plausible"; -export default function Pricing() { - const tiers = [ - { - id: 1, - title: "Free", - priceMonthly: "€0/mo", - description: "What's included:", - features: [ - "Unlimited links", - "30 MB document size limit", - "Notion documents", - "1 user", - "Basic support", - "Email notifications", - "Basic Papermark AI", - "100 credits, 3/day", - ], - }, - { - id: 2, - title: "Pro", - priceMonthly: "€29/mo", - description: "Everything in Free, plus:", - features: [ - "Unlimited documents", - "Large file uploads", - "Team members", - "Priority support", - "Custom domains", - "Custom branding", - "Advanced Papermark AI", - "1500 credits", - ], - }, - { - id: 3, - title: "Enterprise", - priceMonthly: "Get in touch", - description: "Custom tailored plans, incl.:", - features: [ - "Up to 5TB file uploads", - "Dedicated support", - "Custom Papermark AI / BYO", - "Unlimited credits", - ], - }, - ]; +const frequencies: { + value: "monthly" | "annually"; + label: "Monthly" | "Annually"; + priceSuffix: "/month" | "/year"; +}[] = [ + { value: "monthly", label: "Monthly", priceSuffix: "/month" }, + { value: "annually", label: "Annually", priceSuffix: "/year" }, +]; +const tiers: { + name: string; + id: string; + href: string; + price: { + monthly: string; + annually: string; + }; + description: string; + features: string[]; + bgColor: string; + borderColor: string; + textColor: string; + buttonText: string; + mostPopular: boolean; +}[] = [ + { + name: "Free", + id: "tier-free", + href: "/login", + price: { monthly: "€0", annually: "$0" }, + description: "The essentials to start sharing documents securely.", + features: [ + "1 user", + "Unlimited links", + "Analytics for each page", + "Document sharing controls", + ], - return ( - <> - - Papermark | Pricing - - + bgColor: "bg-gray-200", + borderColor: "#bg-gray-800", + textColor: "#bg-gray-800", + buttonText: "Start for free", + mostPopular: false, + }, + { + name: "Pro", + id: "tier-freelancer", + href: "/login", + price: { monthly: "€29", annually: "$290" }, + description: + "The essentials to provide a branded experience for your documents.", + features: [ + "Everything in Free, plus:", + "Up to 3 active users", + "Custom domain", + "Advanced access controls", + "Papermark AI", + ], + bgColor: "bg-gray-200", + borderColor: "#bg-gray-800", + textColor: "#bg-gray-800", + buttonText: "Choose Pro", + mostPopular: false, + }, + { + name: "Business", + id: "tier-startup", + href: "/login", + price: { monthly: "€79", annually: "$790" }, + description: "A plan that scales with your rapidly growing business.", + features: [ + "Everything in Pro, plus:", + "Up to 10 active users", + "Data room", + "Large file uploads", + "Custom Branding", + "24h Priority Support", + ], + bgColor: "#fb7a00", + borderColor: "#fb7a00", + textColor: "#black", + buttonText: "Choose Business", + mostPopular: true, + }, + { + name: "Enterprise", + id: "tier-enterprise", + href: "https://cal.com/marcseitz/papermark", + price: { monthly: "Custom", annually: "$" }, + description: "Self-hosted and advanced infrastructure for your company.", + features: [ + "Self-Hosted version", + "Unlimited users", + "Unlimited documents", + "Different file types", + "Up to 5TB file uploads", + "Dedicated support", + "Custom Papermark AI", + ], + bgColor: "bg-gray-200", + borderColor: "#bg-gray-800", + textColor: "#bg-gray-800", + buttonText: "Book a demo", + mostPopular: false, + }, +]; -
-
-
-

- Pricing -

-
+export default function PricingPage() { + const plausible = usePlausible(); + const frequency = frequencies[0]; -

- Share your Pitch Deck, Sales Deck and other documents and monitor - results on any suitable for you plan. You always can start - - {" "} - open source{" "} - - or - - {" "} - contact us{" "} - - for custom requests, like self hosting, customization and AI - document comparison. -

+ plausible; -
- {tiers.map((tier) => ( -
+
+ +
+
+

+ Find the plan that +
+ works for you +

+ {/*

+ Papermark is an open-source document sharing infrastructure with + built-in page analytics and custom domains. +

*/} + {/*
+ -

{tier.title}

-
- {tier.priceMonthly} -
-
{tier.description}
-
- {tier.features.map((feature) => ( -
- + Help with pricing + + + + + +
*/} +
+
+
+
+
+ {tiers.map((tier) => ( +
+
+
+

- - - {feature} + {tier.name} +

- ))} -
-
- {tier.id === 1 && ( - - Start for free - - )} - {tier.id === 2 && ( - - Start for free - - )} - {tier.id === 3 && ( +
+

+ {tier.description} +

+

+ + {tier.price[frequency.value]} + + + {frequency.priceSuffix} + +

+
    + {tier.features.map((feature) => ( +
  • +
  • + ))} +
+
+
+
{ + plausible("clickedPricing", { + props: { tier: tier.name }, + }); + }} > - Contact us + - )} +
+ ))} +
+
+
+ +
+
+
+

Looking to self-host?

+
+ + + + + +
- ))} +
+
- -