├── .cursor
└── rules
│ ├── rule-claude-sonnet-37.mdc
│ └── rule-trigger-typescript.mdc
├── .cursorignore
├── .env.example
├── .eslintrc.json
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ └── oss-gg-hack-template.yml
├── images
│ ├── papermark-logo.svg
│ └── papermark-welcome.gif
└── workflows
│ └── cla.yml
├── .gitignore
├── .prettierignore
├── .vercelignore
├── CLA.md
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── app
├── (auth)
│ ├── auth
│ │ └── confirm-email-change
│ │ │ └── [token]
│ │ │ ├── page-client.tsx
│ │ │ ├── page.tsx
│ │ │ └── utils.ts
│ ├── layout.tsx
│ ├── login
│ │ ├── page-client.tsx
│ │ └── page.tsx
│ ├── register
│ │ ├── page-client.tsx
│ │ └── page.tsx
│ └── verify
│ │ ├── invitation
│ │ ├── AcceptInvitationButton.tsx
│ │ ├── InvitationStatusContent.tsx
│ │ ├── page.tsx
│ │ └── status
│ │ │ └── ClientRedirect.tsx
│ │ └── page.tsx
├── api
│ ├── cron
│ │ ├── domains
│ │ │ ├── route.ts
│ │ │ └── utils.ts
│ │ ├── upgrade
│ │ │ └── route.ts
│ │ └── year-in-review
│ │ │ └── route.ts
│ ├── csp-report
│ │ └── route.ts
│ ├── feature-flags
│ │ └── route.ts
│ ├── help
│ │ └── route.ts
│ ├── links
│ │ └── [id]
│ │ │ └── upload
│ │ │ └── route.ts
│ ├── og
│ │ ├── route.tsx
│ │ └── yir
│ │ │ └── route.tsx
│ ├── views-dataroom
│ │ └── route.ts
│ ├── views
│ │ └── route.ts
│ └── webhooks
│ │ └── callback
│ │ └── route.ts
├── layout.tsx
└── robots.txt
├── components.json
├── components
├── EmailForm.tsx
├── Skeleton.tsx
├── account
│ ├── account-header.tsx
│ ├── update-subscription.tsx
│ └── upload-avatar.tsx
├── agreements
│ └── agreement-card.tsx
├── analytics
│ ├── analytics-card.tsx
│ ├── dashboard-views-chart.tsx
│ ├── documents-table.tsx
│ ├── links-table.tsx
│ ├── time-range-select.tsx
│ ├── views-table.tsx
│ └── visitors-table.tsx
├── billing
│ ├── add-seat-modal.tsx
│ ├── plan-badge.tsx
│ ├── pro-annual-banner.tsx
│ ├── pro-banner.tsx
│ ├── upgrade-plan-container.tsx
│ ├── upgrade-plan-modal-old.tsx
│ └── upgrade-plan-modal.tsx
├── blur-image.tsx
├── charts
│ ├── bar-chart-tooltip.tsx
│ ├── bar-chart.tsx
│ └── utils.ts
├── chat
│ ├── chat-input.tsx
│ ├── chat-list.tsx
│ ├── chat-message-actions.tsx
│ ├── chat-message.tsx
│ ├── chat-scroll-anchor.tsx
│ ├── chat.tsx
│ └── empty-screen.tsx
├── conversations
│ └── index.tsx
├── datarooms
│ ├── actions
│ │ ├── download-dataroom.tsx
│ │ ├── generate-index-button.tsx
│ │ ├── generate-index-dialog.tsx
│ │ └── remove-document-modal.tsx
│ ├── add-dataroom-modal.tsx
│ ├── add-viewer-modal.tsx
│ ├── analytics
│ │ ├── analytics-overview.tsx
│ │ ├── document-analytics-tree.tsx
│ │ └── mock-analytics-table.tsx
│ ├── dataroom-breadcrumb.tsx
│ ├── dataroom-document-card.tsx
│ ├── dataroom-header.tsx
│ ├── dataroom-items-list.tsx
│ ├── dataroom-navigation.tsx
│ ├── dataroom-trial-modal.tsx
│ ├── empty-dataroom.tsx
│ ├── folders
│ │ ├── index.tsx
│ │ ├── selection-tree.tsx
│ │ ├── sidebar-tree.tsx
│ │ ├── utils.ts
│ │ └── view-tree.tsx
│ ├── groups
│ │ ├── add-group-modal.tsx
│ │ ├── add-member-modal.tsx
│ │ ├── delete-group
│ │ │ ├── delete-group-modal.tsx
│ │ │ └── index.tsx
│ │ ├── group-card-placeholder.tsx
│ │ ├── group-card.tsx
│ │ ├── group-header.tsx
│ │ ├── group-member-table.tsx
│ │ ├── group-navigation.tsx
│ │ ├── group-permissions.tsx
│ │ └── set-group-permissions-modal.tsx
│ ├── move-dataroom-folder-modal.tsx
│ ├── settings
│ │ ├── delete-dataroooom
│ │ │ ├── delete-dataroom-modal.tsx
│ │ │ └── index.tsx
│ │ ├── duplicate-dataroom.tsx
│ │ ├── notification-settings.tsx
│ │ └── settings-tabs.tsx
│ ├── sortable
│ │ ├── sortable-item.tsx
│ │ └── sortable-list.tsx
│ └── stats-card.tsx
├── document-upload.tsx
├── documents
│ ├── actions
│ │ ├── delete-documents-modal.tsx
│ │ └── delete-folder-modal.tsx
│ ├── add-document-modal.tsx
│ ├── add-document-to-dataroom-modal.tsx
│ ├── add-folder-to-dataroom-modal.tsx
│ ├── alert.tsx
│ ├── breadcrumb.tsx
│ ├── delete-folder-modal.tsx
│ ├── document-card.tsx
│ ├── document-header.tsx
│ ├── documents-list.tsx
│ ├── drag-and-drop
│ │ ├── draggable-item.tsx
│ │ └── droppable-folder.tsx
│ ├── empty-document.tsx
│ ├── file-process-status-bar.tsx
│ ├── filters
│ │ └── sort-button.tsx
│ ├── folder-card.tsx
│ ├── loading-document.tsx
│ ├── move-folder-modal.tsx
│ ├── pagination.tsx
│ ├── stats-card.tsx
│ ├── stats-chart-dummy.tsx
│ ├── stats-chart-skeleton.tsx
│ ├── stats-chart.tsx
│ ├── stats-element.tsx
│ ├── stats.tsx
│ ├── video-analytics.tsx
│ └── video-chart-placeholder.tsx
├── domains
│ ├── add-domain-modal.tsx
│ ├── delete-domain-modal.tsx
│ ├── domain-card.tsx
│ ├── domain-configuration.tsx
│ └── use-domain-status.ts
├── emails
│ ├── dataroom-notification.tsx
│ ├── dataroom-trial-end.tsx
│ ├── dataroom-trial-welcome.tsx
│ ├── dataroom-viewer-invitation.tsx
│ ├── deleted-domain.tsx
│ ├── email-updated.tsx
│ ├── email-verification.tsx
│ ├── invalid-domain.tsx
│ ├── onboarding-1.tsx
│ ├── onboarding-2.tsx
│ ├── onboarding-3.tsx
│ ├── onboarding-4.tsx
│ ├── onboarding-5.tsx
│ ├── otp-verification.tsx
│ ├── team-invitation.tsx
│ ├── trial-end-final-reminder.tsx
│ ├── trial-end-reminder.tsx
│ ├── upgrade-plan.tsx
│ ├── verification-email-change.tsx
│ ├── verification-link.tsx
│ ├── viewed-dataroom.tsx
│ ├── viewed-document.tsx
│ ├── welcome.tsx
│ └── year-in-review-papermark.tsx
├── folders
│ ├── add-folder-modal.tsx
│ └── edit-folder-modal.tsx
├── hooks
│ ├── use-optimistic-update.ts
│ └── useLastUsed.tsx
├── layouts
│ ├── app.tsx
│ ├── blocking-modal.tsx
│ ├── breadcrumb.tsx
│ └── trial-banner.tsx
├── links
│ ├── embed-code-modal.tsx
│ ├── link-active-controls.tsx
│ ├── link-sheet
│ │ ├── agreement-panel
│ │ │ └── index.tsx
│ │ ├── agreement-section.tsx
│ │ ├── allow-download-section.tsx
│ │ ├── allow-list-section.tsx
│ │ ├── allow-notification-section.tsx
│ │ ├── conversation-section.tsx
│ │ ├── custom-fields-panel
│ │ │ ├── custom-field.tsx
│ │ │ └── index.tsx
│ │ ├── custom-fields-section.tsx
│ │ ├── deny-list-section.tsx
│ │ ├── domain-section.tsx
│ │ ├── email-authentication-section.tsx
│ │ ├── email-protection-section.tsx
│ │ ├── expiration-section.tsx
│ │ ├── expirationIn-section.tsx
│ │ ├── feedback-section.tsx
│ │ ├── index-file-section.tsx
│ │ ├── index.tsx
│ │ ├── link-item.tsx
│ │ ├── link-options.tsx
│ │ ├── og-section.tsx
│ │ ├── password-section.tsx
│ │ ├── pro-banner-section.tsx
│ │ ├── question-section.tsx
│ │ ├── screenshot-protection-section.tsx
│ │ ├── tags
│ │ │ ├── tag-badge.tsx
│ │ │ ├── tag-details.tsx
│ │ │ └── tag-section.tsx
│ │ ├── upload-section
│ │ │ └── index.tsx
│ │ ├── watermark-panel
│ │ │ └── index.tsx
│ │ └── watermark-section.tsx
│ ├── links-table.tsx
│ └── links-visitors.tsx
├── navigation-menu.tsx
├── profile-menu.tsx
├── profile-search-trigger.tsx
├── providers
│ └── posthog-provider.tsx
├── search-box.tsx
├── search-command.tsx
├── settings
│ ├── delete-team-modal.tsx
│ ├── delete-team.tsx
│ ├── og-preview.tsx
│ └── settings-header.tsx
├── shared
│ ├── icons
│ │ ├── advanced-sheet.tsx
│ │ ├── alert-circle.tsx
│ │ ├── arrow-up.tsx
│ │ ├── badge-check.tsx
│ │ ├── bar-chart.tsx
│ │ ├── check-cirlce-2.tsx
│ │ ├── check.tsx
│ │ ├── chevron-down.tsx
│ │ ├── chevron-right.tsx
│ │ ├── chevron-up.tsx
│ │ ├── circle.tsx
│ │ ├── cloud-download-off.tsx
│ │ ├── copy-right.tsx
│ │ ├── copy.tsx
│ │ ├── external-link.tsx
│ │ ├── eye-off.tsx
│ │ ├── eye.tsx
│ │ ├── facebook.tsx
│ │ ├── file-up.tsx
│ │ ├── files
│ │ │ ├── cad.tsx
│ │ │ ├── docs.tsx
│ │ │ ├── image.tsx
│ │ │ ├── map.tsx
│ │ │ ├── notion.tsx
│ │ │ ├── pdf.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── slides.tsx
│ │ │ └── video.tsx
│ │ ├── folder.tsx
│ │ ├── github.tsx
│ │ ├── globe.tsx
│ │ ├── google.tsx
│ │ ├── grip-vertical.tsx
│ │ ├── home.tsx
│ │ ├── index.tsx
│ │ ├── linkedin.tsx
│ │ ├── menu.tsx
│ │ ├── moon.tsx
│ │ ├── more-horizontal.tsx
│ │ ├── more-vertical.tsx
│ │ ├── papermark-sparkle.tsx
│ │ ├── passkey.tsx
│ │ ├── pie-chart.tsx
│ │ ├── portrait-landscape.tsx
│ │ ├── producthunt.tsx
│ │ ├── search.tsx
│ │ ├── settings.tsx
│ │ ├── sparkle.tsx
│ │ ├── sun.tsx
│ │ ├── teams.tsx
│ │ ├── twitter.tsx
│ │ ├── user-round.tsx
│ │ ├── x-circle.tsx
│ │ └── x.tsx
│ └── logo-cloud.tsx
├── sidebar-folders.tsx
├── sidebar
│ ├── app-sidebar.tsx
│ ├── nav-main.tsx
│ ├── nav-user.tsx
│ └── team-switcher.tsx
├── tab-menu.tsx
├── tags
│ └── add-tag-modal.tsx
├── teams
│ ├── add-team-member-modal.tsx
│ ├── add-team-modal.tsx
│ ├── delete-team-modal.tsx
│ └── select-team.tsx
├── theme-provider.tsx
├── theme-toggle.tsx
├── ui
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── alert.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── bar-list.tsx
│ ├── breadcrumb.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── devices.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── feature-preview.tsx
│ ├── file-upload.tsx
│ ├── form.tsx
│ ├── gauge.tsx
│ ├── input-otp.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── loading-dots.module.css
│ ├── loading-dots.tsx
│ ├── loading-spinner.module.css
│ ├── loading-spinner.tsx
│ ├── modal.tsx
│ ├── multi-select-v2.tsx
│ ├── nextra-filetree.tsx
│ ├── pagination.tsx
│ ├── phone-input.tsx
│ ├── popover.tsx
│ ├── portal.tsx
│ ├── progress.tsx
│ ├── radio-group.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── sidebar.tsx
│ ├── skeleton.tsx
│ ├── smart-date-time-picker.tsx
│ ├── sonner.tsx
│ ├── status-badge.tsx
│ ├── switch.tsx
│ ├── tab-select.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toggle-group.tsx
│ ├── toggle.tsx
│ └── tooltip.tsx
├── upload-notification.tsx
├── upload-zone.tsx
├── user-agent-icon.tsx
├── view
│ ├── ScreenProtection.tsx
│ ├── access-form
│ │ ├── agreement-section.tsx
│ │ ├── custom-fields-section.tsx
│ │ ├── email-section.tsx
│ │ ├── email-verification-form.tsx
│ │ ├── index.tsx
│ │ ├── name-section.tsx
│ │ └── password-section.tsx
│ ├── conversations
│ │ └── sidebar.tsx
│ ├── custom-metatag.tsx
│ ├── dataroom
│ │ ├── dataroom-document-view.tsx
│ │ ├── dataroom-view.tsx
│ │ ├── document-card.tsx
│ │ ├── document-upload-modal.tsx
│ │ ├── folder-card.tsx
│ │ ├── index-file-dialog.tsx
│ │ └── nav-dataroom.tsx
│ ├── document-view.tsx
│ ├── nav.tsx
│ ├── powered-by.tsx
│ ├── question.tsx
│ ├── report-form.tsx
│ ├── toolbar.tsx
│ ├── view-data.tsx
│ ├── viewer
│ │ ├── advanced-excel-viewer.tsx
│ │ ├── away-poster.tsx
│ │ ├── dataroom-viewer.tsx
│ │ ├── download-only-viewer.tsx
│ │ ├── excel-viewer.tsx
│ │ ├── image-viewer.tsx
│ │ ├── notion-page.tsx
│ │ ├── pages-horizontal-viewer.tsx
│ │ ├── pages-vertical-viewer.tsx
│ │ ├── pdf-default-viewer.tsx
│ │ ├── video-player.tsx
│ │ └── video-viewer.tsx
│ ├── visitor-graph.tsx
│ └── watermark-svg.tsx
├── viewer-upload-component.tsx
├── viewer-upload-zone.tsx
├── visitors
│ ├── contacts-document-table.tsx
│ ├── contacts-table.tsx
│ ├── data-table-pagination.tsx
│ ├── dataroom-viewers.tsx
│ ├── dataroom-visitor-custom-fields.tsx
│ ├── dataroom-visitor-useragent.tsx
│ ├── dataroom-visitors-history.tsx
│ ├── dataroom-visitors-table.tsx
│ ├── visitor-avatar.tsx
│ ├── visitor-chart.tsx
│ ├── visitor-clicks.tsx
│ ├── visitor-custom-fields.tsx
│ ├── visitor-useragent-base.tsx
│ ├── visitor-useragent-placeholder.tsx
│ ├── visitor-useragent.tsx
│ ├── visitor-video-chart.tsx
│ └── visitors-table.tsx
├── webhooks
│ └── webhook-events.tsx
└── welcome
│ ├── containers
│ ├── link-option-container.tsx
│ ├── onboarding-dataroom-link-options.tsx
│ ├── onboarding-link-options.tsx
│ └── upload-container.tsx
│ ├── dataroom-trial.tsx
│ ├── dataroom-upload.tsx
│ ├── dataroom.tsx
│ ├── intro.tsx
│ ├── next.tsx
│ ├── notion-form.tsx
│ ├── select.tsx
│ ├── special-upload.tsx
│ └── upload.tsx
├── context
└── team-context.tsx
├── ee
├── LICENSE
├── features
│ └── conversations
│ │ ├── api
│ │ ├── conversations-route.ts
│ │ ├── send-conversation-new-message-notification.ts
│ │ ├── team-conversations-route.ts
│ │ └── toggle-conversations-route.ts
│ │ ├── components
│ │ ├── conversation-list-item.tsx
│ │ ├── conversation-message.tsx
│ │ ├── conversation-view-sidebar.tsx
│ │ ├── conversations-not-enabled-banner.tsx
│ │ └── link-option-conversation-section.tsx
│ │ ├── emails
│ │ ├── components
│ │ │ └── conversation-notification.tsx
│ │ └── lib
│ │ │ └── send-conversation-notification.ts
│ │ ├── lib
│ │ ├── api
│ │ │ ├── conversations
│ │ │ │ └── index.ts
│ │ │ ├── messages
│ │ │ │ └── index.ts
│ │ │ └── notifications
│ │ │ │ └── index.ts
│ │ └── trigger
│ │ │ └── conversation-message-notification.ts
│ │ └── pages
│ │ ├── conversation-detail.tsx
│ │ └── conversation-overview.tsx
├── limits
│ ├── constants.ts
│ ├── handler.ts
│ ├── server.ts
│ └── swr-handler.ts
└── stripe
│ ├── client.ts
│ ├── constants.ts
│ ├── functions
│ ├── get-price-id-from-plan.ts
│ ├── get-quantity-from-plan.ts
│ └── get-subscription-item.ts
│ ├── index.ts
│ ├── utils.ts
│ └── webhooks
│ ├── checkout-session-completed.ts
│ ├── customer-subscription-deleted.ts
│ └── customer-subscription-updated.ts
├── lib
├── analytics
│ └── index.ts
├── api
│ ├── auth
│ │ ├── passkey.ts
│ │ └── token.ts
│ ├── documents
│ │ └── process-document.ts
│ ├── domains.ts
│ ├── links
│ │ └── link-data.ts
│ ├── notification-helper.ts
│ └── views
│ │ └── send-webhook-event.ts
├── auth
│ ├── dataroom-auth.ts
│ └── preview-auth.ts
├── constants.ts
├── cron
│ ├── index.ts
│ └── verify-qstash.ts
├── dataroom
│ └── index-generator.ts
├── documents
│ ├── create-document.ts
│ ├── get-file-helper.ts
│ ├── move-dataroom-documents.ts
│ ├── move-dataroom-folders.ts
│ ├── move-documents.ts
│ └── move-folder.ts
├── domains.ts
├── edge-config
│ ├── blacklist.ts
│ └── custom-email.ts
├── emails
│ ├── send-dataroom-info.ts
│ ├── send-dataroom-notification.ts
│ ├── send-dataroom-trial-end.ts
│ ├── send-dataroom-trial.ts
│ ├── send-dataroom-viewer-invite.ts
│ ├── send-deleted-domain.ts
│ ├── send-email-otp-verification.ts
│ ├── send-email-verification.ts
│ ├── send-invalid-domain.ts
│ ├── send-mail-verification.ts
│ ├── send-onboarding.ts
│ ├── send-teammate-invite.ts
│ ├── send-trial-end-final-reminder.ts
│ ├── send-trial-end-reminder.ts
│ ├── send-upgrade-plan.ts
│ ├── send-verification-request.ts
│ ├── send-viewed-dataroom.ts
│ ├── send-viewed-document.ts
│ └── send-welcome.ts
├── errorHandler.ts
├── featureFlags
│ └── index.ts
├── files
│ ├── aws-client.ts
│ ├── bulk-download.ts
│ ├── copy-file-server.ts
│ ├── copy-file-to-bucket-server.ts
│ ├── delete-file-server.ts
│ ├── delete-team-files-server.ts
│ ├── get-file.ts
│ ├── put-file-server.ts
│ ├── put-file.ts
│ ├── stream-file-server.ts
│ ├── tus-redis-locker.ts
│ ├── tus-upload.ts
│ └── viewer-tus-upload.ts
├── folders
│ └── create-folder.ts
├── hanko.ts
├── hooks
│ └── use-mobile.tsx
├── id-helper.ts
├── incoming-webhooks
│ └── index.ts
├── middleware
│ ├── app.ts
│ ├── domain.ts
│ ├── incoming-webhooks.ts
│ └── posthog.ts
├── notion
│ ├── config.ts
│ ├── index.ts
│ └── utils.ts
├── openai.ts
├── posthog.ts
├── prisma.ts
├── redis.ts
├── resend.ts
├── sheet
│ └── index.ts
├── swr
│ ├── use-agreements.ts
│ ├── use-billing.ts
│ ├── use-brand.ts
│ ├── use-dataroom-document-stats.ts
│ ├── use-dataroom-groups.ts
│ ├── use-dataroom-stats.ts
│ ├── use-dataroom.ts
│ ├── use-datarooms.ts
│ ├── use-document-stats.ts
│ ├── use-document.ts
│ ├── use-documents.ts
│ ├── use-domains.ts
│ ├── use-folders.ts
│ ├── use-invitations.ts
│ ├── use-limits.ts
│ ├── use-link.ts
│ ├── use-stats.ts
│ ├── use-tags.ts
│ ├── use-team.ts
│ ├── use-teams.ts
│ ├── use-viewer.ts
│ └── use-viewers.ts
├── team
│ └── helper.ts
├── tinybird
│ ├── README.md
│ ├── datasources
│ │ ├── click_events.datasource
│ │ ├── page_views.datasource
│ │ ├── pm_click_events.datasource
│ │ ├── video_views.datasource
│ │ └── webhook_events.datasource
│ ├── endpoints
│ │ ├── get_click_events_by_view.pipe
│ │ ├── get_document_duration_per_viewer.pipe
│ │ ├── get_page_duration_per_view.pipe
│ │ ├── get_total_average_page_duration.pipe
│ │ ├── get_total_dataroom_duration.pipe
│ │ ├── get_total_document_duration.pipe
│ │ ├── get_total_link_duration.pipe
│ │ ├── get_total_viewer_duration.pipe
│ │ ├── get_useragent_per_view.pipe
│ │ ├── get_video_events_by_document.pipe
│ │ ├── get_video_events_by_view.pipe
│ │ └── get_webhook_events.pipe
│ ├── index.ts
│ ├── pipes.ts
│ └── publish.ts
├── tracking
│ ├── record-link-view.ts
│ ├── safe-page-view-tracker.ts
│ ├── tracking-config.ts
│ └── video-tracking.ts
├── trigger
│ ├── conversation-message-notification.ts
│ ├── convert-files.ts
│ ├── dataroom-change-notification.ts
│ ├── optimize-video-files.ts
│ ├── pdf-to-image-route.ts
│ └── send-scheduled-email.ts
├── types.ts
├── types
│ └── index-file.ts
├── unsend.ts
├── utils.ts
├── utils
│ ├── csv.ts
│ ├── decode-base64url.ts
│ ├── determine-text-color.ts
│ ├── generate-checksum.ts
│ ├── generate-jwt.ts
│ ├── generate-otp.ts
│ ├── generate-trigger-auth-token.ts
│ ├── generate-trigger-status.ts
│ ├── geo.ts
│ ├── get-content-type.ts
│ ├── get-file-icon.tsx
│ ├── get-file-size-limits.ts
│ ├── get-page-number-count.ts
│ ├── get-search-params.ts
│ ├── ip.ts
│ ├── reliable-tracking.ts
│ ├── resize-image.ts
│ ├── sanitize-html.ts
│ ├── sort-items-by-index-name.ts
│ ├── trigger-utils.ts
│ ├── unsubscribe.ts
│ ├── use-at-bottom.ts
│ ├── use-copy-to-clipboard.ts
│ ├── use-enter-submit.ts
│ ├── use-media-query.ts
│ ├── use-progress-status.ts
│ ├── user-agent.ts
│ └── validate-email.ts
├── webhook
│ ├── constants.ts
│ ├── send-webhooks.ts
│ ├── signature.ts
│ ├── transform.ts
│ ├── triggers
│ │ ├── document-created.ts
│ │ └── link-created.ts
│ └── types.ts
├── webstorage.ts
├── year-in-review
│ ├── calculate-percentile.ts
│ ├── get-stats.ts
│ ├── index.ts
│ └── send-emails.ts
└── zod
│ └── schemas
│ ├── notifications.ts
│ ├── presets.ts
│ └── webhooks.ts
├── middleware.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── pages
├── 404.tsx
├── _app.tsx
├── _document.tsx
├── account
│ ├── general.tsx
│ └── security.tsx
├── api
│ ├── account
│ │ └── index.ts
│ ├── analytics
│ │ └── index.ts
│ ├── assistants
│ │ ├── chat.ts
│ │ ├── index.ts
│ │ └── threads
│ │ │ └── index.ts
│ ├── auth-plus
│ │ └── set-cookie.ts
│ ├── auth
│ │ └── [...nextauth].ts
│ ├── conversations
│ │ └── [[...conversations]].ts
│ ├── feedback
│ │ └── index.ts
│ ├── file
│ │ ├── browser-upload.ts
│ │ ├── image-upload.ts
│ │ ├── notion
│ │ │ └── index.ts
│ │ ├── s3
│ │ │ ├── get-presigned-get-url.ts
│ │ │ └── get-presigned-post-url.ts
│ │ ├── tus-viewer
│ │ │ └── [[...file]].ts
│ │ └── tus
│ │ │ └── [[...file]].ts
│ ├── health.ts
│ ├── jobs
│ │ ├── get-thumbnail.ts
│ │ ├── send-conversation-new-message-notification.ts
│ │ ├── send-dataroom-new-document-notification.ts
│ │ ├── send-dataroom-view-invitation.ts
│ │ └── send-notification.ts
│ ├── links
│ │ ├── [id]
│ │ │ ├── archive.ts
│ │ │ ├── dataroom.ts
│ │ │ ├── documents
│ │ │ │ └── [documentId].ts
│ │ │ ├── duplicate.ts
│ │ │ ├── index.ts
│ │ │ ├── preview.ts
│ │ │ └── visits.ts
│ │ ├── domains
│ │ │ └── [...domainSlug].ts
│ │ ├── download
│ │ │ ├── bulk.ts
│ │ │ ├── dataroom-document.ts
│ │ │ ├── dataroom-folder.ts
│ │ │ └── index.ts
│ │ ├── generate-index.ts
│ │ └── index.ts
│ ├── mupdf
│ │ ├── annotate-document.ts
│ │ ├── convert-page.ts
│ │ └── get-pages.ts
│ ├── passkeys
│ │ └── register.ts
│ ├── progress-token.ts
│ ├── record_click.ts
│ ├── record_reaction.ts
│ ├── record_video_view.ts
│ ├── record_view.ts
│ ├── report.ts
│ ├── revalidate.ts
│ ├── stripe
│ │ ├── webhook-old.ts
│ │ └── webhook.ts
│ ├── teams
│ │ ├── [teamId]
│ │ │ ├── agreements
│ │ │ │ ├── [agreementId]
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── billing
│ │ │ │ ├── index.ts
│ │ │ │ ├── manage.ts
│ │ │ │ ├── plan.ts
│ │ │ │ └── upgrade.ts
│ │ │ ├── branding.ts
│ │ │ ├── change-role.ts
│ │ │ ├── datarooms
│ │ │ │ ├── [id]
│ │ │ │ │ ├── branding.ts
│ │ │ │ │ ├── conversations
│ │ │ │ │ │ ├── [[...conversations]].ts
│ │ │ │ │ │ └── toggle-conversations.ts
│ │ │ │ │ ├── documents
│ │ │ │ │ │ ├── [documentId]
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── stats.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── move.ts
│ │ │ │ │ ├── download
│ │ │ │ │ │ └── bulk.ts
│ │ │ │ │ ├── duplicate.ts
│ │ │ │ │ ├── export-visits.ts
│ │ │ │ │ ├── folders
│ │ │ │ │ │ ├── [...name].ts
│ │ │ │ │ │ ├── documents
│ │ │ │ │ │ │ └── [...name].ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── manage
│ │ │ │ │ │ │ ├── [folderId]
│ │ │ │ │ │ │ │ ├── dataroom-to-dataroom.ts
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── move.ts
│ │ │ │ │ │ └── parents
│ │ │ │ │ │ │ └── [...name].ts
│ │ │ │ │ ├── generate-index.ts
│ │ │ │ │ ├── groups
│ │ │ │ │ │ ├── [groupId]
│ │ │ │ │ │ │ ├── export-visits.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── links.ts
│ │ │ │ │ │ │ ├── members
│ │ │ │ │ │ │ │ ├── [memberId].ts
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── permissions.ts
│ │ │ │ │ │ │ └── views
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── links.ts
│ │ │ │ │ ├── reorder.ts
│ │ │ │ │ ├── stats.ts
│ │ │ │ │ ├── users
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── viewers
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── views
│ │ │ │ │ │ ├── [viewId]
│ │ │ │ │ │ ├── custom-fields.ts
│ │ │ │ │ │ ├── history.ts
│ │ │ │ │ │ └── user-agent.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── create-from-folder.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── trial.ts
│ │ │ ├── documents
│ │ │ │ ├── [id]
│ │ │ │ │ ├── add-to-dataroom.ts
│ │ │ │ │ ├── advanced-mode.ts
│ │ │ │ │ ├── change-orientation.ts
│ │ │ │ │ ├── duplicate.ts
│ │ │ │ │ ├── export-visits.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── links.ts
│ │ │ │ │ ├── stats.ts
│ │ │ │ │ ├── toggle-dark-mode.ts
│ │ │ │ │ ├── toggle-download-only.ts
│ │ │ │ │ ├── update-name.ts
│ │ │ │ │ ├── versions
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── video-analytics.ts
│ │ │ │ │ └── views
│ │ │ │ │ │ ├── [viewId]
│ │ │ │ │ │ ├── click-events.ts
│ │ │ │ │ │ ├── custom-fields.ts
│ │ │ │ │ │ ├── stats.ts
│ │ │ │ │ │ ├── user-agent.ts
│ │ │ │ │ │ └── video-stats.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── agreement.ts
│ │ │ │ ├── document-processing-status.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── move.ts
│ │ │ │ ├── search.ts
│ │ │ │ └── update.ts
│ │ │ ├── domains
│ │ │ │ ├── [domain]
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── verify.ts
│ │ │ │ └── index.ts
│ │ │ ├── enable-advanced-mode.ts
│ │ │ ├── folders
│ │ │ │ ├── [...name].ts
│ │ │ │ ├── documents
│ │ │ │ │ └── [...name].ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── manage
│ │ │ │ │ ├── [folderId]
│ │ │ │ │ │ ├── add-to-dataroom.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── move.ts
│ │ │ │ └── parents
│ │ │ │ │ └── [...name].ts
│ │ │ ├── incoming-webhooks
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── invitations
│ │ │ │ ├── accept.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── resend.ts
│ │ │ ├── invite.ts
│ │ │ ├── limits.ts
│ │ │ ├── presets
│ │ │ │ ├── [id].ts
│ │ │ │ └── index.ts
│ │ │ ├── remove-teammate.ts
│ │ │ ├── tags
│ │ │ │ ├── [id]
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── tokens
│ │ │ │ └── index.ts
│ │ │ ├── update-advanced-mode.ts
│ │ │ ├── update-name.ts
│ │ │ ├── viewers
│ │ │ │ ├── [id]
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── views
│ │ │ │ └── [id]
│ │ │ │ │ └── archive.ts
│ │ │ └── webhooks
│ │ │ │ ├── [id]
│ │ │ │ ├── events.ts
│ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ └── index.ts
│ ├── unsubscribe
│ │ ├── dataroom
│ │ │ └── index.ts
│ │ └── yir
│ │ │ └── index.ts
│ └── webhooks
│ │ └── services
│ │ └── [...path]
│ │ └── index.ts
├── branding.tsx
├── dashboard.tsx
├── datarooms
│ ├── [id]
│ │ ├── analytics
│ │ │ └── index.tsx
│ │ ├── branding
│ │ │ └── index.tsx
│ │ ├── conversations
│ │ │ ├── [conversationId]
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── documents
│ │ │ ├── [...name].tsx
│ │ │ └── index.tsx
│ │ ├── groups
│ │ │ ├── [groupId]
│ │ │ │ ├── group-analytics.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── links.tsx
│ │ │ │ ├── members.tsx
│ │ │ │ └── permissions.tsx
│ │ │ └── index.tsx
│ │ ├── index.tsx
│ │ ├── permissions
│ │ │ └── index.tsx
│ │ ├── settings
│ │ │ ├── index.tsx
│ │ │ └── notifications.tsx
│ │ └── users
│ │ │ └── index.tsx
│ └── index.tsx
├── documents
│ ├── [id]
│ │ ├── chat.tsx
│ │ ├── index.tsx
│ │ └── settings.tsx
│ ├── index.tsx
│ ├── new.tsx
│ └── tree
│ │ └── [...name].tsx
├── entrance_ppreview_demo.tsx
├── nav_ppreview_demo.tsx
├── room_ppreview_demo.tsx
├── settings
│ ├── agreements.tsx
│ ├── billing.tsx
│ ├── domains.tsx
│ ├── general.tsx
│ ├── incoming-webhooks.tsx
│ ├── people.tsx
│ ├── presets
│ │ ├── [id].tsx
│ │ ├── index.tsx
│ │ └── new.tsx
│ ├── tags.tsx
│ ├── tokens.tsx
│ ├── upgrade.tsx
│ └── webhooks
│ │ ├── [id]
│ │ └── index.tsx
│ │ ├── index.tsx
│ │ └── new.tsx
├── unsubscribe.tsx
├── view
│ ├── [linkId]
│ │ ├── chat.tsx
│ │ ├── d
│ │ │ └── [documentId].tsx
│ │ ├── embed.tsx
│ │ └── index.tsx
│ └── domains
│ │ └── [domain]
│ │ └── [slug]
│ │ ├── d
│ │ └── [documentId].tsx
│ │ └── index.tsx
├── visitors
│ ├── [id]
│ │ └── index.tsx
│ └── index.tsx
└── welcome.tsx
├── pkgx.yaml
├── postcss.config.js
├── prettier.config.js
├── prisma
├── README.md
├── add-migration.sh
├── migrations
│ ├── 20230912150657_initialize
│ │ └── migration.sql
│ ├── 202310122339_NewColumnInLinkTable
│ │ └── migration.sql
│ ├── 20231013165123_create_document_version
│ │ └── migration.sql
│ ├── 20231014200337_create_document_pages
│ │ └── migration.sql
│ ├── 202310311254_NewColumnEnableNotificationLinkTable
│ │ └── migration.sql
│ ├── 20231105152632_create_team
│ │ └── migration.sql
│ ├── 20231113051339_create_sent_email
│ │ └── migration.sql
│ ├── 20231114054509_add_domain_to_sent_emails
│ │ └── migration.sql
│ ├── 20231116093816_update_invitations
│ │ └── migration.sql
│ ├── 20231127062841_add_conversation
│ │ └── migration.sql
│ ├── 20231128064540_add_indices
│ │ └── migration.sql
│ ├── 20231204070250_remove_trial
│ │ └── migration.sql
│ ├── 20231207081407_add_reactions
│ │ └── migration.sql
│ ├── 20240110233134_add_disable_feedback
│ │ └── migration.sql
│ ├── 20240117020456_add_branding
│ │ └── migration.sql
│ ├── 20240202052149_add_email_authentication_to_link_and_view
│ │ └── migration.sql
│ ├── 20240205170242_embedded_links
│ │ └── migration.sql
│ ├── 20240212081614_add_downloaded_time_to_view
│ │ └── migration.sql
│ ├── 20240215035046_add_allow_deny_list_to_links
│ │ └── migration.sql
│ ├── 20240221042933_add_document_storage_type_enum
│ │ └── migration.sql
│ ├── 20240313100203_add_folders
│ │ └── migration.sql
│ ├── 20240327102407_add_dataroom
│ │ └── migration.sql
│ ├── 20240330062000_add_viewtype
│ │ └── migration.sql
│ ├── 20240401000000_add_dataroom_brand
│ │ └── migration.sql
│ ├── 20240408000000_add_viewer
│ │ └── migration.sql
│ ├── 20240415000000_add_feedback
│ │ └── migration.sql
│ ├── 20240424152839_add_screenprotection_to_link
│ │ └── migration.sql
│ ├── 20240511000000_add_team_limits
│ │ └── migration.sql
│ ├── 20240520000000_add_vertical_to_document_version
│ │ └── migration.sql
│ ├── 20240521000000_add_manager_role
│ │ └── migration.sql
│ ├── 20240611000000_add_agreements
│ │ └── migration.sql
│ ├── 20240712000000_add_page_metadata
│ │ └── migration.sql
│ ├── 20240720000000_change_owner_dependecy
│ │ └── migration.sql
│ ├── 20240730000000_update_link_defaults
│ │ └── migration.sql
│ ├── 20240731000000_add_link_show_banner
│ │ └── migration.sql
│ ├── 20240809000000_add_dataroom_order_index
│ │ └── migration.sql
│ ├── 20240821000000_add_require_name
│ │ └── migration.sql
│ ├── 20240830000000_add_watermarks
│ │ └── migration.sql
│ ├── 20240901000000_add_domain_default
│ │ └── migration.sql
│ ├── 20240902000000_add_link_presets
│ │ └── migration.sql
│ ├── 20240911000000_add_dataroom_groups_permissions
│ │ └── migration.sql
│ ├── 20240915000000_add_advanced_mode
│ │ └── migration.sql
│ ├── 20240916000000_add_content_type_to_document
│ │ └── migration.sql
│ ├── 20240921000000_add_viewer_migration
│ │ └── migration.sql
│ ├── 20241004024010_add_favicon_column
│ │ └── migration.sql
│ ├── 20241020000000_add_teamid_to_link_and_view
│ │ └── migration.sql
│ ├── 20241029000000_add_archived_view
│ │ └── migration.sql
│ ├── 20241107000000_add_tokens_and_webhooks
│ │ └── migration.sql
│ ├── 20241118000000_add_filesize_and_downloadonly
│ │ └── migration.sql
│ ├── 20241123000000_add_viewer_notification_preferences
│ │ └── migration.sql
│ ├── 20241126000000_add_screen_shield
│ │ └── migration.sql
│ ├── 20241208000000_add_webhooks
│ │ └── migration.sql
│ ├── 20241212000000_add_yir
│ │ └── migration.sql
│ ├── 20250110000000_add_length_to_document_version
│ │ └── migration.sql
│ ├── 20250113000000_add_custom_fields
│ │ └── migration.sql
│ ├── 20250204000000_add_anonymous_group
│ │ └── migration.sql
│ ├── 20250217000000_add_contactid_to_user
│ │ └── migration.sql
│ ├── 20250217000000_remove_link_column
│ │ └── migration.sql
│ ├── 20250310000000_rename_conversation_table
│ │ └── migration.sql
│ ├── 20250404000000_add_questions_answer_conversation
│ │ └── migration.sql
│ ├── 20250413000000_add_dataroom_upload
│ │ └── migration.sql
│ ├── 20250425000000_update_link_presets
│ │ └── migration.sql
│ ├── 20250428000000_add_tags
│ │ └── migration.sql
│ ├── 20250502000000_add_additional_present_fields
│ │ └── migration.sql
│ ├── 20250511000000_add_index_file_to_links
│ │ └── migration.sql
│ ├── 20250513000000_add_notification_to_dataroom
│ │ └── migration.sql
│ ├── 20250516000000_add_advanced_mode_to_team
│ │ └── migration.sql
│ ├── 20250526000000_add_status_to_users
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema
│ ├── conversation.prisma
│ ├── dataroom.prisma
│ ├── link.prisma
│ └── schema.prisma
├── public
├── _example
│ ├── papermark-example-document.pdf
│ └── papermark-example-page.png
├── _icons
│ ├── doc.svg
│ ├── gif.svg
│ ├── jpg.svg
│ ├── other.svg
│ ├── pdf-light.svg
│ ├── pdf.svg
│ ├── png.svg
│ ├── ppt.svg
│ ├── sheet-light.svg
│ ├── sheet.svg
│ └── xls.svg
├── _static
│ ├── Inter-Bold.ttf
│ ├── blank.gif
│ ├── macos.png
│ ├── meta-image.png
│ ├── papermark-banner.png
│ ├── papermark-logo-light.svg
│ ├── papermark-logo.svg
│ ├── papermark-p.svg
│ └── testimonials
│ │ ├── backtrace.jpeg
│ │ ├── jaski.jpeg
│ │ └── steven.jpeg
├── favicon.ico
└── vendor
│ └── handsontable
│ ├── handsontable.full.min.css
│ └── handsontable.full.min.js
├── styles
├── Inter-Regular.ttf
├── custom-notion-styles.css
├── custom-viewer-styles.css
└── globals.css
├── tailwind.config.js
├── trigger.config.ts
├── tsconfig.json
└── vercel.json
/.cursorignore:
--------------------------------------------------------------------------------
1 | .env
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "@next/next/no-img-element": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Learn how to add code owners here:
2 | # https://help.github.com/en/articles/about-code-owners
3 |
4 | # These owners will be the default owners for everything in the repo.
5 | # Order is important; the last matching pattern takes the most precedence.
6 |
7 | * @mfts
8 | /.github/ @mfts
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/oss-gg-hack-template.yml:
--------------------------------------------------------------------------------
1 | name: oss.gg hack submission 🕹️
2 | description: "Submit your contribution for the for the oss.gg hackathon"
3 | title: "[🕹️]"
4 | labels: 🕹️ oss.gg, player submission, hacktoberfest
5 | assignees: []
6 | body:
7 | - type: textarea
8 | id: contribution-name
9 | attributes:
10 | label: What side quest or challenge are you solving?
11 | description: Add the name of the side quest or challenge.
12 | validations:
13 | required: true
14 | - type: textarea
15 | id: points
16 | attributes:
17 | label: Points
18 | description: How many points are assigned to this contribution?
19 | validations:
20 | required: true
21 | - type: textarea
22 | id: description
23 | attributes:
24 | label: Description
25 | description: What's the task your performed?
26 | validations:
27 | - type: textarea
28 | id: proof
29 | attributes:
30 | label: Provide proof that you've completed the task
31 | description: Screenshots, loom recordings, links to the content you shared or interacted with.
32 | validations:
33 | required: true
34 |
--------------------------------------------------------------------------------
/.github/images/papermark-welcome.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/.github/images/papermark-welcome.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 |
12 | # nvm
13 | .npmrc
14 | .nvmrc
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # local env files
33 | .env*.local
34 | .env
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | # react-email
44 | .react-email
45 |
46 | # Tinybird config for the cli is stored in this file
47 | .tinyb
48 |
49 | # Internal scripts
50 | pages/api/scripts
51 | scripts/
52 |
53 | # marketing emails
54 | components/emails/marketing
55 | lib/emails/marketing
56 |
57 | # vscode configs
58 | .vscode
59 |
60 | # trigger.dev
61 | .trigger
62 |
63 | # changelog
64 | changelog
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .next/
3 | .react-email/
4 | .vercel/
5 | .github/
6 | *.min.js
7 | *.min.css
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | oss-gg
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | tinybird-cli = "*"
8 |
9 | [dev-packages]
10 |
11 | [requires]
12 | python_version = "3.11"
13 |
--------------------------------------------------------------------------------
/app/(auth)/auth/confirm-email-change/[token]/page-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 |
5 | import { useEffect, useRef } from "react";
6 |
7 | import { useSession } from "next-auth/react";
8 | import { toast } from "sonner";
9 |
10 | import LoadingSpinner from "@/components/ui/loading-spinner";
11 |
12 | export default function ConfirmEmailChangePageClient() {
13 | const router = useRouter();
14 | const { update, status } = useSession();
15 | const hasUpdatedSession = useRef(false);
16 |
17 | useEffect(() => {
18 | if (status !== "authenticated" || hasUpdatedSession.current) {
19 | return;
20 | }
21 |
22 | async function updateSession() {
23 | hasUpdatedSession.current = true;
24 | await update();
25 | toast.success("Email update successful!");
26 | router.replace("/account/general");
27 | }
28 |
29 | updateSession();
30 | }, [status, update]);
31 |
32 | return (
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/(auth)/auth/confirm-email-change/[token]/utils.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/pages/api/auth/[...nextauth]";
2 | import { getServerSession } from "next-auth";
3 |
4 | export const getSession = async () => {
5 | return getServerSession(authOptions);
6 | };
7 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SessionProvider } from "next-auth/react";
4 | import { Toaster } from "sonner";
5 |
6 | import { ThemeProvider } from "@/components/theme-provider";
7 |
8 | export default function Layout({ children }: { children: React.ReactNode }) {
9 | return (
10 |
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 |
3 | import LoginClient from "./page-client";
4 |
5 | const data = {
6 | description: "Login to Papermark",
7 | title: "Login | Papermark",
8 | url: "/login",
9 | };
10 |
11 | export const metadata: Metadata = {
12 | metadataBase: new URL("https://www.papermark.com"),
13 | title: data.title,
14 | description: data.description,
15 | openGraph: {
16 | title: data.title,
17 | description: data.description,
18 | url: data.url,
19 | siteName: "Papermark",
20 | images: [
21 | {
22 | url: "/_static/meta-image.png",
23 | width: 800,
24 | height: 600,
25 | },
26 | ],
27 | locale: "en_US",
28 | type: "website",
29 | },
30 | twitter: {
31 | card: "summary_large_image",
32 | title: data.title,
33 | description: data.description,
34 | creator: "@papermarkio",
35 | images: ["/_static/meta-image.png"],
36 | },
37 | };
38 |
39 | export default function LoginPage() {
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 |
3 | import RegisterClient from "./page-client";
4 |
5 | const data = {
6 | description: "Signup to Papermark",
7 | title: "Sign up | Papermark",
8 | url: "/register",
9 | };
10 |
11 | export const metadata: Metadata = {
12 | metadataBase: new URL("https://www.papermark.com"),
13 | title: data.title,
14 | description: data.description,
15 | openGraph: {
16 | title: data.title,
17 | description: data.description,
18 | url: data.url,
19 | siteName: "Papermark",
20 | images: [
21 | {
22 | url: "/_static/meta-image.png",
23 | width: 800,
24 | height: 600,
25 | },
26 | ],
27 | locale: "en_US",
28 | type: "website",
29 | },
30 | twitter: {
31 | card: "summary_large_image",
32 | title: data.title,
33 | description: data.description,
34 | creator: "@papermarkio",
35 | images: ["/_static/meta-image.png"],
36 | },
37 | };
38 |
39 | export default function RegisterPage() {
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/app/(auth)/verify/invitation/AcceptInvitationButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 |
5 | import { useState } from "react";
6 |
7 | import { Button } from "@/components/ui/button";
8 |
9 | interface AcceptInvitationButtonProps {
10 | verificationUrl: string;
11 | }
12 |
13 | export default function AcceptInvitationButton({
14 | verificationUrl,
15 | }: AcceptInvitationButtonProps) {
16 | const [isLoading, setIsLoading] = useState(false);
17 |
18 | const handleAccept = () => {
19 | setIsLoading(true);
20 | };
21 |
22 | return (
23 |
24 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/(auth)/verify/invitation/status/ClientRedirect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 |
5 | export default function CleanUrlOnExpire({
6 | shouldClean,
7 | }: {
8 | shouldClean: boolean;
9 | }) {
10 | useEffect(() => {
11 | if (shouldClean && typeof window !== "undefined") {
12 | const url = new URL(window.location.href);
13 | url.search = "";
14 | window.history.replaceState({}, "", url.toString());
15 | }
16 | }, [shouldClean]);
17 |
18 | return null;
19 | }
20 |
--------------------------------------------------------------------------------
/app/api/cron/year-in-review/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | import { receiver } from "@/lib/cron";
4 | import { log } from "@/lib/utils";
5 | import { processEmailQueue } from "@/lib/year-in-review/send-emails";
6 |
7 | // Runs every hour (0 * * * *)
8 | export const maxDuration = 300; // 5 minutes in seconds
9 |
10 | export async function POST(req: Request) {
11 | const body = await req.json();
12 | if (process.env.VERCEL === "1") {
13 | const isValid = await receiver.verify({
14 | signature: req.headers.get("Upstash-Signature") || "",
15 | body: JSON.stringify(body),
16 | });
17 | if (!isValid) {
18 | return new Response("Unauthorized", { status: 401 });
19 | }
20 | }
21 |
22 | try {
23 | await processEmailQueue();
24 | return NextResponse.json({ success: true });
25 | } catch (error) {
26 | await log({
27 | message: `Year in review email cron failed. \n\nError: ${(error as Error).message}`,
28 | type: "cron",
29 | mention: true,
30 | });
31 | return NextResponse.json({ error: (error as Error).message });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/api/csp-report/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export async function POST(request: Request) {
4 | const report = await request.json();
5 |
6 | // Log the report or send to your logging service
7 | // console.log("CSP Violation:", report);
8 |
9 | // You could send this to your logging service
10 | // await fetch('your-logging-service', {
11 | // method: 'POST',
12 | // body: JSON.stringify(report)
13 | // })
14 |
15 | return NextResponse.json({ success: true });
16 | }
17 |
--------------------------------------------------------------------------------
/app/api/feature-flags/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | import { getFeatureFlags } from "@/lib/featureFlags";
4 |
5 | export const runtime = "edge";
6 |
7 | export async function GET(request: Request) {
8 | const { searchParams } = new URL(request.url);
9 | const teamId = searchParams.get("teamId");
10 |
11 | try {
12 | const features = await getFeatureFlags({ teamId: teamId || undefined });
13 | return NextResponse.json(features);
14 | } catch (error) {
15 | console.error("Error fetching feature flags:", error);
16 | return NextResponse.json(
17 | { error: "Failed to fetch feature flags" },
18 | { status: 500 },
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Disallow: /login
3 | Disallow: /register
4 | Disallow: /verify/
5 | Disallow: /auth/
6 | Disallow: /unsubscribe
7 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "styles/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils",
15 | "ui": "@/components/ui",
16 | "lib": "@/lib",
17 | "hooks": "@/lib/hooks"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/components/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export default Skeleton;
16 |
--------------------------------------------------------------------------------
/components/account/account-header.tsx:
--------------------------------------------------------------------------------
1 | import { NavMenu } from "../navigation-menu";
2 |
3 | export function AccountHeader() {
4 | return (
5 |
6 |
7 |
8 |
9 | User Account
10 |
11 |
Manage your profile
12 |
13 |
14 |
15 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/components/billing/plan-badge.tsx:
--------------------------------------------------------------------------------
1 | import { CrownIcon } from "lucide-react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export default function PlanBadge({
6 | plan,
7 | className,
8 | }: {
9 | plan: string;
10 | className?: string;
11 | }) {
12 | return (
13 |
19 | {plan}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/blur-image.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image, { ImageProps } from "next/image";
4 |
5 | import { useEffect, useState } from "react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | export function BlurImage(props: ImageProps) {
10 | const [loading, setLoading] = useState(true);
11 | const [src, setSrc] = useState(props.src);
12 | useEffect(() => setSrc(props.src), [props.src]); // update the `src` value when the `prop.src` value changes
13 |
14 | const handleLoad = (e: React.SyntheticEvent) => {
15 | setLoading(false);
16 | const target = e.target as HTMLImageElement;
17 | if (target.naturalWidth <= 16 && target.naturalHeight <= 16) {
18 | setSrc(`https://avatar.vercel.sh/${encodeURIComponent(props.alt)}`);
19 | }
20 | };
21 |
22 | return (
23 | {
30 | setSrc(`https://avatar.vercel.sh/${encodeURIComponent(props.alt)}`); // if the image fails to load, use the default avatar
31 | }}
32 | unoptimized
33 | />
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/chat/chat-message-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { type Message } from "ai";
4 |
5 | import { Button } from "@/components/ui/button";
6 |
7 | import { cn } from "@/lib/utils";
8 | import { useCopyToClipboard } from "@/lib/utils/use-copy-to-clipboard";
9 |
10 | import Check from "../shared/icons/check";
11 | import Copy from "../shared/icons/copy";
12 |
13 | interface ChatMessageActionsProps extends React.ComponentProps<"div"> {
14 | message: Message;
15 | }
16 |
17 | export function ChatMessageActions({
18 | message,
19 | className,
20 | ...props
21 | }: ChatMessageActionsProps) {
22 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
23 |
24 | const onCopy = () => {
25 | if (isCopied) return;
26 | copyToClipboard(message.content);
27 | };
28 |
29 | return (
30 |
37 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/chat/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { useInView } from "react-intersection-observer";
6 |
7 | import { useAtBottom } from "@/lib/utils/use-at-bottom";
8 |
9 | interface ChatScrollAnchorProps {
10 | trackVisibility?: boolean;
11 | }
12 |
13 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
14 | const isAtBottom = useAtBottom();
15 | const { ref, entry, inView } = useInView({
16 | trackVisibility,
17 | delay: 100,
18 | rootMargin: "0px 0px -100px 0px",
19 | });
20 |
21 | React.useEffect(() => {
22 | if (isAtBottom && trackVisibility && !inView) {
23 | entry?.target.scrollIntoView({
24 | block: "start",
25 | });
26 | }
27 | }, [inView, entry, isAtBottom, trackVisibility]);
28 |
29 | return ;
30 | }
31 |
--------------------------------------------------------------------------------
/components/conversations/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ConversationListItem as ConversationListItemEE } from "@/ee/features/conversations/components/conversation-list-item";
4 | import { ConversationMessage as ConversationMessageEE } from "@/ee/features/conversations/components/conversation-message";
5 |
6 | export function ConversationListItem(props: any) {
7 | return ;
8 | }
9 |
10 | export function ConversationMessage(props: any) {
11 | return ;
12 | }
13 |
--------------------------------------------------------------------------------
/components/datarooms/actions/generate-index-button.tsx:
--------------------------------------------------------------------------------
1 | import GenerateIndexDialog from "./generate-index-dialog";
2 |
3 | interface GenerateIndexButtonProps {
4 | teamId: string;
5 | dataroomId: string;
6 | disabled?: boolean;
7 | }
8 |
9 | export default function GenerateIndexButton({
10 | teamId,
11 | dataroomId,
12 | disabled = false,
13 | }: GenerateIndexButtonProps) {
14 | return (
15 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/datarooms/empty-dataroom.tsx:
--------------------------------------------------------------------------------
1 | import { ServerIcon } from "lucide-react";
2 |
3 | export function EmptyDataroom() {
4 | return (
5 |
6 |
10 |
11 | No datarooms here
12 |
13 |
14 | Get started by creating a new dataroom.
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/datarooms/folders/index.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarFolderTreeSelection } from "./selection-tree";
2 | import { SidebarFolderTree } from "./sidebar-tree";
3 | import { ViewFolderTree } from "./view-tree";
4 |
5 | export { SidebarFolderTree, SidebarFolderTreeSelection, ViewFolderTree };
6 |
--------------------------------------------------------------------------------
/components/datarooms/groups/group-card-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardFooter,
5 | CardHeader,
6 | } from "@/components/ui/card";
7 | import { Skeleton } from "@/components/ui/skeleton";
8 |
9 | export function GroupCardPlaceholder() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/datarooms/groups/group-header.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { ChevronRightIcon } from "lucide-react";
4 |
5 | export const GroupHeader = ({
6 | dataroomId,
7 | groupName,
8 | }: {
9 | dataroomId: string;
10 | groupName: string;
11 | }) => {
12 | return (
13 |
14 |
15 |
16 | All Groups
17 |
18 |
19 |
20 |
21 | {groupName ?? "Management Team"}
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/components/datarooms/sortable/sortable-item.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useSortable } from "@dnd-kit/sortable";
4 | import { CSS } from "@dnd-kit/utilities";
5 |
6 | export type ItemCategory = "folder" | "document";
7 |
8 | interface SortableItemProps {
9 | id: string;
10 | category: ItemCategory;
11 | children: React.ReactElement;
12 | }
13 |
14 | export const SortableItem: React.FC = ({
15 | id,
16 | category,
17 | children,
18 | }) => {
19 | const { attributes, listeners, setNodeRef, transform, isDragging } =
20 | useSortable({
21 | id: id,
22 | data: {
23 | category: category,
24 | id: id.replace(category, ""),
25 | },
26 | });
27 |
28 | const style = {
29 | transform: CSS.Transform.toString(transform),
30 | opacity: isDragging ? 0.5 : 1,
31 | };
32 |
33 | const childWithProps = React.cloneElement(children, {
34 | isDragging,
35 | });
36 |
37 | return (
38 |
45 | {childWithProps}
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/documents/alert.tsx:
--------------------------------------------------------------------------------
1 | import { AlertCircleIcon } from "lucide-react";
2 |
3 | import {
4 | Alert,
5 | AlertClose,
6 | AlertDescription,
7 | AlertTitle,
8 | } from "@/components/ui/alert";
9 |
10 | interface AlertProps {
11 | id: string;
12 | variant: "default" | "destructive";
13 | title: string;
14 | description: React.ReactNode;
15 | onClose?: () => void;
16 | }
17 |
18 | const AlertBanner: React.FC = ({
19 | id,
20 | variant,
21 | title,
22 | description,
23 | onClose,
24 | }) => {
25 | return (
26 |
27 |
28 | {title}
29 | {description}
30 |
31 |
32 | );
33 | };
34 |
35 | export default AlertBanner;
36 |
--------------------------------------------------------------------------------
/components/documents/drag-and-drop/droppable-folder.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useDroppable } from "@dnd-kit/core";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | interface DroppableFolderProps {
8 | id: string;
9 | disabledFolder: string[];
10 | children: React.ReactElement;
11 | path: string;
12 | }
13 |
14 | export function DroppableFolder({
15 | id,
16 | disabledFolder,
17 | children,
18 | path,
19 | }: DroppableFolderProps) {
20 | const { isOver, setNodeRef } = useDroppable({
21 | id: id,
22 | data: { type: "folder", id, path },
23 | });
24 |
25 | const childWithProps = React.cloneElement(children, {
26 | isOver,
27 | });
28 |
29 | return (
30 |
38 | {childWithProps}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/components/documents/empty-document.tsx:
--------------------------------------------------------------------------------
1 | import { FilePlusIcon, PlusIcon } from "lucide-react";
2 |
3 | import { Button } from "../ui/button";
4 | import { AddDocumentModal } from "./add-document-modal";
5 |
6 | export function EmptyDocuments() {
7 | return (
8 |
9 |
13 |
14 | No documents here
15 |
16 |
17 | Get started by uploading a new document.
18 |
19 | {/*
20 |
21 |
28 |
29 |
*/}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/components/documents/loading-document.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "../ui/skeleton";
2 |
3 | export function LoadingDocuments({ count }: { count: number }) {
4 | return (
5 |
6 | {Array.from({ length: count }).map((_, i) => (
7 | -
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 | ))}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/documents/stats-chart-dummy.tsx:
--------------------------------------------------------------------------------
1 | import BarChartComponent from "../charts/bar-chart";
2 |
3 | export default function StatsChartDummy({
4 | totalPagesMax = 0,
5 | }: {
6 | totalPagesMax?: number;
7 | }) {
8 | let durationData = Array.from({ length: totalPagesMax }, (_, i) => ({
9 | pageNumber: (i + 1).toString(),
10 | data: [
11 | {
12 | versionNumber: 1,
13 | avg_duration: 16000 / (i + 1),
14 | },
15 | ],
16 | }));
17 |
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/documents/stats-chart-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { Skeleton } from "../ui/skeleton";
4 |
5 | const StatsChartSkeleton = ({ className }: { className?: string }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | {Array.from({ length: 5 }).map((_, i) => (
14 |
15 | ))}
16 |
17 |
18 | {[250, 200, 150, 100, 50, 20].map((item, i) => (
19 |
24 | ))}
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default StatsChartSkeleton;
32 |
--------------------------------------------------------------------------------
/components/domains/use-domain-status.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import useSWR from "swr";
3 | import useSWRImmutable from "swr/immutable";
4 |
5 | import {
6 | DomainConfigResponse,
7 | DomainResponse,
8 | DomainVerificationStatusProps,
9 | } from "@/lib/types";
10 | import { fetcher } from "@/lib/utils";
11 |
12 | export function useDomainStatus({ domain }: { domain: string }) {
13 | const teamInfo = useTeam();
14 |
15 | const { data, isValidating, mutate } = useSWR<{
16 | status: DomainVerificationStatusProps;
17 | response: {
18 | domainJson: DomainResponse & { error: { code: string; message: string } };
19 | configJson: DomainConfigResponse;
20 | };
21 | }>(
22 | `/api/teams/${teamInfo?.currentTeam?.id}/domains/${domain}/verify`,
23 | fetcher,
24 | );
25 |
26 | return {
27 | status: data?.status,
28 | domainJson: data?.response.domainJson,
29 | configJson: data?.response.configJson,
30 | loading: isValidating,
31 | mutate,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/components/emails/dataroom-trial-welcome.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Body, Head, Html, Tailwind, Text } from "@react-email/components";
4 |
5 | interface WelcomeEmailProps {
6 | name: string | null | undefined;
7 | }
8 |
9 | const DataroomTrialWelcomeEmail = ({ name }: WelcomeEmailProps) => {
10 | return (
11 |
12 |
13 |
14 |
15 | Hi {name},
16 |
17 | I am Marc, founder of Papermark. Thanks for creating a trial. Do you
18 | need any help with Data Rooms setup?
19 |
20 | Marc
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default DataroomTrialWelcomeEmail;
28 |
--------------------------------------------------------------------------------
/components/hooks/use-optimistic-update.ts:
--------------------------------------------------------------------------------
1 | import { toast } from "sonner";
2 | import useSWR from "swr";
3 |
4 | import { fetcher } from "@/lib/utils";
5 |
6 | export function useOptimisticUpdate(
7 | url: string,
8 | toastCopy?: { loading: string; success: string; error: string },
9 | ) {
10 | const { data, isLoading, mutate } = useSWR(url, fetcher);
11 |
12 | return {
13 | data,
14 | isLoading,
15 | update: async (fn: (data: T) => Promise, optimisticData: T) => {
16 | return toast.promise(
17 | mutate(fn(data as T), {
18 | optimisticData,
19 | rollbackOnError: true,
20 | populateCache: true,
21 | revalidate: true,
22 | }),
23 | {
24 | loading: toastCopy?.loading || "Updating...",
25 | success: toastCopy?.success || "Successfully updated",
26 | error: toastCopy?.error || "Failed to update",
27 | },
28 | );
29 | },
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/components/links/link-sheet/allow-download-section.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | import { DEFAULT_LINK_TYPE } from ".";
4 | import LinkItem from "./link-item";
5 |
6 | export default function AllowDownloadSection({
7 | data,
8 | setData,
9 | }: {
10 | data: DEFAULT_LINK_TYPE;
11 | setData: React.Dispatch>;
12 | }) {
13 | const { allowDownload } = data;
14 | const [enabled, setEnabled] = useState(false);
15 |
16 | useEffect(() => {
17 | setEnabled(allowDownload);
18 | }, [allowDownload]);
19 |
20 | const handleAllowDownload = () => {
21 | const updatedAllowDownload = !enabled;
22 | setData({ ...data, allowDownload: updatedAllowDownload });
23 | setEnabled(updatedAllowDownload);
24 | };
25 |
26 | return (
27 |
28 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/links/link-sheet/allow-notification-section.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | import { DEFAULT_LINK_TYPE } from ".";
4 | import LinkItem from "./link-item";
5 |
6 | export default function AllowNotificationSection({
7 | data,
8 | setData,
9 | }: {
10 | data: DEFAULT_LINK_TYPE;
11 | setData: React.Dispatch>;
12 | }) {
13 | const { enableNotification } = data;
14 | const [enabled, setEnabled] = useState(true);
15 |
16 | useEffect(() => {
17 | setEnabled(enableNotification);
18 | }, [enableNotification]);
19 |
20 | const handleEnableNotification = () => {
21 | const updatedEnableNotification = !enabled;
22 | setData({ ...data, enableNotification: updatedEnableNotification });
23 | setEnabled(updatedEnableNotification);
24 | };
25 |
26 | return (
27 |
28 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/components/links/link-sheet/conversation-section.tsx:
--------------------------------------------------------------------------------
1 | import ConversationSection from "@/ee/features/conversations/components/link-option-conversation-section";
2 |
3 | export default ConversationSection;
4 |
--------------------------------------------------------------------------------
/components/links/link-sheet/feedback-section.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | import { DEFAULT_LINK_TYPE } from ".";
4 | import LinkItem from "./link-item";
5 |
6 | export default function FeedbackSection({
7 | data,
8 | setData,
9 | }: {
10 | data: DEFAULT_LINK_TYPE;
11 | setData: React.Dispatch>;
12 | }) {
13 | const { enableFeedback } = data;
14 | const [enabled, setEnabled] = useState(true);
15 |
16 | useEffect(() => {
17 | setEnabled(enableFeedback);
18 | }, [enableFeedback]);
19 |
20 | const handleEnableFeedback = () => {
21 | const updatedEnableFeedback = !enabled;
22 | setData({ ...data, enableFeedback: updatedEnableFeedback });
23 | setEnabled(updatedEnableFeedback);
24 | };
25 |
26 | return (
27 |
28 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/profile-search-trigger.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Search } from "lucide-react";
4 |
5 | interface ProfileSearchTriggerProps {
6 | onClick: () => void;
7 | }
8 |
9 | export function ProfileSearchTrigger({ onClick }: ProfileSearchTriggerProps) {
10 | return (
11 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/alert-circle.tsx:
--------------------------------------------------------------------------------
1 | export default function AlertCircle({
2 | className,
3 | fill,
4 | }: {
5 | className?: string;
6 | fill?: string;
7 | }) {
8 | return (
9 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/shared/icons/arrow-up.tsx:
--------------------------------------------------------------------------------
1 | export default function ArrowUp({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/badge-check.tsx:
--------------------------------------------------------------------------------
1 | export default function BadgeCheck({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/bar-chart.tsx:
--------------------------------------------------------------------------------
1 | export default function BarChart({ className }: { className?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/icons/check-cirlce-2.tsx:
--------------------------------------------------------------------------------
1 | export default function CheckCircle2({
2 | className,
3 | fill,
4 | }: {
5 | className?: string;
6 | fill?: string;
7 | }) {
8 | return (
9 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/shared/icons/check.tsx:
--------------------------------------------------------------------------------
1 | export default function Check({ className }: { className?: string }) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/chevron-down.tsx:
--------------------------------------------------------------------------------
1 | export default function ChevronDown({ className }: { className?: string }) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/chevron-right.tsx:
--------------------------------------------------------------------------------
1 | export default function ChevronRight({ className }: { className?: string }) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/chevron-up.tsx:
--------------------------------------------------------------------------------
1 | export default function ChevronUp({ className }: { className?: string }) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/circle.tsx:
--------------------------------------------------------------------------------
1 | export default function Circle({ className }: { className?: string }) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/copy-right.tsx:
--------------------------------------------------------------------------------
1 | export default function CopyRight({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/copy.tsx:
--------------------------------------------------------------------------------
1 | export default function Copy({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/external-link.tsx:
--------------------------------------------------------------------------------
1 | export default function CheckCircle2({ className }: { className?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/icons/eye-off.tsx:
--------------------------------------------------------------------------------
1 | export default function EyeOff({ className }: { className?: string }) {
2 | return (
3 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/shared/icons/eye.tsx:
--------------------------------------------------------------------------------
1 | export default function Eye({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/facebook.tsx:
--------------------------------------------------------------------------------
1 | export function Facebook({
2 | className,
3 | fill = "#1977f3",
4 | }: {
5 | className?: string;
6 | fill?: string;
7 | }) {
8 | return (
9 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/shared/icons/file-up.tsx:
--------------------------------------------------------------------------------
1 | export default function FileUp({ className }: { className?: string }) {
2 | return (
3 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/shared/icons/folder.tsx:
--------------------------------------------------------------------------------
1 | export default function Folder({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/github.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function GitHubIcon(
4 | props: React.JSX.IntrinsicAttributes & React.SVGProps,
5 | ) {
6 | return (
7 |
14 | );
15 | }
16 |
17 | export default GitHubIcon;
18 |
--------------------------------------------------------------------------------
/components/shared/icons/globe.tsx:
--------------------------------------------------------------------------------
1 | export default function Globe({ className }: { className?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/icons/google.tsx:
--------------------------------------------------------------------------------
1 | export default function Google({ className }: { className?: string }) {
2 | return (
3 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/shared/icons/grip-vertical.tsx:
--------------------------------------------------------------------------------
1 | export default function GripVertical({ className }: { className?: string }) {
2 | return (
3 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/shared/icons/home.tsx:
--------------------------------------------------------------------------------
1 | export default function Home({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ComponentType, SVGProps } from "react";
4 |
5 | import { LucideIcon } from "lucide-react";
6 |
7 | export type Icon = LucideIcon | ComponentType>;
8 |
--------------------------------------------------------------------------------
/components/shared/icons/linkedin.tsx:
--------------------------------------------------------------------------------
1 | export default function LinkedIn({
2 | className,
3 | color = true,
4 | }: {
5 | className?: string;
6 | color?: boolean;
7 | }) {
8 | return (
9 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/shared/icons/menu.tsx:
--------------------------------------------------------------------------------
1 | export default function Menu({ className }: { className?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/icons/moon.tsx:
--------------------------------------------------------------------------------
1 | export default function Moon({ className }: { className?: string }) {
2 | return (
3 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/icons/more-horizontal.tsx:
--------------------------------------------------------------------------------
1 | export default function MoreHorizontal({ className }: { className?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/icons/more-vertical.tsx:
--------------------------------------------------------------------------------
1 | export default function MoreVertical({ className }: { className?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/shared/icons/passkey.tsx:
--------------------------------------------------------------------------------
1 | export default function Passkey({ className }: { className?: string }) {
2 | return (
3 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/components/shared/icons/pie-chart.tsx:
--------------------------------------------------------------------------------
1 | export default function PieChart({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/portrait-landscape.tsx:
--------------------------------------------------------------------------------
1 | export default function PortraitLandscape({
2 | className,
3 | fill,
4 | }: {
5 | className?: string;
6 | fill?: string;
7 | }) {
8 | return (
9 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/shared/icons/producthunt.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function ProductHuntIcon(
4 | props: React.JSX.IntrinsicAttributes & React.SVGProps,
5 | ) {
6 | return (
7 |
20 | );
21 | }
22 |
23 | export default ProductHuntIcon;
24 |
--------------------------------------------------------------------------------
/components/shared/icons/search.tsx:
--------------------------------------------------------------------------------
1 | export default function Search({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/settings.tsx:
--------------------------------------------------------------------------------
1 | export default function Settings({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/sparkle.tsx:
--------------------------------------------------------------------------------
1 | export default function Sparkle({ className }: { className?: string }) {
2 | return (
3 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/shared/icons/sun.tsx:
--------------------------------------------------------------------------------
1 | export default function Sun({ className }: { className?: string }) {
2 | return (
3 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/shared/icons/teams.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const TeamsIcon = () => {
4 | return (
5 |
19 | );
20 | };
21 |
22 | export default TeamsIcon;
23 |
--------------------------------------------------------------------------------
/components/shared/icons/user-round.tsx:
--------------------------------------------------------------------------------
1 | export default function UserRound({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/icons/x-circle.tsx:
--------------------------------------------------------------------------------
1 | export default function XCircle({
2 | className,
3 | fill,
4 | }: {
5 | className?: string;
6 | fill?: string;
7 | }) {
8 | return (
9 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/shared/icons/x.tsx:
--------------------------------------------------------------------------------
1 | export default function X({ className }: { className?: string }) {
2 | return (
3 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import {
6 | ThemeProvider as NextThemesProvider,
7 | type ThemeProviderProps,
8 | } from "next-themes";
9 |
10 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
11 | return {children};
12 | }
13 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
6 |
7 | import Check from "@/components/shared/icons/check";
8 |
9 | import { cn } from "@/lib/utils";
10 |
11 | const Checkbox = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 |
26 |
27 |
28 |
29 | ));
30 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
31 |
32 | export { Checkbox };
33 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2 |
3 | const Collapsible = CollapsiblePrimitive.Root;
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent };
10 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import { type VariantProps, cva } from "class-variance-authority";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const labelVariants = cva(
9 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
10 | );
11 |
12 | const Label = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef &
15 | VariantProps
16 | >(({ className, ...props }, ref) => (
17 |
22 | ));
23 | Label.displayName = LabelPrimitive.Root.displayName;
24 |
25 | export { Label };
26 |
--------------------------------------------------------------------------------
/components/ui/loading-dots.module.css:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: inline-flex;
3 | align-items: center;
4 | }
5 |
6 | .loading .spacer {
7 | margin-right: 2px;
8 | }
9 |
10 | .loading span {
11 | animation-name: blink;
12 | animation-duration: 1.4s;
13 | animation-iteration-count: infinite;
14 | animation-fill-mode: both;
15 | width: 5px;
16 | height: 5px;
17 | border-radius: 50%;
18 | display: inline-block;
19 | margin: 0 1px;
20 | }
21 |
22 | .loading span:nth-of-type(2) {
23 | animation-delay: 0.2s;
24 | }
25 |
26 | .loading span:nth-of-type(3) {
27 | animation-delay: 0.4s;
28 | }
29 |
30 | @keyframes blink {
31 | 0% {
32 | opacity: 0.2;
33 | }
34 | 20% {
35 | opacity: 1;
36 | }
37 | 100% {
38 | opacity: 0.2;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/components/ui/loading-dots.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./loading-dots.module.css";
2 |
3 | const LoadingDots = ({ color = "#000" }: { color?: string }) => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default LoadingDots;
14 |
--------------------------------------------------------------------------------
/components/ui/loading-spinner.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import styles from "./loading-spinner.module.css";
4 |
5 | export default function LoadingSpinner({ className }: { className?: string }) {
6 | return (
7 |
8 |
9 | {[...Array(12)].map((_, i) => (
10 |
11 | ))}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/components/ui/portal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import * as PortalPrimitive from "@radix-ui/react-portal";
4 |
5 | const Portal = ({
6 | containerId,
7 | className,
8 | children,
9 | }: {
10 | containerId?: string | null;
11 | children: React.ReactElement;
12 | className?: string;
13 | }) => {
14 | const [mounted, setMounted] = React.useState(false);
15 | React.useEffect(() => setMounted(true), []);
16 |
17 | return (
18 |
26 | {children}
27 |
28 | );
29 | };
30 | Portal.displayName = PortalPrimitive.Root.displayName;
31 |
32 | export { Portal };
33 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Separator = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(
13 | (
14 | { className, orientation = "horizontal", decorative = true, ...props },
15 | ref,
16 | ) => (
17 |
28 | ),
29 | );
30 | Separator.displayName = SeparatorPrimitive.Root.displayName;
31 |
32 | export { Separator };
33 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
15 | );
16 | }
17 |
18 | export { Skeleton };
19 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 |
6 | type ToasterProps = React.ComponentProps;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme();
10 |
11 | return (
12 |
30 | );
31 | };
32 |
33 | export { Toaster };
34 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Textarea.displayName = "Textarea";
24 |
25 | export { Textarea };
26 |
--------------------------------------------------------------------------------
/components/view/conversations/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | ConversationSidebarProps,
5 | ConversationViewSidebar as ConversationViewSidebarEE,
6 | } from "@/ee/features/conversations/components/conversation-view-sidebar";
7 |
8 | export function ConversationSidebar(props: ConversationSidebarProps) {
9 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/components/view/powered-by.tsx:
--------------------------------------------------------------------------------
1 | import { createPortal } from "react-dom";
2 |
3 | export const PoweredBy = ({ linkId }: { linkId: string }) => {
4 | return createPortal(
5 | ,
21 | document.body,
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/visitors/dataroom-visitor-custom-fields.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 |
3 | import useSWR from "swr";
4 |
5 | import { fetcher } from "@/lib/utils";
6 |
7 | type CustomFieldResponse = {
8 | identifier: string;
9 | label: string;
10 | response: string;
11 | };
12 |
13 | export default function VisitorCustomFields({
14 | viewId,
15 | teamId,
16 | dataroomId,
17 | }: {
18 | viewId: string;
19 | teamId: string;
20 | dataroomId: string;
21 | }) {
22 | const { data: customFieldResponse } = useSWR(
23 | `/api/teams/${teamId}/datarooms/${dataroomId}/views/${viewId}/custom-fields`,
24 | fetcher,
25 | );
26 |
27 | if (!customFieldResponse) return null;
28 |
29 | return (
30 |
31 |
32 | {customFieldResponse.map((field, index) => (
33 |
34 | - {field.label}
35 | - {field.response}
36 |
37 | ))}
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/components/visitors/dataroom-visitor-useragent.tsx:
--------------------------------------------------------------------------------
1 | import { useDataroomVisitorUserAgent } from "@/lib/swr/use-dataroom-stats";
2 |
3 | import VisitorUserAgentBase from "./visitor-useragent-base";
4 |
5 | export function DataroomVisitorUserAgent({ viewId }: { viewId: string }) {
6 | const { userAgent, error } = useDataroomVisitorUserAgent(viewId);
7 |
8 | if (error) {
9 | return null;
10 | }
11 |
12 | if (!userAgent) {
13 | return Loading...
;
14 | }
15 |
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/components/visitors/visitor-custom-fields.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 |
3 | import useSWR from "swr";
4 |
5 | import { fetcher } from "@/lib/utils";
6 |
7 | type CustomFieldResponse = {
8 | identifier: string;
9 | label: string;
10 | response: string;
11 | };
12 |
13 | export default function VisitorCustomFields({
14 | viewId,
15 | teamId,
16 | documentId,
17 | }: {
18 | viewId: string;
19 | teamId: string;
20 | documentId: string;
21 | }) {
22 | const { data: customFieldResponse } = useSWR(
23 | `/api/teams/${teamId}/documents/${documentId}/views/${viewId}/custom-fields`,
24 | fetcher,
25 | );
26 |
27 | if (!customFieldResponse) return null;
28 |
29 | return (
30 |
31 |
32 | {customFieldResponse.map((field, index) => (
33 |
34 | - {field.label}
35 | - {field.response}
36 |
37 | ))}
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/components/visitors/visitor-useragent.tsx:
--------------------------------------------------------------------------------
1 | import { useVisitorUserAgent } from "@/lib/swr/use-stats";
2 |
3 | import VisitorUserAgentBase from "./visitor-useragent-base";
4 |
5 | export default function VisitorUserAgent({ viewId }: { viewId: string }) {
6 | const { userAgent, error } = useVisitorUserAgent(viewId);
7 |
8 | if (error) {
9 | return null;
10 | }
11 |
12 | if (!userAgent) {
13 | return Loading...
;
14 | }
15 |
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/ee/features/conversations/components/conversation-message.tsx:
--------------------------------------------------------------------------------
1 | import { format } from "date-fns";
2 |
3 | export function ConversationMessage({
4 | message,
5 | isAuthor,
6 | senderEmail,
7 | }: {
8 | message: any;
9 | isAuthor: boolean;
10 | senderEmail: string;
11 | }) {
12 | return (
13 |
18 |
{message.content}
19 |
20 | {isAuthor ? "You" : message.userId ? "Admin" : senderEmail} •{" "}
21 | {format(new Date(message.createdAt), "MMM d, h:mm a")}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/ee/features/conversations/emails/lib/send-conversation-notification.ts:
--------------------------------------------------------------------------------
1 | import ConversationNotification from "@/ee/features/conversations/emails/components/conversation-notification";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendConversationNotification = async ({
6 | dataroomName,
7 | conversationTitle,
8 | senderEmail,
9 | to,
10 | url,
11 | unsubscribeUrl,
12 | }: {
13 | dataroomName: string;
14 | conversationTitle: string;
15 | senderEmail: string;
16 | to: string;
17 | url: string;
18 | unsubscribeUrl: string;
19 | }) => {
20 | try {
21 | await sendEmail({
22 | to: to,
23 | replyTo: senderEmail,
24 | subject: `New message in ${dataroomName}`,
25 | react: ConversationNotification({
26 | senderEmail,
27 | conversationTitle,
28 | dataroomName,
29 | url,
30 | unsubscribeUrl,
31 | }),
32 | test: process.env.NODE_ENV === "development",
33 | system: true,
34 | unsubscribeUrl,
35 | });
36 | } catch (e) {
37 | console.error(e);
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/ee/features/conversations/lib/api/notifications/index.ts:
--------------------------------------------------------------------------------
1 | import prisma from "@/lib/prisma";
2 |
3 | export const notificationService = {
4 | // Toggle notifications for a conversation
5 | async toggleNotificationsForConversation({
6 | conversationId,
7 | viewerId,
8 | enabled,
9 | }: {
10 | conversationId: string;
11 | viewerId: string;
12 | enabled: boolean;
13 | }) {
14 | return prisma.conversationParticipant.update({
15 | where: {
16 | conversationId_viewerId: { conversationId, viewerId },
17 | },
18 | data: { receiveNotifications: enabled },
19 | });
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/ee/stripe/client.ts:
--------------------------------------------------------------------------------
1 | // Stripe Client SDK
2 | import { Stripe as StripeProps, loadStripe } from "@stripe/stripe-js";
3 |
4 | let stripePromise: Promise;
5 |
6 | export const getStripe = (account: boolean = false) => {
7 | if (!stripePromise) {
8 | if (account) {
9 | stripePromise = loadStripe(
10 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE_OLD ??
11 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_OLD ??
12 | "",
13 | {
14 | apiVersion: "2024-06-20",
15 | },
16 | );
17 | } else {
18 | stripePromise = loadStripe(
19 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ??
20 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ??
21 | "",
22 | {
23 | apiVersion: "2024-06-20",
24 | },
25 | );
26 | }
27 | }
28 |
29 | return stripePromise;
30 | };
31 |
--------------------------------------------------------------------------------
/ee/stripe/functions/get-price-id-from-plan.ts:
--------------------------------------------------------------------------------
1 | import { PLANS, isOldAccount } from "../utils";
2 |
3 | export function getPriceIdFromPlan(
4 | planName: string,
5 | period: "monthly" | "yearly",
6 | ) {
7 | const env =
8 | process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ? "production" : "test";
9 | const accountType = isOldAccount(planName) ? "old" : "new";
10 | const cleanPlanName = planName.split("+")[0];
11 |
12 | const priceId = PLANS.find((p) => p.name === cleanPlanName)?.price[period]
13 | .priceIds[env][accountType];
14 | return priceId;
15 | }
16 |
--------------------------------------------------------------------------------
/ee/stripe/functions/get-quantity-from-plan.ts:
--------------------------------------------------------------------------------
1 | import { PLANS, getPlanFromPriceId } from "../utils";
2 |
3 | export function getQuantityFromPriceId(priceId?: string) {
4 | if (!priceId) {
5 | return 1;
6 | }
7 | const plan = getPlanFromPriceId(priceId);
8 | return plan.minQuantity ?? 1;
9 | }
10 |
--------------------------------------------------------------------------------
/ee/stripe/functions/get-subscription-item.ts:
--------------------------------------------------------------------------------
1 | import { stripeInstance } from "..";
2 |
3 | export default async function getSubscriptionItem(
4 | subscriptionId: string,
5 | isOldAccount: boolean,
6 | ) {
7 | const stripe = stripeInstance(isOldAccount);
8 | const subscription = await stripe.subscriptions.retrieve(subscriptionId);
9 | const subscriptionItemId = subscription.items.data[0].id;
10 | return subscriptionItemId;
11 | }
12 |
--------------------------------------------------------------------------------
/lib/api/auth/token.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from "crypto";
2 |
3 | export const hashToken = (
4 | token: string,
5 | {
6 | noSecret = false,
7 | }: {
8 | noSecret?: boolean;
9 | } = {},
10 | ) => {
11 | return createHash("sha256")
12 | .update(`${token}${noSecret ? "" : process.env.NEXTAUTH_SECRET}`)
13 | .digest("hex");
14 | };
15 |
--------------------------------------------------------------------------------
/lib/api/domains.ts:
--------------------------------------------------------------------------------
1 | import prisma from "@/lib/prisma";
2 |
3 | import { getApexDomain, removeDomainFromVercel } from "../domains";
4 |
5 | // calculate the domainCount
6 | export async function getDomainCount(domain: string) {
7 | const apexDomain = getApexDomain(`https://${domain}`);
8 | const response = await prisma.domain.count({
9 | where: {
10 | OR: [
11 | {
12 | slug: apexDomain,
13 | },
14 | {
15 | slug: {
16 | endsWith: `.${apexDomain}`,
17 | },
18 | },
19 | ],
20 | },
21 | });
22 |
23 | return response;
24 | }
25 |
26 | /* Delete a domain */
27 | export async function deleteDomain(
28 | domain: string,
29 | {
30 | // Note: in certain cases, we don't need to remove the domain from the Prisma
31 | skipPrismaDelete = false,
32 | } = {},
33 | ) {
34 | const domainCount = await getDomainCount(domain);
35 |
36 | return await Promise.allSettled([
37 | // remove the domain from Vercel
38 | removeDomainFromVercel(domain, domainCount),
39 | // delete domain
40 | !skipPrismaDelete &&
41 | prisma.domain.delete({
42 | where: {
43 | slug: domain,
44 | },
45 | }),
46 | ]);
47 | }
48 |
--------------------------------------------------------------------------------
/lib/cron/index.ts:
--------------------------------------------------------------------------------
1 | import { Receiver } from "@upstash/qstash";
2 | import { Client } from "@upstash/qstash";
3 | import Bottleneck from "bottleneck";
4 |
5 | // we're using Bottleneck to avoid running into Resend's rate limit of 10 req/s
6 | export const limiter = new Bottleneck({
7 | maxConcurrent: 1, // maximum concurrent requests
8 | minTime: 100, // minimum time between requests in ms
9 | });
10 |
11 | // we're using Upstash's Receiver to verify the request signature
12 | export const receiver = new Receiver({
13 | currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY || "",
14 | nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY || "",
15 | });
16 |
17 | export const qstash = new Client({
18 | token: process.env.QSTASH_TOKEN || "",
19 | });
20 |
--------------------------------------------------------------------------------
/lib/cron/verify-qstash.ts:
--------------------------------------------------------------------------------
1 | import { receiver } from ".";
2 | import { log } from "../utils";
3 |
4 | export const verifyQstashSignature = async ({
5 | req,
6 | rawBody,
7 | }: {
8 | req: Request;
9 | rawBody: string; // Make sure to pass the raw body not the parsed JSON
10 | }) => {
11 | // skip verification in local development
12 | if (process.env.VERCEL !== "1") {
13 | return;
14 | }
15 |
16 | const signature = req.headers.get("Upstash-Signature");
17 |
18 | if (!signature) {
19 | throw new Error("Upstash-Signature header not found.");
20 | }
21 |
22 | const isValid = await receiver.verify({
23 | signature,
24 | body: rawBody,
25 | });
26 |
27 | if (!isValid) {
28 | const url = req.url;
29 | const messageId = req.headers.get("Upstash-Message-Id");
30 |
31 | log({
32 | message: `Invalid QStash request signature: *${url}* - *${messageId}*`,
33 | type: "error",
34 | mention: true,
35 | });
36 |
37 | throw new Error("Invalid QStash request signature.");
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/lib/edge-config/blacklist.ts:
--------------------------------------------------------------------------------
1 | import { get } from "@vercel/edge-config";
2 |
3 | export const isBlacklistedEmail = async (email: string) => {
4 | if (!process.env.EDGE_CONFIG) {
5 | return false;
6 | }
7 |
8 | let blacklistedEmails: string[] = [];
9 | try {
10 | const result = await get("emails");
11 | // Make sure we only use string arrays
12 | blacklistedEmails = Array.isArray(result)
13 | ? result.filter((item): item is string => typeof item === "string")
14 | : [];
15 | } catch (e) {
16 | // Already initialized as empty array
17 | }
18 |
19 | if (blacklistedEmails.length === 0) return false;
20 | return new RegExp(blacklistedEmails.join("|"), "i").test(email);
21 | };
22 |
--------------------------------------------------------------------------------
/lib/edge-config/custom-email.ts:
--------------------------------------------------------------------------------
1 | import { get } from "@vercel/edge-config";
2 |
3 | export const getCustomEmail = async (teamId?: string) => {
4 | if (!process.env.EDGE_CONFIG || !teamId) {
5 | return null;
6 | }
7 |
8 | let customEmails: Record = {};
9 | try {
10 | const result = await get("customEmail");
11 | // Make sure we get a valid object
12 | customEmails =
13 | typeof result === "object" && result !== null
14 | ? (result as Record)
15 | : {};
16 | } catch (e) {
17 | // Error getting custom emails, return null
18 | return null;
19 | }
20 |
21 | // Return the custom email for the team if it exists
22 | return customEmails[teamId] || null;
23 | };
24 |
--------------------------------------------------------------------------------
/lib/emails/send-dataroom-info.ts:
--------------------------------------------------------------------------------
1 | import Onboarding5Email from "@/components/emails/onboarding-5";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | import { CreateUserEmailProps } from "../types";
6 |
7 | export const sendDataroomInfoEmail = async (params: CreateUserEmailProps) => {
8 | const { email } = params.user;
9 |
10 | let emailTemplate;
11 | let subject;
12 |
13 | emailTemplate = Onboarding5Email();
14 | subject = "Virtual Data Rooms";
15 |
16 | try {
17 | await sendEmail({
18 | to: email as string,
19 | subject,
20 | react: emailTemplate,
21 | test: process.env.NODE_ENV === "development",
22 | });
23 | } catch (e) {
24 | console.error(e);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/lib/emails/send-dataroom-notification.ts:
--------------------------------------------------------------------------------
1 | import DataroomNotification from "@/components/emails/dataroom-notification";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendDataroomNotification = async ({
6 | dataroomName,
7 | documentName,
8 | senderEmail,
9 | to,
10 | url,
11 | unsubscribeUrl,
12 | }: {
13 | dataroomName: string;
14 | documentName: string | undefined;
15 | senderEmail: string;
16 | to: string;
17 | url: string;
18 | unsubscribeUrl: string;
19 | }) => {
20 | try {
21 | await sendEmail({
22 | to: to,
23 | subject: `New document available in ${dataroomName}`,
24 | react: DataroomNotification({
25 | senderEmail,
26 | dataroomName,
27 | documentName,
28 | url,
29 | unsubscribeUrl,
30 | }),
31 | test: process.env.NODE_ENV === "development",
32 | system: true,
33 | unsubscribeUrl,
34 | });
35 | } catch (e) {
36 | console.error(e);
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/lib/emails/send-dataroom-trial-end.ts:
--------------------------------------------------------------------------------
1 | import DataroomTrialEnd from "@/components/emails/dataroom-trial-end";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendDataroomTrialEndEmail = async (params: {
6 | email: string;
7 | name: string;
8 | }) => {
9 | const { email, name } = params;
10 |
11 | let emailTemplate;
12 | let subject;
13 |
14 | emailTemplate = DataroomTrialEnd({ name });
15 | subject = "Your dataroom trial has ended";
16 |
17 | try {
18 | await sendEmail({
19 | to: email as string,
20 | subject,
21 | react: emailTemplate,
22 | test: process.env.NODE_ENV === "development",
23 | });
24 | } catch (e) {
25 | console.error(e);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/lib/emails/send-dataroom-trial.ts:
--------------------------------------------------------------------------------
1 | import DataroomTrialWelcome from "@/components/emails/dataroom-trial-welcome";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendDataroomTrialWelcome = async ({
6 | fullName,
7 | to,
8 | }: {
9 | fullName: string;
10 | to: string;
11 | }) => {
12 | // Schedule the email to be sent 6 minutes from now
13 | const sixMinuteFromNow = new Date(Date.now() + 1000 * 60 * 6).toISOString();
14 |
15 | // get the first name from the full name
16 | const name = fullName.split(" ")[0];
17 |
18 | try {
19 | await sendEmail({
20 | to: to,
21 | subject: `For ${name}`,
22 | react: DataroomTrialWelcome({ name }),
23 | test: process.env.NODE_ENV === "development",
24 | scheduledAt: sixMinuteFromNow,
25 | });
26 | } catch (e) {
27 | console.error(e);
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/lib/emails/send-dataroom-viewer-invite.ts:
--------------------------------------------------------------------------------
1 | import DataroomViewerInvitation from "@/components/emails/dataroom-viewer-invitation";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendDataroomViewerInvite = async ({
6 | dataroomName,
7 | senderEmail,
8 | to,
9 | url,
10 | }: {
11 | dataroomName: string;
12 | senderEmail: string;
13 | to: string;
14 | url: string;
15 | }) => {
16 | try {
17 | await sendEmail({
18 | to: to,
19 | subject: `You are invited to view ${dataroomName}`,
20 | react: DataroomViewerInvitation({
21 | senderEmail,
22 | dataroomName,
23 | url,
24 | }),
25 | test: process.env.NODE_ENV === "development",
26 | system: true,
27 | });
28 | } catch (e) {
29 | console.error(e);
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/lib/emails/send-deleted-domain.ts:
--------------------------------------------------------------------------------
1 | import DeletedDomainEmail from "@/components/emails/deleted-domain";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendDeletedDomainEmail = async (email: string, domain: string) => {
6 | const emailTemplate = DeletedDomainEmail({ domain });
7 | try {
8 | await sendEmail({
9 | to: email,
10 | subject: `Your domain ${domain} has been deleted`,
11 | react: emailTemplate,
12 | test: process.env.NODE_ENV === "development",
13 | system: true,
14 | });
15 | } catch (e) {
16 | console.error(e);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/lib/emails/send-email-otp-verification.ts:
--------------------------------------------------------------------------------
1 | import { getCustomEmail } from "@/lib/edge-config/custom-email";
2 | import prisma from "@/lib/prisma";
3 | import { redis } from "@/lib/redis";
4 | import { sendEmail } from "@/lib/resend";
5 |
6 | import OtpEmailVerification from "@/components/emails/otp-verification";
7 |
8 | export const sendOtpVerificationEmail = async (
9 | email: string,
10 | code: string,
11 | isDataroom: boolean = false,
12 | teamId: string,
13 | ) => {
14 | let logo: string | null = null;
15 | let from: string | undefined;
16 |
17 | const customEmail = await getCustomEmail(teamId);
18 |
19 | if (customEmail && teamId) {
20 | from = customEmail;
21 | logo = await redis.get(`brand:logo:${teamId}`);
22 | }
23 |
24 | const emailTemplate = OtpEmailVerification({
25 | email,
26 | code,
27 | isDataroom,
28 | logo: logo ?? undefined,
29 | });
30 |
31 | try {
32 | await sendEmail({
33 | from,
34 | to: email,
35 | subject: `One-time passcode to access the ${isDataroom ? "dataroom" : "document"}`,
36 | react: emailTemplate,
37 | test: process.env.NODE_ENV === "development",
38 | verify: true,
39 | });
40 | } catch (e) {
41 | console.error(e);
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/lib/emails/send-email-verification.ts:
--------------------------------------------------------------------------------
1 | import EmailVerification from "@/components/emails/email-verification";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendVerificationEmail = async (
6 | email: string,
7 | verificationURL: string,
8 | isDataroom: boolean = false,
9 | ) => {
10 | const emailTemplate = EmailVerification({
11 | verificationURL,
12 | email,
13 | isDataroom,
14 | });
15 | try {
16 | await sendEmail({
17 | to: email,
18 | subject: `Verify your email address to access the ${isDataroom ? "dataroom" : "document"}`,
19 | react: emailTemplate,
20 | test: process.env.NODE_ENV === "development",
21 | verify: true,
22 | });
23 | } catch (e) {
24 | console.error(e);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/lib/emails/send-invalid-domain.ts:
--------------------------------------------------------------------------------
1 | import InvalidDomainEmail from "@/components/emails/invalid-domain";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendInvalidDomainEmail = async (
6 | email: string,
7 | domain: string,
8 | invalidDays: number,
9 | ) => {
10 | const emailTemplate = InvalidDomainEmail({ domain, invalidDays });
11 | try {
12 | await sendEmail({
13 | to: email,
14 | subject: `Your domain ${domain} needs to be configured`,
15 | react: emailTemplate,
16 | test: process.env.NODE_ENV === "development",
17 | system: true,
18 | });
19 | } catch (e) {
20 | console.error(e);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/lib/emails/send-mail-verification.ts:
--------------------------------------------------------------------------------
1 | import ConfirmEmailChange from "@/components/emails/verification-email-change";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendEmailChangeVerificationRequestEmail = async (params: {
6 | email: string;
7 | url: string;
8 | newEmail: string;
9 | }) => {
10 | const { url, email, newEmail } = params;
11 |
12 | const emailTemplate = ConfirmEmailChange({
13 | confirmUrl: url,
14 | email,
15 | newEmail,
16 | });
17 |
18 | try {
19 | await sendEmail({
20 | to: email,
21 | system: true,
22 | subject: "Confirm your email address change for Papermark!",
23 | react: emailTemplate,
24 | test: process.env.NODE_ENV === "development",
25 | });
26 | } catch (e) {
27 | console.error(e);
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/lib/emails/send-teammate-invite.ts:
--------------------------------------------------------------------------------
1 | import TeamInvitation from "@/components/emails/team-invitation";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendTeammateInviteEmail = async ({
6 | senderName,
7 | senderEmail,
8 | teamName,
9 | to,
10 | url,
11 | }: {
12 | senderName: string;
13 | senderEmail: string;
14 | teamName: string;
15 | to: string;
16 | url: string;
17 | }) => {
18 | try {
19 | await sendEmail({
20 | to: to,
21 | subject: `You are invited to join team`,
22 | react: TeamInvitation({
23 | senderName,
24 | senderEmail,
25 | teamName,
26 | url,
27 | }),
28 | test: process.env.NODE_ENV === "development",
29 | system: true,
30 | });
31 | } catch (e) {
32 | console.error(e);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/lib/emails/send-trial-end-final-reminder.ts:
--------------------------------------------------------------------------------
1 | import TrialEndFinalReminderEmail from "@/components/emails/trial-end-final-reminder";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendTrialEndFinalReminderEmail = async (
6 | email: string,
7 | name: string | null,
8 | ) => {
9 | const emailTemplate = TrialEndFinalReminderEmail({ name });
10 | try {
11 | await sendEmail({
12 | to: email,
13 | subject: `Your pro trial expires in 24 hours`,
14 | react: emailTemplate,
15 | test: process.env.NODE_ENV === "development",
16 | system: true,
17 | });
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/lib/emails/send-trial-end-reminder.ts:
--------------------------------------------------------------------------------
1 | import TrialEndReminderEmail from "@/components/emails/trial-end-reminder";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | export const sendTrialEndReminderEmail = async (
6 | email: string,
7 | name: string | null,
8 | ) => {
9 | const emailTemplate = TrialEndReminderEmail({ name });
10 | try {
11 | await sendEmail({
12 | to: email,
13 | subject: `Your pro trial is ending soon`,
14 | react: emailTemplate,
15 | test: process.env.NODE_ENV === "development",
16 | system: true,
17 | });
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/lib/emails/send-upgrade-plan.ts:
--------------------------------------------------------------------------------
1 | import UpgradePlanEmail from "@/components/emails/upgrade-plan";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 | import { CreateUserEmailProps } from "@/lib/types";
5 |
6 | export const sendUpgradePlanEmail = async (
7 | params: CreateUserEmailProps & { planType: string },
8 | ) => {
9 | const { name, email } = params.user;
10 | const { planType } = params;
11 | const emailTemplate = UpgradePlanEmail({ name, planType });
12 | try {
13 | await sendEmail({
14 | to: email as string,
15 | subject: `Thank you for upgrading to Papermark ${planType}!`,
16 | react: emailTemplate,
17 | test: process.env.NODE_ENV === "development",
18 | });
19 | } catch (e) {
20 | console.error(e);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/lib/emails/send-verification-request.ts:
--------------------------------------------------------------------------------
1 | import LoginLink from "@/components/emails/verification-link";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | import { generateChecksum } from "../utils/generate-checksum";
6 |
7 | export const sendVerificationRequestEmail = async (params: {
8 | email: string;
9 | url: string;
10 | }) => {
11 | const { url, email } = params;
12 | const checksum = generateChecksum(url);
13 | const verificationUrlParams = new URLSearchParams({
14 | verification_url: url,
15 | checksum,
16 | });
17 |
18 | const verificationUrl = `${process.env.NEXTAUTH_URL}/verify?${verificationUrlParams}`;
19 | const emailTemplate = LoginLink({ url: verificationUrl });
20 | try {
21 | await sendEmail({
22 | to: email as string,
23 | subject: "Welcome to Papermark!",
24 | react: emailTemplate,
25 | test: process.env.NODE_ENV === "development",
26 | });
27 | } catch (e) {
28 | console.error(e);
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/lib/emails/send-welcome.ts:
--------------------------------------------------------------------------------
1 | import WelcomeEmail from "@/components/emails/welcome";
2 |
3 | import { sendEmail } from "@/lib/resend";
4 |
5 | import { CreateUserEmailProps } from "../types";
6 |
7 | export const sendWelcomeEmail = async (params: CreateUserEmailProps) => {
8 | const { name, email } = params.user;
9 | const emailTemplate = WelcomeEmail({ name });
10 | try {
11 | await sendEmail({
12 | to: email as string,
13 | subject: "Welcome to Papermark!",
14 | react: emailTemplate,
15 | test: process.env.NODE_ENV === "development",
16 | });
17 | } catch (e) {
18 | console.error(e);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/lib/errorHandler.ts:
--------------------------------------------------------------------------------
1 | import { NextApiResponse } from "next";
2 |
3 | export function errorhandler(err: unknown, res: NextApiResponse) {
4 | if (err instanceof TeamError || err instanceof DocumentError) {
5 | return res.status(err.statusCode).end(err.message);
6 | } else {
7 | return res.status(500).json({
8 | message: "Internal Server Error",
9 | error: (err as Error).message,
10 | });
11 | }
12 | }
13 |
14 | export class TeamError extends Error {
15 | statusCode = 400;
16 | constructor(public message: string) {
17 | super(message);
18 | }
19 | }
20 |
21 | export class DocumentError extends Error {
22 | statusCode = 400;
23 | constructor(public message: string) {
24 | super(message);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/hanko.ts:
--------------------------------------------------------------------------------
1 | import { tenant } from "@teamhanko/passkeys-next-auth-provider";
2 |
3 | if (!process.env.HANKO_API_KEY || !process.env.NEXT_PUBLIC_HANKO_TENANT_ID) {
4 | // These need to be set in .env.local
5 | // You get them from the Passkey API itself, e.g. when first setting up the server.
6 | throw new Error(
7 | "Please set HANKO_API_KEY and NEXT_PUBLIC_HANKO_TENANT_ID in your .env.local file.",
8 | );
9 | }
10 |
11 | const hanko = tenant({
12 | apiKey: process.env.HANKO_API_KEY!,
13 | tenantId: process.env.NEXT_PUBLIC_HANKO_TENANT_ID!,
14 | });
15 |
16 | export default hanko;
17 |
--------------------------------------------------------------------------------
/lib/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/lib/middleware/incoming-webhooks.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | export default async function IncomingWebhookMiddleware(req: NextRequest) {
4 | const url = req.nextUrl.clone();
5 | const path = url.pathname;
6 |
7 | // Only handle /services/* paths
8 | if (path.startsWith("/services/")) {
9 | // Rewrite to /api/webhooks/services/*
10 | url.pathname = `/api/webhooks${path}`;
11 |
12 | return NextResponse.rewrite(url);
13 | }
14 |
15 | // Return 404 for all other paths
16 | url.pathname = "/404";
17 | return NextResponse.rewrite(url, { status: 404 });
18 | }
19 |
20 | export function isWebhookPath(host: string | null) {
21 | if (!process.env.NEXT_PUBLIC_WEBHOOK_BASE_HOST) {
22 | return false;
23 | }
24 |
25 | if (host === process.env.NEXT_PUBLIC_WEBHOOK_BASE_HOST) {
26 | return true;
27 | }
28 |
29 | return false;
30 | }
31 |
--------------------------------------------------------------------------------
/lib/middleware/posthog.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | export default async function PostHogMiddleware(req: NextRequest) {
4 | let url = req.nextUrl.clone();
5 | const hostname = url.pathname.startsWith("/ingest/static/")
6 | ? "eu-assets.i.posthog.com"
7 | : "eu.i.posthog.com";
8 | const requestHeaders = new Headers(req.headers);
9 | requestHeaders.set("host", hostname);
10 |
11 | // Handle OPTIONS method for CORS preflight
12 | if (req.method === "OPTIONS") {
13 | return new NextResponse("", {
14 | status: 200,
15 | });
16 | }
17 |
18 | url.protocol = "https";
19 | url.hostname = hostname;
20 | url.port = "443";
21 | url.pathname = url.pathname.replace(/^\/ingest/, "");
22 |
23 | return NextResponse.rewrite(url, {
24 | headers: requestHeaders,
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/lib/notion/config.ts:
--------------------------------------------------------------------------------
1 | export const isDev =
2 | process.env.NODE_ENV === "development" || !process.env.NODE_ENV;
3 |
--------------------------------------------------------------------------------
/lib/notion/index.ts:
--------------------------------------------------------------------------------
1 | import { NotionAPI } from "notion-client";
2 |
3 | const notion = new NotionAPI();
4 | export default notion;
5 |
--------------------------------------------------------------------------------
/lib/openai.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 |
3 | // Create an OpenAI API client (that's edge friendly!)
4 | export const openai = new OpenAI({
5 | apiKey: process.env.OPENAI_API_KEY || "",
6 | });
7 |
--------------------------------------------------------------------------------
/lib/posthog.ts:
--------------------------------------------------------------------------------
1 | export function getPostHogConfig(): { key: string; host: string } | null {
2 | const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
3 | const postHogHost = `${process.env.NEXT_PUBLIC_BASE_URL}/ingest`;
4 |
5 | if (!postHogKey || !postHogHost) {
6 | return null;
7 | }
8 |
9 | return {
10 | key: postHogKey,
11 | host: postHogHost,
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined;
5 | }
6 |
7 | const prisma = global.prisma || new PrismaClient();
8 |
9 | if (process.env.NODE_ENV === "development") global.prisma = prisma;
10 |
11 | export default prisma;
12 |
--------------------------------------------------------------------------------
/lib/redis.ts:
--------------------------------------------------------------------------------
1 | import { Ratelimit } from "@upstash/ratelimit";
2 | import { Redis } from "@upstash/redis";
3 |
4 | export const redis = new Redis({
5 | url: process.env.UPSTASH_REDIS_REST_URL as string,
6 | token: process.env.UPSTASH_REDIS_REST_TOKEN as string,
7 | });
8 |
9 | export const lockerRedisClient = new Redis({
10 | url: process.env.UPSTASH_REDIS_REST_LOCKER_URL as string,
11 | token: process.env.UPSTASH_REDIS_REST_LOCKER_TOKEN as string,
12 | });
13 |
14 | // Create a new ratelimiter, that allows 10 requests per 10 seconds by default
15 | export const ratelimit = (
16 | requests: number = 10,
17 | seconds:
18 | | `${number} ms`
19 | | `${number} s`
20 | | `${number} m`
21 | | `${number} h`
22 | | `${number} d` = "10 s",
23 | ) => {
24 | return new Ratelimit({
25 | redis: redis,
26 | limiter: Ratelimit.slidingWindow(requests, seconds),
27 | analytics: true,
28 | prefix: "papermark",
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/lib/swr/use-agreements.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import { Agreement } from "@prisma/client";
3 | import useSWR from "swr";
4 |
5 | import { fetcher } from "@/lib/utils";
6 |
7 | export interface AgreementWithLinksCount extends Agreement {
8 | _count: {
9 | links: number;
10 | };
11 | }
12 |
13 | export function useAgreements() {
14 | const teamInfo = useTeam();
15 | const teamId = teamInfo?.currentTeam?.id;
16 |
17 | const { data: agreements, error } = useSWR(
18 | teamId && `/api/teams/${teamId}/agreements`,
19 | fetcher,
20 | {
21 | dedupingInterval: 60000,
22 | },
23 | );
24 |
25 | return {
26 | agreements: agreements || [],
27 | loading: !agreements && !error,
28 | error,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/lib/swr/use-brand.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | import { useMemo } from "react";
4 |
5 | import { useTeam } from "@/context/team-context";
6 | import { Brand, DataroomBrand } from "@prisma/client";
7 | import useSWR from "swr";
8 |
9 | import { fetcher } from "@/lib/utils";
10 |
11 | export function useBrand() {
12 | const teamInfo = useTeam();
13 |
14 | const { data: brand, error } = useSWR(
15 | teamInfo?.currentTeam?.id &&
16 | `/api/teams/${teamInfo?.currentTeam?.id}/branding`,
17 | fetcher,
18 | {
19 | dedupingInterval: 30000,
20 | },
21 | );
22 |
23 | return {
24 | brand,
25 | error,
26 | loading: !brand && !error,
27 | };
28 | }
29 |
30 | export function useDataroomBrand({
31 | dataroomId,
32 | }: {
33 | dataroomId: string | undefined;
34 | }) {
35 | const teamInfo = useTeam();
36 | const teamId = teamInfo?.currentTeam?.id;
37 |
38 | const { data: brand, error } = useSWR(
39 | teamId &&
40 | dataroomId &&
41 | `/api/teams/${teamId}/datarooms/${dataroomId}/branding`,
42 | fetcher,
43 | {
44 | dedupingInterval: 30000,
45 | },
46 | );
47 |
48 | return {
49 | brand,
50 | error,
51 | loading: !brand && !error,
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/lib/swr/use-dataroom-document-stats.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | import { useTeam } from "@/context/team-context";
4 | import useSWR from "swr";
5 |
6 | import { TStatsData } from "@/lib/swr/use-stats";
7 | import { fetcher } from "@/lib/utils";
8 |
9 | type TDataroomDocumentStats = TStatsData & {
10 | totalPagesMax: number;
11 | };
12 |
13 | export function useDataroomDocumentStats(
14 | documentId: string | null | undefined,
15 | ) {
16 | const { currentTeamId: teamId } = useTeam();
17 | const router = useRouter();
18 | const { id: dataroomId } = router.query as { id: string };
19 |
20 | const { data: stats, error } = useSWR(
21 | documentId && teamId && dataroomId
22 | ? `/api/teams/${teamId}/datarooms/${dataroomId}/documents/${encodeURIComponent(documentId)}/stats`
23 | : null,
24 | fetcher,
25 | {
26 | dedupingInterval: 10000,
27 | },
28 | );
29 |
30 | return {
31 | stats,
32 | loading: documentId ? !error && !stats : false,
33 | error,
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/lib/swr/use-datarooms.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import { Dataroom } from "@prisma/client";
3 | import useSWR from "swr";
4 |
5 | import { fetcher } from "@/lib/utils";
6 |
7 | export type DataroomWithCount = Dataroom & {
8 | _count: {
9 | documents: number;
10 | views: number;
11 | };
12 | };
13 |
14 | export default function useDatarooms() {
15 | const teamInfo = useTeam();
16 |
17 | const { data: datarooms, error } = useSWR(
18 | teamInfo?.currentTeam?.id &&
19 | `/api/teams/${teamInfo?.currentTeam?.id}/datarooms`,
20 | fetcher,
21 | {
22 | revalidateOnFocus: false,
23 | dedupingInterval: 30000,
24 | },
25 | );
26 |
27 | return {
28 | datarooms,
29 | loading: !datarooms && !error,
30 | error,
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/lib/swr/use-document-stats.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import { View } from "@prisma/client";
3 | import useSWR from "swr";
4 |
5 | import { TStatsData } from "@/lib/swr/use-stats";
6 | import { fetcher } from "@/lib/utils";
7 |
8 | export function useDocumentStats(documentId: string | null | undefined) {
9 | const { currentTeamId: teamId } = useTeam();
10 |
11 | const { data: stats, error } = useSWR(
12 | documentId && teamId
13 | ? `/api/teams/${teamId}/documents/${encodeURIComponent(documentId)}/stats`
14 | : null,
15 | fetcher,
16 | {
17 | dedupingInterval: 10000,
18 | },
19 | );
20 |
21 | return {
22 | stats,
23 | loading: documentId ? !error && !stats : false,
24 | error,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/lib/swr/use-domains.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import { Domain } from "@prisma/client";
3 | import useSWR from "swr";
4 |
5 | import { fetcher } from "@/lib/utils";
6 |
7 | export function useDomains() {
8 | const teamInfo = useTeam();
9 |
10 | const { data: domains, error } = useSWR(
11 | teamInfo?.currentTeam?.id ? `/api/teams/${teamInfo.currentTeam.id}/domains` : null,
12 | fetcher,
13 | {
14 | dedupingInterval: 60000,
15 | },
16 | );
17 |
18 | return {
19 | domains,
20 | loading: !domains && !error,
21 | error,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/lib/swr/use-folders.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import useSWR from "swr";
3 |
4 | import { fetcher } from "../utils";
5 |
6 | type FolderWithParents = {
7 | id: string;
8 | name: string;
9 | parentId: string | null;
10 | teamId: string;
11 | _count: {
12 | documents: number;
13 | childFolders: number;
14 | };
15 | parent: FolderWithParents | null;
16 | };
17 |
18 | export function useFolderWithParents({ name }: { name: string[] }) {
19 | const teamInfo = useTeam();
20 |
21 | const { data: folders, error } = useSWR<{ name: string; path: string }[]>(
22 | teamInfo?.currentTeam?.id &&
23 | name && !!name.length &&
24 | `/api/teams/${teamInfo?.currentTeam?.id}/folders/parents/${name.join("/")}`,
25 | fetcher,
26 | {
27 | revalidateOnFocus: false,
28 | dedupingInterval: 30000,
29 | },
30 | );
31 |
32 | return {
33 | folders,
34 | loading: !folders && !error,
35 | error,
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/lib/swr/use-invitations.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import { Invitation } from "@prisma/client";
3 | import useSWR from "swr";
4 |
5 | import { fetcher } from "@/lib/utils";
6 |
7 | export function useInvitations() {
8 | const teamInfo = useTeam();
9 |
10 | // only fetch data once when linkId is present
11 | const { data: invitations, error } = useSWR(
12 | teamInfo?.currentTeam &&
13 | `/api/teams/${teamInfo.currentTeam.id}/invitations`,
14 | fetcher,
15 | {
16 | dedupingInterval: 10000,
17 | },
18 | );
19 |
20 | return {
21 | invitations,
22 | loading: !error && !invitations,
23 | error,
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/lib/swr/use-limits.ts:
--------------------------------------------------------------------------------
1 | import { useLimits } from "@/ee/limits/swr-handler";
2 |
3 | export default useLimits;
4 |
--------------------------------------------------------------------------------
/lib/swr/use-team.ts:
--------------------------------------------------------------------------------
1 | import { useTeam } from "@/context/team-context";
2 | import useSWR from "swr";
3 |
4 | import { TeamDetail } from "@/lib/types";
5 | import { fetcher } from "@/lib/utils";
6 |
7 | export function useGetTeam() {
8 | const { currentTeamId } = useTeam();
9 |
10 | const { data: team, error } = useSWR(
11 | currentTeamId && `/api/teams/${currentTeamId}`,
12 | fetcher,
13 | {
14 | dedupingInterval: 20000,
15 | },
16 | );
17 |
18 | return {
19 | team,
20 | loading: team ? false : true,
21 | error,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/lib/swr/use-teams.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | import { useSession } from "next-auth/react";
4 | import useSWR from "swr";
5 |
6 | import { Team } from "@/lib/types";
7 | import { fetcher } from "@/lib/utils";
8 |
9 | export function useTeams() {
10 | const router = useRouter();
11 | const { data: session } = useSession();
12 |
13 | const { data: teams, isValidating } = useSWR(
14 | router.isReady && session && "/api/teams",
15 | fetcher,
16 | {
17 | dedupingInterval: 20000,
18 | },
19 | );
20 |
21 | return {
22 | teams,
23 | loading: teams ? false : true,
24 | isValidating,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/lib/swr/use-viewer.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | import { useTeam } from "@/context/team-context";
4 | import { View, Viewer } from "@prisma/client";
5 | import useSWR from "swr";
6 |
7 | import { fetcher } from "@/lib/utils";
8 |
9 | type ViewerWithViews = Viewer & {
10 | views: {
11 | documentId: string;
12 | viewCount: number;
13 | viewIds: string[];
14 | lastViewed: Date;
15 | }[];
16 | };
17 |
18 | export default function useViewer() {
19 | const router = useRouter();
20 | const teamInfo = useTeam();
21 | const teamId = teamInfo?.currentTeam?.id;
22 |
23 | const { id } = router.query;
24 |
25 | const { data: viewer, error } = useSWR(
26 | teamId && id && `/api/teams/${teamId}/viewers/${id}`,
27 | fetcher,
28 | );
29 |
30 | return {
31 | viewer,
32 | loading: !viewer && !error,
33 | error,
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/lib/swr/use-viewers.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | import { useTeam } from "@/context/team-context";
4 | import { Viewer } from "@prisma/client";
5 | import useSWR from "swr";
6 |
7 | import { fetcher } from "@/lib/utils";
8 |
9 | export default function useViewers() {
10 | const router = useRouter();
11 | const teamInfo = useTeam();
12 | const teamId = teamInfo?.currentTeam?.id;
13 |
14 | const queryParams = router.query;
15 | const searchQuery = queryParams["search"];
16 |
17 | const {
18 | data: viewers,
19 | isValidating,
20 | error,
21 | } = useSWR(
22 | teamId &&
23 | `/api/teams/${teamId}/viewers${searchQuery ? `?query=${searchQuery}` : ""}`,
24 | fetcher,
25 | {
26 | revalidateOnFocus: false,
27 | dedupingInterval: 30000,
28 | keepPreviousData: true,
29 | },
30 | );
31 |
32 | return {
33 | viewers,
34 | isValidating,
35 | loading: !viewers && !error,
36 | isFiltered: !!searchQuery,
37 | error,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/lib/tinybird/README.md:
--------------------------------------------------------------------------------
1 | ## Add new pipes to Tinybird
2 |
3 | So you added a new pipe to Tinybird and want to push that to the server.
4 |
5 | ```sh
6 | tb push lib/tinybird/endpoints/.pipe
7 | ```
8 |
9 | ## Danger Zone
10 |
11 | ### Delete a data from datasource
12 |
13 | ```sh
14 | tb datasource delete page_views__v3 --dry-run --sql-condition "viewId='VIEWID' and CAST(pageNumber AS UInt8) = PAGENUMBER" --wait
15 | ```
16 |
--------------------------------------------------------------------------------
/lib/tinybird/datasources/click_events.datasource:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | DESCRIPTION >
4 | Click events track when a user clicks a link within a document
5 |
6 | SCHEMA >
7 | `timestamp` DateTime64(3) `json:$.timestamp`,
8 | `event_id` String `json:$.event_id`,
9 | `session_id` String `json:$.session_id`,
10 | `link_id` String `json:$.link_id`,
11 | `document_id` String `json:$.document_id`,
12 | `dataroom_id` Nullable(String) `json:$.dataroom_id`,
13 | `view_id` String `json:$.view_id`,
14 | `page_number` LowCardinality(String) `json:$.page_number`,
15 | `version_number` UInt16 `json:$.version_number`,
16 | `href` String `json:$.href`
17 |
18 | ENGINE "MergeTree"
19 | ENGINE_SORTING_KEY "document_id,view_id,link_id,timestamp"
20 | ENGINE_PARTITION_KEY "toYYYYMM(timestamp)"
--------------------------------------------------------------------------------
/lib/tinybird/datasources/webhook_events.datasource:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | DESCRIPTION >
4 | Webhook events are events when a webhook is triggered
5 |
6 | SCHEMA >
7 | `timestamp` DateTime64(3) `json:$.timestamp` DEFAULT now(),
8 | `event_id` String `json:$.event_id`,
9 | `webhook_id` String `json:$.webhook_id`,
10 | `url` String `json:$.url`,
11 | `event` LowCardinality(String) `json:$.event`,
12 | `http_status` UInt16 `json:$.http_status`,
13 | `request_body` String `json:$.request_body`,
14 | `response_body` String `json:$.response_body`,
15 | `message_id` String `json:$.message_id`
16 |
17 | ENGINE "MergeTree"
18 | ENGINE_PARTITION_KEY "toYYYYMM(timestamp)"
19 | ENGINE_SORTING_KEY "timestamp, webhook_id, event_id"
20 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_click_events_by_view.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | DESCRIPTION >
4 | Get all click events for a specific view
5 |
6 | NODE endpoint
7 | SQL >
8 | %
9 | SELECT
10 | timestamp,
11 | document_id,
12 | dataroom_id,
13 | view_id,
14 | page_number,
15 | version_number,
16 | href
17 | FROM click_events
18 | WHERE document_id = {{ String(document_id, required=True) }}
19 | AND view_id = {{ String(view_id, required=True) }}
20 | ORDER BY timestamp ASC
21 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_document_duration_per_viewer.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT
7 | SUM(duration) AS sum_duration
8 | FROM
9 | page_views__v3
10 | WHERE
11 | documentId = {{ String(documentId, required=True)}}
12 | AND viewId IN splitByChar(',', {{ String(viewIds, required=True) }})
13 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_page_duration_per_view.pipe:
--------------------------------------------------------------------------------
1 | VERSION 4
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT
7 | pageNumber,
8 | SUM(duration) AS sum_duration
9 | FROM
10 | page_views__v3
11 | WHERE
12 | documentId = {{ String(documentId, required=True) }}
13 | AND viewId = {{ String(viewId, required=True) }}
14 | AND time >= {{ Int64(since, required=True) }}
15 | GROUP BY
16 | pageNumber
17 | ORDER BY
18 | pageNumber ASC
19 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_total_average_page_duration.pipe:
--------------------------------------------------------------------------------
1 | VERSION 5
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | WITH
7 | DistinctDurations AS (
8 | SELECT versionNumber, pageNumber, viewId, SUM(duration) AS distinct_duration
9 | FROM page_views__v3
10 | WHERE
11 | documentId = {{ String(documentId, required=true) }}
12 | AND time >= {{ Int64(since, required=true) }}
13 | AND linkId NOT IN splitByChar(',', {{ String(excludedLinkIds, required=True) }})
14 | AND viewId NOT IN splitByChar(',', {{ String(excludedViewIds, required=True) }})
15 | GROUP BY versionNumber, pageNumber, viewId
16 | )
17 | SELECT versionNumber, pageNumber, AVG(distinct_duration) AS avg_duration
18 | FROM DistinctDurations
19 | GROUP BY versionNumber, pageNumber
20 | ORDER BY versionNumber ASC, pageNumber ASC
21 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_total_dataroom_duration.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT
7 | viewId,
8 | SUM(duration) AS sum_duration
9 | FROM
10 | page_views__v3
11 | WHERE
12 | dataroomId = {{ String(dataroomId, required=true) }}
13 | AND time >= {{ Int64(since, required=true) }}
14 | AND linkId NOT IN {{ Array(excludedLinkIds, String) }}
15 | AND viewId NOT IN {{ Array(excludedViewIds, String) }}
16 | GROUP BY
17 | viewId
18 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_total_document_duration.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT SUM(duration) AS sum_duration
7 | FROM page_views__v3
8 | WHERE
9 | documentId = {{ String(documentId, required=true) }}
10 | AND time >= {{ Int64(since, required=true) }}
11 | AND linkId NOT IN splitByChar(',', {{ String(excludedLinkIds, required=True) }})
12 | AND viewId NOT IN splitByChar(',', {{ String(excludedViewIds, required=True) }})
13 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_total_link_duration.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT SUM(duration) AS sum_duration, COUNT(distinct viewId) as view_count
7 | FROM page_views__v3
8 | WHERE
9 | linkId = {{ String(linkId, required=true) }}
10 | AND time >= {{ Int64(since, required=true) }}
11 | AND documentId = {{ String(documentId, required=true) }}
12 | AND viewId NOT IN splitByChar(',', {{ String(excludedViewIds, required=true) }})
13 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_total_viewer_duration.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT
7 | SUM(duration) AS sum_duration
8 | FROM
9 | page_views__v3
10 | WHERE
11 | viewId IN splitByChar(',', {{ String(viewIds, required=true) }})
12 | AND time >= {{ Int64(since, required=true) }}
13 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_useragent_per_view.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT
7 | country,
8 | city,
9 | browser,
10 | os,
11 | device
12 | FROM
13 | page_views__v3
14 | WHERE
15 | documentId = {{ String(documentId, required=True) }}
16 | AND viewId = {{ String(viewId, required=True) }}
17 | AND time >= {{ Int64(since, required=True) }}
18 | ORDER BY
19 | viewId, time ASC
20 | LIMIT 1
21 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_video_events_by_document.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | DESCRIPTION >
4 | Get all video views for a specific document
5 |
6 | NODE get_document_video_views
7 | SQL >
8 | %
9 | SELECT
10 | timestamp,
11 | view_id,
12 | event_type,
13 | start_time,
14 | end_time,
15 | playback_rate,
16 | volume,
17 | is_muted,
18 | is_focused,
19 | is_fullscreen
20 | FROM video_views
21 | WHERE document_id = {{String(document_id, required=True)}}
22 | ORDER BY timestamp ASC
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_video_events_by_view.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | DESCRIPTION >
4 | Get all video events for a specific view
5 |
6 | NODE get_view_video_events
7 | SQL >
8 | %
9 | SELECT
10 | timestamp,
11 | event_type,
12 | start_time,
13 | end_time,
14 | playback_rate,
15 | volume,
16 | is_muted,
17 | is_focused,
18 | is_fullscreen
19 | FROM video_views
20 | WHERE document_id = {{String(document_id, required=True)}}
21 | AND view_id = {{String(view_id, required=True)}}
22 | ORDER BY timestamp ASC
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/tinybird/endpoints/get_webhook_events.pipe:
--------------------------------------------------------------------------------
1 | VERSION 1
2 |
3 | NODE endpoint
4 | SQL >
5 | %
6 | SELECT
7 | *
8 | FROM
9 | webhook_events__v1
10 | WHERE
11 | webhook_id = {{ String(webhookId, required=True) }}
12 | ORDER BY
13 | timestamp DESC
14 | LIMIT 100
15 |
--------------------------------------------------------------------------------
/lib/tinybird/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./pipes";
2 | export * from "./publish";
3 |
--------------------------------------------------------------------------------
/lib/tracking/tracking-config.ts:
--------------------------------------------------------------------------------
1 | export const TRACKING_CONFIG = {
2 | // Interval tracking settings
3 | INTERVAL_TRACKING_ENABLED: true,
4 | INTERVAL_DURATION: 10000, // 10 seconds (in milliseconds)
5 |
6 | // Activity tracking settings
7 | ACTIVITY_TRACKING_ENABLED: true,
8 | INACTIVITY_THRESHOLD: 60000, // 1 minute (in milliseconds)
9 |
10 | // Activity detection settings
11 | ACTIVITY_DETECTION_ENABLED: true,
12 |
13 | // Minimum duration to track (prevents very short sessions)
14 | MIN_TRACKING_DURATION: 1000, // 1 second (in milliseconds)
15 | } as const;
16 |
17 | export function getTrackingOptions(overrides: Partial = {}) {
18 | return {
19 | intervalTracking: overrides.INTERVAL_TRACKING_ENABLED ?? TRACKING_CONFIG.INTERVAL_TRACKING_ENABLED,
20 | intervalDuration: overrides.INTERVAL_DURATION ?? TRACKING_CONFIG.INTERVAL_DURATION,
21 | activityTracking: overrides.ACTIVITY_TRACKING_ENABLED ?? TRACKING_CONFIG.ACTIVITY_TRACKING_ENABLED,
22 | inactivityThreshold: overrides.INACTIVITY_THRESHOLD ?? TRACKING_CONFIG.INACTIVITY_THRESHOLD,
23 | enableActivityDetection: overrides.ACTIVITY_DETECTION_ENABLED ?? TRACKING_CONFIG.ACTIVITY_DETECTION_ENABLED,
24 | };
25 | }
--------------------------------------------------------------------------------
/lib/trigger/conversation-message-notification.ts:
--------------------------------------------------------------------------------
1 | import { sendConversationMessageNotificationTask } from "@/ee/features/conversations/lib/trigger/conversation-message-notification";
2 |
3 | export { sendConversationMessageNotificationTask };
4 |
--------------------------------------------------------------------------------
/lib/types/index-file.ts:
--------------------------------------------------------------------------------
1 | export interface DataroomIndexEntry {
2 | name: string;
3 | type: "File" | "Folder" | "Root Folder";
4 | path: string;
5 | size?: number;
6 | pages?: number;
7 | lastUpdated: Date;
8 | onlineUrl?: string;
9 | mimeType?: string;
10 | createdAt?: Date;
11 | }
12 |
13 | export interface DataroomIndex {
14 | dataroomId: string;
15 | dataroomName: string;
16 | linkId: string;
17 | generatedAt: Date;
18 | entries: DataroomIndexEntry[];
19 | totalFiles: number;
20 | totalFolders: number;
21 | totalSize: number;
22 | }
23 |
24 | export type IndexFileFormat = "excel" | "csv" | "json";
25 |
--------------------------------------------------------------------------------
/lib/utils/decode-base64url.ts:
--------------------------------------------------------------------------------
1 | export function decodeBase64Url(base64url: string) {
2 | base64url = base64url.replace(/-/g, "+").replace(/_/g, "/");
3 | while (base64url.length % 4) {
4 | base64url += "=";
5 | }
6 | return decodeURIComponent(
7 | Array.prototype.map
8 | .call(atob(base64url), function (c) {
9 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
10 | })
11 | .join(""),
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/determine-text-color.ts:
--------------------------------------------------------------------------------
1 | function hexToRgb(hex: string) {
2 | let r = 0,
3 | g = 0,
4 | b = 0;
5 | // 3 digits
6 | if (hex.length === 4) {
7 | r = parseInt(hex[1] + hex[1], 16);
8 | g = parseInt(hex[2] + hex[2], 16);
9 | b = parseInt(hex[3] + hex[3], 16);
10 | }
11 | // 6 digits
12 | else if (hex.length === 7) {
13 | r = parseInt(hex[1] + hex[2], 16);
14 | g = parseInt(hex[3] + hex[4], 16);
15 | b = parseInt(hex[5] + hex[6], 16);
16 | }
17 | return [r, g, b];
18 | }
19 |
20 | function luminance(r: number, g: number, b: number) {
21 | return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
22 | }
23 |
24 | export function determineTextColor(hexColor: string | null | undefined) {
25 | if (!hexColor) return "white";
26 | const [r, g, b] = hexToRgb(hexColor);
27 | return luminance(r, g, b) > 0.5 ? "black" : "white";
28 | }
29 |
--------------------------------------------------------------------------------
/lib/utils/generate-checksum.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 |
3 | export function generateChecksum(url: string): string {
4 | // Use a secure secret key stored in environment variables
5 | const secret = process.env.NEXT_PRIVATE_VERIFICATION_SECRET!;
6 |
7 | // Create HMAC using SHA-256
8 | const hmac = crypto.createHmac("sha256", secret);
9 | hmac.update(url);
10 |
11 | // Return hex digest
12 | return hmac.digest("hex");
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/generate-jwt.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | const JWT_SECRET = process.env.NEXT_PRIVATE_UNSUBSCRIBE_JWT_SECRET as string;
4 |
5 | type JWTPayload = {
6 | [key: string]: any;
7 | exp?: number; // Expiration timestamp
8 | };
9 |
10 | /**
11 | * Generates a JWT token with the provided payload
12 | * @param payload The data to encode in the JWT
13 | * @param expiresInSeconds Optional expiration time in seconds (default: 24 hours)
14 | * @returns The signed JWT token
15 | */
16 | export function generateJWT(
17 | payload: JWTPayload,
18 | expiresInSeconds: number = 60 * 60 * 24,
19 | ): string {
20 | const tokenPayload = {
21 | ...payload,
22 | exp: payload.exp || Math.floor(Date.now() / 1000) + expiresInSeconds,
23 | };
24 |
25 | return jwt.sign(tokenPayload, JWT_SECRET);
26 | }
27 |
28 | /**
29 | * Verifies a JWT token and returns the decoded payload
30 | * @param token The JWT token to verify
31 | * @returns The decoded payload or null if invalid
32 | */
33 | export function verifyJWT(token: string): T | null {
34 | try {
35 | return jwt.verify(token, JWT_SECRET) as T;
36 | } catch (error) {
37 | return null;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/utils/generate-otp.ts:
--------------------------------------------------------------------------------
1 | export function generateOTP(): string {
2 | // Generate a random number between 0 and 999999
3 | const randomNumber = Math.floor(Math.random() * 1000000);
4 |
5 | // Pad the number with leading zeros if necessary to ensure it is always 6 digits
6 | const otp = randomNumber.toString().padStart(6, "0");
7 |
8 | return otp;
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/generate-trigger-auth-token.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@trigger.dev/sdk/v3";
2 |
3 | export async function generateTriggerPublicAccessToken(tag: string) {
4 | return auth.createPublicToken({
5 | scopes: {
6 | read: {
7 | tags: [tag],
8 | },
9 | },
10 | expirationTime: "15m",
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/lib/utils/generate-trigger-status.ts:
--------------------------------------------------------------------------------
1 | import { metadata } from "@trigger.dev/sdk/v3";
2 | import { z } from "zod";
3 |
4 | const ZDocumentProgressStatus = z.object({
5 | progress: z.number(),
6 | text: z.string(),
7 | });
8 |
9 | type TDocumentProgressStatus = z.infer;
10 |
11 | const ZDocumentProgressMetadata = z.object({
12 | status: ZDocumentProgressStatus,
13 | });
14 |
15 | type TDocumentProgressMetadata = z.infer;
16 |
17 | /**
18 | * Update the status of the convert document task. Wraps the `metadata.set` method.
19 | */
20 | export function updateStatus(status: TDocumentProgressStatus) {
21 | // `metadata.set` can be used to update the status of the task
22 | // as long as `updateStatus` is called within the task's `run` function.
23 | metadata.set("status", status);
24 | }
25 |
26 | /**
27 | * Parse the status from the metadata.
28 | */
29 | export function parseStatus(data: unknown): TDocumentProgressStatus {
30 | return ZDocumentProgressMetadata.parse(data).status;
31 | }
32 |
--------------------------------------------------------------------------------
/lib/utils/geo.ts:
--------------------------------------------------------------------------------
1 | import { Geo } from "../types";
2 |
3 | export function getGeoData(headers: {
4 | [key: string]: string | string[] | undefined;
5 | }): Geo {
6 | return {
7 | city: Array.isArray(headers["x-vercel-ip-city"])
8 | ? headers["x-vercel-ip-city"][0]
9 | : headers["x-vercel-ip-city"],
10 | region: Array.isArray(headers["x-vercel-ip-region"])
11 | ? headers["x-vercel-ip-region"][0]
12 | : headers["x-vercel-ip-region"],
13 | country: Array.isArray(headers["x-vercel-ip-country"])
14 | ? headers["x-vercel-ip-country"][0]
15 | : headers["x-vercel-ip-country"],
16 | latitude: Array.isArray(headers["x-vercel-ip-latitude"])
17 | ? headers["x-vercel-ip-latitude"][0]
18 | : headers["x-vercel-ip-latitude"],
19 | longitude: Array.isArray(headers["x-vercel-ip-longitude"])
20 | ? headers["x-vercel-ip-longitude"][0]
21 | : headers["x-vercel-ip-longitude"],
22 | };
23 | }
24 |
25 | export const LOCALHOST_GEO_DATA = {
26 | continent: "Europe",
27 | city: "Munich",
28 | region: "BY",
29 | country: "DE",
30 | latitude: "48.137154",
31 | longitude: "11.576124",
32 | };
33 |
34 | export const LOCALHOST_IP = "127.0.0.1";
35 |
--------------------------------------------------------------------------------
/lib/utils/get-search-params.ts:
--------------------------------------------------------------------------------
1 | export const getSearchParams = (url: string) => {
2 | // Create a params object
3 | let params = {} as Record;
4 |
5 | new URL(url).searchParams.forEach(function (val, key) {
6 | params[key] = val;
7 | });
8 |
9 | return params;
10 | };
11 |
--------------------------------------------------------------------------------
/lib/utils/ip.ts:
--------------------------------------------------------------------------------
1 | export function getIpAddress(headers: {
2 | [key: string]: string | string[] | undefined;
3 | }): string {
4 | if (typeof headers["x-forwarded-for"] === "string") {
5 | return (headers["x-forwarded-for"] ?? "127.0.0.1").split(",")[0];
6 | }
7 | return "127.0.0.1";
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/sanitize-html.ts:
--------------------------------------------------------------------------------
1 | import sanitizeHtml from "sanitize-html";
2 |
3 | export function validateContent(html: string) {
4 | if (html.length > 1000) {
5 | throw new Error("Content cannot be longer than 1000 characters");
6 | }
7 | const sanitized = sanitizeHtml(html, {
8 | allowedTags: [],
9 | allowedAttributes: {},
10 | });
11 |
12 | if (sanitized.length === 0 || sanitized === "") {
13 | throw new Error("Content cannot be empty");
14 | }
15 |
16 | return sanitized.trim();
17 | }
18 |
--------------------------------------------------------------------------------
/lib/utils/sort-items-by-index-name.ts:
--------------------------------------------------------------------------------
1 | export const sortItemsByIndexAndName = <
2 | T extends {
3 | orderIndex: number | null;
4 | document: { name: string };
5 | },
6 | >(
7 | items: T[],
8 | ): T[] => {
9 | // Sort documents by orderIndex or name considering the numerical part
10 | return items.sort((a, b) => {
11 | // First, compare by orderIndex if both items have it
12 | if (a.orderIndex && b.orderIndex) {
13 | if (a.orderIndex !== b.orderIndex) {
14 | return a.orderIndex - b.orderIndex;
15 | }
16 | }
17 |
18 | // If orderIndex is not available or equal, use name-based sorting
19 | const numA = getNumber(a.document.name);
20 | const numB = getNumber(b.document.name);
21 | if (numA !== numB) {
22 | return numA - numB;
23 | }
24 | // If numerical parts are the same, fall back to lexicographical order
25 | return a.document.name.localeCompare(b.document.name);
26 | });
27 | };
28 |
29 | // Helper function to extract the numerical part of a string
30 | const getNumber = (str: string): number => {
31 | const match = str.match(/^\d+/);
32 | return match ? parseInt(match[0], 10) : 0;
33 | };
34 |
--------------------------------------------------------------------------------
/lib/utils/trigger-utils.ts:
--------------------------------------------------------------------------------
1 | import { BasePlan } from "../swr/use-billing";
2 |
3 | type TQueueConfig = {
4 | name: string;
5 | concurrencyLimit: number;
6 | };
7 |
8 | const concurrencyConfig: Record = {
9 | free: 1,
10 | starter: 1,
11 | pro: 2,
12 | business: 10,
13 | datarooms: 10,
14 | "datarooms-plus": 10,
15 | };
16 |
17 | export const conversionQueue = (plan: string): TQueueConfig => {
18 | const planName = plan.split("+")[0] as BasePlan;
19 |
20 | return {
21 | name: `conversion-${planName}`,
22 | concurrencyLimit: concurrencyConfig[planName],
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/lib/utils/unsubscribe.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | const JWT_SECRET = process.env.NEXT_PRIVATE_UNSUBSCRIBE_JWT_SECRET as string;
4 | const UNSUBSCRIBE_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL as string;
5 |
6 | type UnsubscribePayload = {
7 | viewerId: string;
8 | teamId: string;
9 | dataroomId?: string;
10 | exp?: number; // Expiration timestamp
11 | };
12 |
13 | export function generateUnsubscribeUrl(payload: UnsubscribePayload): string {
14 | // Add expiration of 3 months
15 | const tokenPayload = {
16 | ...payload,
17 | exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 90,
18 | };
19 |
20 | const token = jwt.sign(tokenPayload, JWT_SECRET);
21 | return `${UNSUBSCRIBE_BASE_URL}/api/unsubscribe/${
22 | payload.dataroomId ? "dataroom" : "yir"
23 | }?token=${token}`;
24 | }
25 |
26 | export function verifyUnsubscribeToken(
27 | token: string,
28 | ): UnsubscribePayload | null {
29 | try {
30 | return jwt.verify(token, JWT_SECRET) as UnsubscribePayload;
31 | } catch (error) {
32 | return null;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/utils/use-at-bottom.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset,
11 | );
12 | };
13 |
14 | window.addEventListener("scroll", handleScroll, { passive: true });
15 | handleScroll();
16 |
17 | return () => {
18 | window.removeEventListener("scroll", handleScroll);
19 | };
20 | }, [offset]);
21 |
22 | return isAtBottom;
23 | }
24 |
--------------------------------------------------------------------------------
/lib/utils/use-copy-to-clipboard.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { toast } from "sonner";
6 |
7 | export interface useCopyToClipboardProps {
8 | timeout?: number;
9 | }
10 |
11 | export function useCopyToClipboard({
12 | timeout = 2000,
13 | }: useCopyToClipboardProps) {
14 | const [isCopied, setIsCopied] = React.useState(false);
15 |
16 | const copyToClipboard = (value: string, message?: string) => {
17 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
18 | return;
19 | }
20 |
21 | if (!value) {
22 | return;
23 | }
24 |
25 | navigator.clipboard.writeText(value).then(() => {
26 | setIsCopied(true);
27 | message && toast.success(message);
28 |
29 | setTimeout(() => {
30 | setIsCopied(false);
31 | }, timeout);
32 | });
33 | };
34 |
35 | return { isCopied, copyToClipboard };
36 | }
37 |
--------------------------------------------------------------------------------
/lib/utils/use-enter-submit.ts:
--------------------------------------------------------------------------------
1 | import { type RefObject, useRef } from "react";
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject;
5 | onKeyDown: (event: React.KeyboardEvent) => void;
6 | } {
7 | const formRef = useRef(null);
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent,
11 | ): void => {
12 | if (
13 | event.key === "Enter" &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit();
18 | event.preventDefault();
19 | }
20 | };
21 |
22 | return { formRef, onKeyDown: handleKeyDown };
23 | }
24 |
--------------------------------------------------------------------------------
/lib/utils/user-agent.ts:
--------------------------------------------------------------------------------
1 | import { UAParser } from "ua-parser-js";
2 |
3 | export function isBot(input: string) {
4 | return /bot|chatgpt|Googlebot|Mediapartners-Google|AdsBot-Google|googleweblight|Storebot-Google|Google-PageRenderer|Bingbot|BingPreview|Slurp|DuckDuckBot|baiduspider|yandex|sogou|LinkedInBot|bitlybot|tumblr|vkShare|quora link preview|facebookexternalhit|facebookcatalog|Twitterbot|applebot|redditbot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|ia_archiver/i.test(
5 | input,
6 | );
7 | }
8 |
9 | export function userAgentFromString(input: string | undefined): UserAgent {
10 | return {
11 | ...new UAParser(input).getResult(),
12 | isBot: input === undefined ? false : isBot(input),
13 | };
14 | }
15 |
16 | interface UserAgent {
17 | isBot: boolean;
18 | ua: string;
19 | browser: {
20 | name?: string;
21 | version?: string;
22 | };
23 | device: {
24 | model?: string;
25 | type?: string;
26 | vendor?: string;
27 | };
28 | engine: {
29 | name?: string;
30 | version?: string;
31 | };
32 | os: {
33 | name?: string;
34 | version?: string;
35 | };
36 | cpu: {
37 | architecture?: string;
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/lib/utils/validate-email.ts:
--------------------------------------------------------------------------------
1 | // RFC 5322 compliant regex
2 | export const fullyCompliantEmailRegex =
3 | /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
4 |
5 | // Simple email regex
6 | export const simpleEmailRegex =
7 | /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+$/;
8 |
9 | export const validateEmail = (email: string) => {
10 | return simpleEmailRegex.test(email.toLowerCase().trim());
11 | };
12 |
--------------------------------------------------------------------------------
/lib/webhook/constants.ts:
--------------------------------------------------------------------------------
1 | export const TEAM_LEVEL_WEBHOOK_TRIGGERS = [
2 | "document.created",
3 | "document.updated",
4 | "document.deleted",
5 | "dataroom.created",
6 | ] as const;
7 |
8 | export const DOCUMENT_LEVEL_WEBHOOK_TRIGGERS = [
9 | "link.created",
10 | "link.updated",
11 | ] as const;
12 |
13 | export const LINK_LEVEL_WEBHOOK_TRIGGERS = [
14 | "link.viewed",
15 | "link.downloaded",
16 | ] as const;
17 |
18 | export const WEBHOOK_TRIGGERS = [
19 | ...TEAM_LEVEL_WEBHOOK_TRIGGERS,
20 | ...DOCUMENT_LEVEL_WEBHOOK_TRIGGERS,
21 | ...LINK_LEVEL_WEBHOOK_TRIGGERS,
22 | ] as const;
23 |
24 | export const WEBHOOK_TRIGGER_DESCRIPTIONS = {
25 | "link.created": "Link created",
26 | "link.updated": "Link updated",
27 | "link.deleted": "Link deleted",
28 | "link.viewed": "Link viewed",
29 | "link.downloaded": "Link downloaded",
30 | "document.created": "Document created",
31 | "document.updated": "Document updated",
32 | "document.deleted": "Document deleted",
33 | "dataroom.created": "Data room created",
34 | } as const;
35 |
--------------------------------------------------------------------------------
/lib/webhook/signature.ts:
--------------------------------------------------------------------------------
1 | export const createWebhookSignature = async (secret: string, body: any) => {
2 | if (!secret) {
3 | throw new Error("A secret must be provided to create a webhook signature.");
4 | }
5 |
6 | const keyData = new TextEncoder().encode(secret);
7 | const messageData = new TextEncoder().encode(JSON.stringify(body));
8 |
9 | const cryptoKey = await crypto.subtle.importKey(
10 | "raw",
11 | keyData,
12 | { name: "HMAC", hash: "SHA-256" },
13 | false,
14 | ["sign"],
15 | );
16 |
17 | const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
18 | const signatureArray = Array.from(new Uint8Array(signature));
19 | const hexSignature = signatureArray
20 | .map((byte) => byte.toString(16).padStart(2, "0"))
21 | .join("");
22 |
23 | return hexSignature;
24 | };
25 |
--------------------------------------------------------------------------------
/lib/webhook/transform.ts:
--------------------------------------------------------------------------------
1 | import { newId } from "@/lib/id-helper";
2 | import { webhookPayloadSchema } from "@/lib/zod/schemas/webhooks";
3 |
4 | import { WebhookTrigger } from "./types";
5 |
6 | export const prepareWebhookPayload = (trigger: WebhookTrigger, data: any) => {
7 | const payload = webhookPayloadSchema.parse({
8 | id: newId("webhookEvent"),
9 | event: trigger,
10 | data: data,
11 | createdAt: new Date().toISOString(),
12 | });
13 |
14 | return payload;
15 | };
16 |
--------------------------------------------------------------------------------
/lib/webhook/types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import {
4 | dataroomCreatedWebhookSchema,
5 | documentCreatedWebhookSchema,
6 | linkCreatedWebhookSchema,
7 | webhookPayloadSchema,
8 | } from "../zod/schemas/webhooks";
9 | import { WEBHOOK_TRIGGER_DESCRIPTIONS } from "./constants";
10 |
11 | export type WebhookTrigger = keyof typeof WEBHOOK_TRIGGER_DESCRIPTIONS;
12 |
13 | export type WebhookPayload =
14 | | z.infer
15 | | z.infer
16 | | z.infer
17 | | z.infer;
18 |
19 | // TODO: only show the link.viewed, link.created, document.created data props for now
20 | export type EventDataProps = WebhookPayload["data"];
21 |
--------------------------------------------------------------------------------
/lib/webstorage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Provides a wrapper around localStorage(and sessionStorage(TODO when needed)) to avoid errors in case of restricted storage access.
3 | *
4 | * TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
5 | */
6 | export const localStorage = {
7 | getItem(key: string) {
8 | try {
9 | return window.localStorage.getItem(key);
10 | } catch (e) {
11 | // In case storage is restricted. Possible reasons
12 | // 1. Third Party Context in Chrome Incognito mode.
13 | return null;
14 | }
15 | },
16 | setItem(key: string, value: string) {
17 | try {
18 | window.localStorage.setItem(key, value);
19 | } catch (e) {
20 | // In case storage is restricted. Possible reasons
21 | // 1. Third Party Context in Chrome Incognito mode.
22 | // 2. Storage limit reached
23 | return;
24 | }
25 | },
26 | removeItem: (key: string) => {
27 | try {
28 | window.localStorage.removeItem(key);
29 | } catch (e) {
30 | return;
31 | }
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/lib/zod/schemas/notifications.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const ZViewerNotificationPreferencesSchema = z
4 | .object({
5 | dataroom: z.record(
6 | z.object({
7 | enabled: z.boolean(),
8 | }),
9 | ),
10 | })
11 | .optional()
12 | .default({ dataroom: {} });
13 |
14 | export const ZUserNotificationPreferencesSchema = z
15 | .object({
16 | yearInReview: z.object({
17 | enabled: z.boolean(),
18 | }),
19 | })
20 | .optional()
21 | .default({ yearInReview: { enabled: true } });
22 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/pages/api/auth-plus/set-cookie.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { getToken } from "next-auth/jwt";
4 |
5 | const VERCEL_DEPLOYMENT = !!process.env.VERCEL_URL;
6 |
7 | export default async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponse,
10 | ) {
11 | const token = await getToken({ req });
12 |
13 | if (!token) {
14 | return res.status(401).end();
15 | }
16 |
17 | // Set the cookie for the other domain
18 | res.setHeader(
19 | "Set-Cookie",
20 | `${VERCEL_DEPLOYMENT ? "__Secure-" : ""}next-auth.session-token=${req.cookies[`${VERCEL_DEPLOYMENT ? "__Secure-" : ""}next-auth.session-token`]}; HttpOnly; Path=/; SameSite=Lax; ${VERCEL_DEPLOYMENT ? "Secure; " : ""}Domain=.papermark.io; Max-Age=${30 * 24 * 60 * 60}`,
21 | );
22 |
23 | res.status(200).end();
24 | }
25 |
--------------------------------------------------------------------------------
/pages/api/conversations/[[...conversations]].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { handleRoute } from "@/ee/features/conversations/api/conversations-route";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | return handleRoute(req, res);
10 | }
11 |
--------------------------------------------------------------------------------
/pages/api/health.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/lib/prisma";
4 |
5 | export default async function handler(
6 | _req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | try {
10 | await prisma.$queryRaw`SELECT 1`;
11 |
12 | return res.json({
13 | status: "ok",
14 | message: "All systems operational",
15 | });
16 | } catch (err) {
17 | console.error(err);
18 |
19 | return res.status(500).json({
20 | status: "error",
21 | message: (err as Error).message,
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/pages/api/jobs/get-thumbnail.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { getServerSession } from "next-auth";
4 |
5 | import { getFileForDocumentPage } from "@/lib/documents/get-file-helper";
6 |
7 | import { authOptions } from "../auth/[...nextauth]";
8 |
9 | export default async function handle(
10 | req: NextApiRequest,
11 | res: NextApiResponse,
12 | ) {
13 | // We only allow GET requests
14 | if (req.method !== "GET") {
15 | res.status(405).json({ message: "Method Not Allowed" });
16 | return;
17 | }
18 |
19 | const session = await getServerSession(req, res, authOptions);
20 | if (!session) {
21 | return res.status(401).end("Unauthorized");
22 | }
23 |
24 | const { documentId, pageNumber, versionNumber } = req.query as {
25 | documentId: string;
26 | pageNumber: string;
27 | versionNumber: string;
28 | };
29 |
30 | try {
31 | const imageUrl = await getFileForDocumentPage(
32 | Number(pageNumber),
33 | documentId,
34 | versionNumber === "undefined" ? undefined : Number(versionNumber),
35 | );
36 |
37 | return res.status(200).json({ imageUrl });
38 | } catch (error) {
39 | res.status(500).json({ message: (error as Error).message });
40 | return;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/pages/api/jobs/send-conversation-new-message-notification.ts:
--------------------------------------------------------------------------------
1 | import defaultHandler from "@/ee/features/conversations/api/send-conversation-new-message-notification";
2 |
3 | export default defaultHandler;
4 |
--------------------------------------------------------------------------------
/pages/api/links/[id]/preview.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { getServerSession } from "next-auth/next";
4 |
5 | import { createPreviewSession } from "@/lib/auth/preview-auth";
6 | import { CustomUser } from "@/lib/types";
7 |
8 | import { authOptions } from "../../auth/[...nextauth]";
9 |
10 | export default async function handle(
11 | req: NextApiRequest,
12 | res: NextApiResponse,
13 | ) {
14 | if (req.method === "POST") {
15 | // POST /api/links/:id/preview
16 | const session = await getServerSession(req, res, authOptions);
17 | if (!session) {
18 | return res.status(401).end("Unauthorized");
19 | }
20 |
21 | const { id } = req.query as { id: string };
22 |
23 | const previewSession = await createPreviewSession(
24 | id,
25 | (session.user as CustomUser).id,
26 | );
27 |
28 | return res.status(200).json({ previewToken: previewSession.token });
29 | } else {
30 | // We only allow POST requests
31 | res.setHeader("Allow", ["POST"]);
32 | return res.status(405).end(`Method ${req.method} Not Allowed`);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/pages/api/mupdf/get-pages.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import * as mupdf from "mupdf";
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | // check if post method
7 | if (req.method !== "POST") {
8 | res.status(405).json({ error: "Method Not Allowed" });
9 | return;
10 | }
11 |
12 | try {
13 | const { url } = req.body as { url: string };
14 | // Fetch the PDF data
15 | const response = await fetch(url);
16 | // Convert the response to an ArrayBuffer
17 | const pdfData = await response.arrayBuffer();
18 | // Create a MuPDF instance
19 | var doc = new mupdf.PDFDocument(pdfData);
20 |
21 | var n = doc.countPages();
22 |
23 | // Send the images as a response
24 | res.status(200).json({ numPages: n });
25 | } catch (error) {
26 | console.error("Error:", error);
27 | res.status(500).json({ error: "Internal Server Error" });
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/pages/api/progress-token.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { generateTriggerPublicAccessToken } from "@/lib/utils/generate-trigger-auth-token";
4 |
5 | export default async function handle(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | if (req.method !== "GET") {
10 | return res.status(405).json({ error: "Method not allowed" });
11 | }
12 |
13 | const { documentVersionId } = req.query;
14 |
15 | if (!documentVersionId || typeof documentVersionId !== "string") {
16 | return res.status(400).json({ error: "Document version ID is required" });
17 | }
18 |
19 | try {
20 | const publicAccessToken = await generateTriggerPublicAccessToken(
21 | `version:${documentVersionId}`,
22 | );
23 | return res.status(200).json({ publicAccessToken });
24 | } catch (error) {
25 | console.error("Error generating token:", error);
26 | return res.status(500).json({ error: "Failed to generate token" });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pages/api/record_reaction.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/lib/prisma";
4 |
5 | export default async function handle(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | // We only allow POST requests
10 | if (req.method !== "POST") {
11 | res.status(405).json({ message: "Method Not Allowed" });
12 | return;
13 | }
14 |
15 | // POST /api/record_reaction
16 |
17 | const { viewId, pageNumber, type } = req.body as {
18 | viewId: string;
19 | pageNumber: number;
20 | type: string;
21 | };
22 |
23 | try {
24 | const reaction = await prisma.reaction.create({
25 | data: {
26 | viewId,
27 | pageNumber,
28 | type,
29 | },
30 | });
31 |
32 | if (!reaction) {
33 | res.status(500).json({ message: "Internal Server Error" });
34 | return;
35 | }
36 |
37 | res.status(200).json({ message: "Reaction recorded" });
38 | return;
39 | } catch (error) {
40 | res.status(500).json({ message: "Internal Server Error" });
41 | return;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pages/api/teams/[teamId]/datarooms/[id]/conversations/[[...conversations]].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { handleRoute } from "@/ee/features/conversations/api/team-conversations-route";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | return handleRoute(req, res);
10 | }
11 |
--------------------------------------------------------------------------------
/pages/api/teams/[teamId]/datarooms/[id]/conversations/toggle-conversations.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import toggleConversationsRoute from "@/ee/features/conversations/api/toggle-conversations-route";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | return toggleConversationsRoute(req, res);
10 | }
11 |
--------------------------------------------------------------------------------
/pages/api/teams/[teamId]/documents/document-processing-status.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/lib/prisma";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | const { documentVersionId } = req.query as { documentVersionId: string };
10 |
11 | const documentVersion = await prisma.documentVersion.findUnique({
12 | where: { id: documentVersionId },
13 | select: {
14 | numPages: true,
15 | hasPages: true,
16 | _count: { select: { pages: true } },
17 | },
18 | });
19 |
20 | if (!documentVersion) {
21 | return res.status(404).end();
22 | }
23 |
24 | const status = {
25 | currentPageCount: documentVersion._count.pages,
26 | totalPages: documentVersion.numPages,
27 | hasPages: documentVersion.hasPages,
28 | };
29 |
30 | res.status(200).json(status);
31 | }
32 |
--------------------------------------------------------------------------------
/pages/api/teams/[teamId]/limits.ts:
--------------------------------------------------------------------------------
1 | import limitsHandler from "@/ee/limits/handler";
2 |
3 | export default limitsHandler;
4 |
--------------------------------------------------------------------------------
/pages/datarooms/[id]/conversations/[conversationId]/index.tsx:
--------------------------------------------------------------------------------
1 | import ConversationDetail from "@/ee/features/conversations/pages/conversation-detail";
2 |
3 | export default function ConversationDetailPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/pages/datarooms/[id]/conversations/index.tsx:
--------------------------------------------------------------------------------
1 | import ConversationOverview from "@/ee/features/conversations/pages/conversation-overview";
2 |
3 | export default function ConversationOverviewPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/pages/datarooms/[id]/users/index.tsx:
--------------------------------------------------------------------------------
1 | import { useDataroom } from "@/lib/swr/use-dataroom";
2 |
3 | import { DataroomHeader } from "@/components/datarooms/dataroom-header";
4 | import { DataroomNavigation } from "@/components/datarooms/dataroom-navigation";
5 | import AppLayout from "@/components/layouts/app";
6 | import DataroomViewersTable from "@/components/visitors/dataroom-viewers";
7 |
8 | export default function DataroomUsersPage() {
9 | const { dataroom } = useDataroom();
10 |
11 | if (!dataroom) {
12 | return Loading...
;
13 | }
14 |
15 | return (
16 |
17 |
18 |
23 |
24 |
25 | {/* Visitors */}
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/pkgx.yaml:
--------------------------------------------------------------------------------
1 | dependencies: |
2 | pipenv.pypa.io@2023.9.1
3 | python@3.11
4 | node@22
5 | npm@11
6 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | singleQuote: false,
4 | jsxSingleQuote: false,
5 | bracketSpacing: true,
6 | singleAttributePerLine: false,
7 | bracketSameLine: false,
8 | tabWidth: 2,
9 | useTabs: false,
10 | trailingComma: "all",
11 | printWidth: 80,
12 | quoteProps: "as-needed",
13 | arrowParens: "always",
14 | endOfLine: "lf",
15 | proseWrap: "preserve",
16 | importOrder: [
17 | "^(next/(.*)$)|^(next$)",
18 | "^(react/(.*)$)|^(react$)",
19 | "",
20 | "^@/lib/(.*)$",
21 | "^@/components/(.*)$|^components/(.*)$",
22 | "^[./]",
23 | "^@/styles/(.*)$",
24 | ],
25 | importOrderSeparation: true,
26 | importOrderSortSpecifiers: true,
27 | plugins: [
28 | "@trivago/prettier-plugin-sort-imports",
29 | "prettier-plugin-tailwindcss",
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/prisma/README.md:
--------------------------------------------------------------------------------
1 | ## Create migrations after changes in schema.prisma
2 |
3 | So you have made changes to the schema.prisma file and you want to apply those changes to the database.
4 |
5 | Chances are you are getting this error from Prisma after running `npx prisma migrate dev`:
6 |
7 | ```txt
8 | Drift detected: Your database schema is not in sync with your migration history.
9 | ...
10 | Do you want to continue? All data will be lost.
11 | ```
12 |
13 | ### Requirements
14 |
15 | 1. Local prisma
16 |
17 | ```bash
18 | npm install -g prisma
19 | # npx prisma won't work
20 | ```
21 |
22 | 2. shadow database, otherwise you overwrite your local db
23 |
24 | ### Steps
25 |
26 | - Create a new migration folder
27 |
28 | ```bash
29 | mkdir -p prisma/migrations/20240408000000_add_model
30 | ```
31 |
32 | - Generate the migration
33 |
34 | ```bash
35 | prisma migrate diff --from-migrations prisma/migrations --to-schema-datamodel prisma/schema.prisma --shadow-database-url "postgresql://@localhost:5432/papermark-shadow-db" --script > prisma/migrations/20240408000000_add_model/migration.sql
36 | ```
37 |
38 | - Apply the migration
39 |
40 | ```bash
41 | prisma migrate resolve --applied 20240408000000_add_model
42 | ```
43 |
--------------------------------------------------------------------------------
/prisma/migrations/202310122339_NewColumnInLinkTable/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "allowDownload" BOOLEAN DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20231013165123_create_document_version/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "DocumentVersion" (
3 | "id" TEXT NOT NULL,
4 | "versionNumber" INTEGER NOT NULL,
5 | "documentId" TEXT NOT NULL,
6 | "file" TEXT NOT NULL,
7 | "type" TEXT,
8 | "numPages" INTEGER,
9 | "isPrimary" BOOLEAN NOT NULL DEFAULT false,
10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 | "updatedAt" TIMESTAMP(3) NOT NULL,
12 |
13 | CONSTRAINT "DocumentVersion_pkey" PRIMARY KEY ("id")
14 | );
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "DocumentVersion" ADD CONSTRAINT "DocumentVersion_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
--------------------------------------------------------------------------------
/prisma/migrations/20231014200337_create_document_pages/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[versionNumber,documentId]` on the table `DocumentVersion` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "DocumentVersion" ADD COLUMN "hasPages" BOOLEAN NOT NULL DEFAULT false;
9 |
10 | -- CreateTable
11 | CREATE TABLE "DocumentPage" (
12 | "id" TEXT NOT NULL,
13 | "versionId" TEXT NOT NULL,
14 | "pageNumber" INTEGER NOT NULL,
15 | "file" TEXT NOT NULL,
16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
17 | "updatedAt" TIMESTAMP(3) NOT NULL,
18 |
19 | CONSTRAINT "DocumentPage_pkey" PRIMARY KEY ("id")
20 | );
21 |
22 | -- CreateIndex
23 | CREATE UNIQUE INDEX "DocumentPage_pageNumber_versionId_key" ON "DocumentPage"("pageNumber", "versionId");
24 |
25 | -- CreateIndex
26 | CREATE UNIQUE INDEX "DocumentVersion_versionNumber_documentId_key" ON "DocumentVersion"("versionNumber", "documentId");
27 |
28 | -- AddForeignKey
29 | ALTER TABLE "DocumentPage" ADD CONSTRAINT "DocumentPage_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "DocumentVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
30 |
--------------------------------------------------------------------------------
/prisma/migrations/202310311254_NewColumnEnableNotificationLinkTable/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "enableNotification" BOOLEAN DEFAULT true;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20231113051339_create_sent_email/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "EmailType" AS ENUM ('FIRST_DAY_DOMAIN_REMINDER_EMAIL', 'FIRST_DOMAIN_INVALID_EMAIL', 'SECOND_DOMAIN_INVALID_EMAIL', 'FIRST_TRIAL_END_REMINDER_EMAIL', 'FINAL_TRIAL_END_REMINDER_EMAIL');
3 |
4 | -- CreateTable
5 | CREATE TABLE "SentEmail" (
6 | "id" TEXT NOT NULL,
7 | "type" "EmailType" NOT NULL,
8 | "recipient" TEXT NOT NULL,
9 | "marketing" BOOLEAN NOT NULL DEFAULT false,
10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 | "teamId" TEXT NOT NULL,
12 |
13 | CONSTRAINT "SentEmail_pkey" PRIMARY KEY ("id")
14 | );
15 |
16 | -- CreateIndex
17 | CREATE INDEX "SentEmail_teamId_idx" ON "SentEmail"("teamId");
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "SentEmail" ADD CONSTRAINT "SentEmail_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
--------------------------------------------------------------------------------
/prisma/migrations/20231114054509_add_domain_to_sent_emails/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "SentEmail" ADD COLUMN "domainSlug" TEXT;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20231116093816_update_invitations/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[email,teamId]` on the table `Invitation` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- DropIndex
8 | DROP INDEX "Invitation_email_key";
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "Invitation_email_teamId_key" ON "Invitation"("email", "teamId");
12 |
--------------------------------------------------------------------------------
/prisma/migrations/20231128064540_add_indices/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateIndex
2 | CREATE INDEX "Document_ownerId_idx" ON "Document"("ownerId");
3 |
4 | -- CreateIndex
5 | CREATE INDEX "Document_teamId_idx" ON "Document"("teamId");
6 |
7 | -- CreateIndex
8 | CREATE INDEX "DocumentPage_versionId_idx" ON "DocumentPage"("versionId");
9 |
10 | -- CreateIndex
11 | CREATE INDEX "DocumentVersion_documentId_idx" ON "DocumentVersion"("documentId");
12 |
13 | -- CreateIndex
14 | CREATE INDEX "Domain_userId_idx" ON "Domain"("userId");
15 |
16 | -- CreateIndex
17 | CREATE INDEX "Domain_teamId_idx" ON "Domain"("teamId");
18 |
19 | -- CreateIndex
20 | CREATE INDEX "Link_documentId_idx" ON "Link"("documentId");
21 |
22 | -- CreateIndex
23 | CREATE INDEX "View_linkId_idx" ON "View"("linkId");
24 |
25 | -- CreateIndex
26 | CREATE INDEX "View_documentId_idx" ON "View"("documentId");
27 |
--------------------------------------------------------------------------------
/prisma/migrations/20231204070250_remove_trial/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ALTER COLUMN "plan" SET DEFAULT 'free';
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20231207081407_add_reactions/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Reaction" (
3 | "id" TEXT NOT NULL,
4 | "viewId" TEXT NOT NULL,
5 | "pageNumber" INTEGER NOT NULL,
6 | "type" TEXT NOT NULL,
7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8 |
9 | CONSTRAINT "Reaction_pkey" PRIMARY KEY ("id")
10 | );
11 |
12 | -- CreateIndex
13 | CREATE INDEX "Reaction_viewId_idx" ON "Reaction"("viewId");
14 |
15 | -- AddForeignKey
16 | ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "View"("id") ON DELETE CASCADE ON UPDATE CASCADE;
17 |
--------------------------------------------------------------------------------
/prisma/migrations/20240110233134_add_disable_feedback/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "enableFeedback" BOOLEAN DEFAULT true;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240117020456_add_branding/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "enableCustomMetatag" BOOLEAN DEFAULT false,
3 | ADD COLUMN "metaDescription" TEXT,
4 | ADD COLUMN "metaImage" TEXT,
5 | ADD COLUMN "metaTitle" TEXT;
6 |
7 | -- CreateTable
8 | CREATE TABLE "Brand" (
9 | "id" TEXT NOT NULL,
10 | "logo" TEXT,
11 | "brandColor" TEXT,
12 | "accentColor" TEXT,
13 | "teamId" TEXT NOT NULL,
14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 | "updatedAt" TIMESTAMP(3) NOT NULL,
16 |
17 | CONSTRAINT "Brand_pkey" PRIMARY KEY ("id")
18 | );
19 |
20 | -- CreateIndex
21 | CREATE UNIQUE INDEX "Brand_teamId_key" ON "Brand"("teamId");
22 |
23 | -- AddForeignKey
24 | ALTER TABLE "Brand" ADD CONSTRAINT "Brand_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
25 |
--------------------------------------------------------------------------------
/prisma/migrations/20240202052149_add_email_authentication_to_link_and_view/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "emailAuthenticated" BOOLEAN NOT NULL DEFAULT false;
3 |
4 | -- AlterTable
5 | ALTER TABLE "View" ADD COLUMN "verified" BOOLEAN NOT NULL DEFAULT false;
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20240205170242_embedded_links/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "DocumentPage" ADD COLUMN "embeddedLinks" TEXT[];
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240212081614_add_downloaded_time_to_view/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "View" ADD COLUMN "downloadedAt" TIMESTAMP(3);
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240215035046_add_allow_deny_list_to_links/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `allowedEmails` on the `Link` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Link" DROP COLUMN "allowedEmails",
9 | ADD COLUMN "allowList" TEXT[],
10 | ADD COLUMN "denyList" TEXT[];
11 |
--------------------------------------------------------------------------------
/prisma/migrations/20240221042933_add_document_storage_type_enum/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "DocumentStorageType" AS ENUM ('S3_PATH', 'VERCEL_BLOB');
3 |
4 | -- AlterTable
5 | ALTER TABLE "Document" ADD COLUMN "storageType" "DocumentStorageType" NOT NULL DEFAULT 'VERCEL_BLOB';
6 |
7 | -- AlterTable
8 | ALTER TABLE "DocumentPage" ADD COLUMN "storageType" "DocumentStorageType" NOT NULL DEFAULT 'VERCEL_BLOB';
9 |
10 | -- AlterTable
11 | ALTER TABLE "DocumentVersion" ADD COLUMN "storageType" "DocumentStorageType" NOT NULL DEFAULT 'VERCEL_BLOB';
12 |
--------------------------------------------------------------------------------
/prisma/migrations/20240330062000_add_viewtype/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ViewType" AS ENUM ('DOCUMENT_VIEW', 'DATAROOM_VIEW');
3 |
4 | -- AlterTable
5 | ALTER TABLE "View" ADD COLUMN "viewType" "ViewType" NOT NULL DEFAULT 'DOCUMENT_VIEW';
6 |
7 | -- CreateIndex
8 | CREATE INDEX "View_dataroomId_idx" ON "View"("dataroomId");
9 |
10 | -- CreateIndex
11 | CREATE INDEX "View_dataroomViewId_idx" ON "View"("dataroomViewId");
12 |
13 |
--------------------------------------------------------------------------------
/prisma/migrations/20240401000000_add_dataroom_brand/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "DataroomBrand" (
3 | "id" TEXT NOT NULL,
4 | "logo" TEXT,
5 | "banner" TEXT,
6 | "brandColor" TEXT,
7 | "accentColor" TEXT,
8 | "dataroomId" TEXT NOT NULL,
9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10 | "updatedAt" TIMESTAMP(3) NOT NULL,
11 |
12 | CONSTRAINT "DataroomBrand_pkey" PRIMARY KEY ("id")
13 | );
14 |
15 | -- CreateIndex
16 | CREATE UNIQUE INDEX "DataroomBrand_dataroomId_key" ON "DataroomBrand"("dataroomId");
17 |
18 | -- AddForeignKey
19 | ALTER TABLE "DataroomBrand" ADD CONSTRAINT "DataroomBrand_dataroomId_fkey" FOREIGN KEY ("dataroomId") REFERENCES "Dataroom"("id") ON DELETE CASCADE ON UPDATE CASCADE;
20 |
21 |
--------------------------------------------------------------------------------
/prisma/migrations/20240408000000_add_viewer/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "View" ADD COLUMN "viewerId" TEXT;
3 |
4 | -- CreateTable
5 | CREATE TABLE "Viewer" (
6 | "id" TEXT NOT NULL,
7 | "email" TEXT NOT NULL,
8 | "verified" BOOLEAN NOT NULL DEFAULT false,
9 | "invitedAt" TIMESTAMP(3),
10 | "dataroomId" TEXT NOT NULL,
11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12 | "updatedAt" TIMESTAMP(3) NOT NULL,
13 |
14 | CONSTRAINT "Viewer_pkey" PRIMARY KEY ("id")
15 | );
16 |
17 | -- CreateIndex
18 | CREATE INDEX "Viewer_dataroomId_idx" ON "Viewer"("dataroomId");
19 |
20 | -- CreateIndex
21 | CREATE UNIQUE INDEX "Viewer_dataroomId_email_key" ON "Viewer"("dataroomId", "email");
22 |
23 | -- AddForeignKey
24 | ALTER TABLE "View" ADD CONSTRAINT "View_viewerId_fkey" FOREIGN KEY ("viewerId") REFERENCES "Viewer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
25 |
26 | -- AddForeignKey
27 | ALTER TABLE "Viewer" ADD CONSTRAINT "Viewer_dataroomId_fkey" FOREIGN KEY ("dataroomId") REFERENCES "Dataroom"("id") ON DELETE CASCADE ON UPDATE CASCADE;
28 |
29 |
--------------------------------------------------------------------------------
/prisma/migrations/20240424152839_add_screenprotection_to_link/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "enableScreenshotProtection" BOOLEAN DEFAULT false;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240511000000_add_team_limits/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Team" ADD COLUMN "limits" JSONB;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240520000000_add_vertical_to_document_version/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "DocumentVersion" ADD COLUMN "isVertical" BOOLEAN NOT NULL DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240521000000_add_manager_role/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "Role" ADD VALUE 'MANAGER';
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240712000000_add_page_metadata/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "DocumentPage" ADD COLUMN "metadata" JSONB,
3 | ADD COLUMN "pageLinks" JSONB;
4 |
5 |
--------------------------------------------------------------------------------
/prisma/migrations/20240730000000_update_link_defaults/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ALTER COLUMN "enableFeedback" SET DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240731000000_add_link_show_banner/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "showBanner" BOOLEAN DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240809000000_add_dataroom_order_index/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "DataroomDocument" ADD COLUMN "orderIndex" INTEGER;
3 |
4 | -- AlterTable
5 | ALTER TABLE "DataroomFolder" ADD COLUMN "orderIndex" INTEGER;
6 |
7 | -- CreateIndex
8 | CREATE INDEX "DataroomDocument_dataroomId_folderId_orderIndex_idx" ON "DataroomDocument"("dataroomId", "folderId", "orderIndex");
9 |
10 | -- CreateIndex
11 | CREATE INDEX "DataroomFolder_dataroomId_parentId_orderIndex_idx" ON "DataroomFolder"("dataroomId", "parentId", "orderIndex");
12 |
13 |
--------------------------------------------------------------------------------
/prisma/migrations/20240821000000_add_require_name/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Agreement" ADD COLUMN "requireName" BOOLEAN NOT NULL DEFAULT true;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240830000000_add_watermarks/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "enableWatermark" BOOLEAN DEFAULT false,
3 | ADD COLUMN "watermarkConfig" JSONB;
4 |
5 |
--------------------------------------------------------------------------------
/prisma/migrations/20240901000000_add_domain_default/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Domain" ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240902000000_add_link_presets/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "LinkPreset" (
3 | "id" TEXT NOT NULL,
4 | "name" TEXT NOT NULL,
5 | "teamId" TEXT NOT NULL,
6 | "enableCustomMetaTag" BOOLEAN DEFAULT false,
7 | "metaTitle" TEXT,
8 | "metaDescription" TEXT,
9 | "metaImage" TEXT,
10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 | "updatedAt" TIMESTAMP(3) NOT NULL,
12 |
13 | CONSTRAINT "LinkPreset_pkey" PRIMARY KEY ("id")
14 | );
15 |
16 | -- CreateIndex
17 | CREATE INDEX "LinkPreset_teamId_idx" ON "LinkPreset"("teamId");
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "LinkPreset" ADD CONSTRAINT "LinkPreset_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
21 |
22 |
--------------------------------------------------------------------------------
/prisma/migrations/20240915000000_add_advanced_mode/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Document" ADD COLUMN "advancedExcelEnabled" BOOLEAN NOT NULL DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20240916000000_add_content_type_to_document/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Document" ADD COLUMN "contentType" TEXT,
3 | ADD COLUMN "originalFile" TEXT;
4 |
5 | -- AlterTable
6 | ALTER TABLE "DocumentVersion" ADD COLUMN "contentType" TEXT,
7 | ADD COLUMN "originalFile" TEXT;
8 |
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240921000000_add_viewer_migration/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropForeignKey
2 | ALTER TABLE "Viewer" DROP CONSTRAINT "Viewer_dataroomId_fkey";
3 |
4 | -- DropIndex
5 | DROP INDEX "Viewer_dataroomId_email_key";
6 |
7 | -- AlterTable
8 | ALTER TABLE "Viewer" ALTER COLUMN "dataroomId" DROP NOT NULL,
9 | ALTER COLUMN "teamId" SET NOT NULL;
10 |
11 | -- CreateIndex
12 | CREATE UNIQUE INDEX "Viewer_teamId_email_key" ON "Viewer"("teamId", "email");
13 |
14 | -- AddForeignKey
15 | ALTER TABLE "Viewer" ADD CONSTRAINT "Viewer_dataroomId_fkey" FOREIGN KEY ("dataroomId") REFERENCES "Dataroom"("id") ON DELETE SET NULL ON UPDATE CASCADE;
16 |
17 |
--------------------------------------------------------------------------------
/prisma/migrations/20241004024010_add_favicon_column/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "metaFavicon" TEXT;
3 |
4 | -- AlterTable
5 | ALTER TABLE "LinkPreset" ADD COLUMN "metaFavicon" TEXT;
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20241020000000_add_teamid_to_link_and_view/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "teamId" TEXT;
3 |
4 | -- AlterTable
5 | ALTER TABLE "View" ADD COLUMN "teamId" TEXT;
6 |
7 | -- CreateIndex
8 | CREATE INDEX "Link_teamId_idx" ON "Link"("teamId");
9 |
10 | -- CreateIndex
11 | CREATE INDEX "View_teamId_idx" ON "View"("teamId");
12 |
13 | -- AddForeignKey
14 | ALTER TABLE "Link" ADD CONSTRAINT "Link_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "View" ADD CONSTRAINT "View_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
19 |
--------------------------------------------------------------------------------
/prisma/migrations/20241029000000_add_archived_view/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "View" ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20241118000000_add_filesize_and_downloadonly/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Document" ADD COLUMN "downloadOnly" BOOLEAN NOT NULL DEFAULT false;
3 |
4 | -- AlterTable
5 | ALTER TABLE "DocumentVersion" ADD COLUMN "fileSize" INTEGER;
6 |
7 |
--------------------------------------------------------------------------------
/prisma/migrations/20241123000000_add_viewer_notification_preferences/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Viewer" ADD COLUMN "notificationPreferences" JSONB;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20241126000000_add_screen_shield/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "screenShieldPercentage" INTEGER;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20241208000000_add_webhooks/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Webhook" (
3 | "id" TEXT NOT NULL,
4 | "pId" TEXT NOT NULL,
5 | "name" TEXT NOT NULL,
6 | "url" TEXT NOT NULL,
7 | "secret" TEXT NOT NULL,
8 | "triggers" JSONB NOT NULL,
9 | "teamId" TEXT NOT NULL,
10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 | "updatedAt" TIMESTAMP(3) NOT NULL,
12 |
13 | CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id")
14 | );
15 |
16 | -- CreateIndex
17 | CREATE UNIQUE INDEX "Webhook_pId_key" ON "Webhook"("pId");
18 |
19 | -- CreateIndex
20 | CREATE INDEX "Webhook_teamId_idx" ON "Webhook"("teamId");
21 |
22 | -- AddForeignKey
23 | ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
24 |
25 |
--------------------------------------------------------------------------------
/prisma/migrations/20241212000000_add_yir/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "UserTeam" ADD COLUMN "notificationPreferences" JSONB;
3 |
4 | -- CreateTable
5 | CREATE TABLE "YearInReview" (
6 | "id" TEXT NOT NULL,
7 | "teamId" TEXT NOT NULL,
8 | "status" TEXT NOT NULL DEFAULT 'pending',
9 | "attempts" INTEGER NOT NULL DEFAULT 0,
10 | "lastAttempted" TIMESTAMP(3),
11 | "error" TEXT,
12 | "stats" JSONB NOT NULL,
13 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14 | "updatedAt" TIMESTAMP(3) NOT NULL,
15 |
16 | CONSTRAINT "YearInReview_pkey" PRIMARY KEY ("id")
17 | );
18 |
19 | -- CreateIndex
20 | CREATE INDEX "YearInReview_status_attempts_idx" ON "YearInReview"("status", "attempts");
21 |
22 | -- CreateIndex
23 | CREATE INDEX "YearInReview_teamId_idx" ON "YearInReview"("teamId");
24 |
25 |
--------------------------------------------------------------------------------
/prisma/migrations/20250110000000_add_length_to_document_version/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "DocumentVersion" ADD COLUMN "length" INTEGER;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20250204000000_add_anonymous_group/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "ViewerGroup" ADD COLUMN "allowAll" BOOLEAN NOT NULL DEFAULT false,
3 | ADD COLUMN "domains" TEXT[];
4 |
5 |
--------------------------------------------------------------------------------
/prisma/migrations/20250217000000_add_contactid_to_user/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "contactId" TEXT;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20250217000000_remove_link_column/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" DROP COLUMN "screenShieldPercentage";
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20250310000000_rename_conversation_table/migration.sql:
--------------------------------------------------------------------------------
1 | -- Rename the table (this preserves all data)
2 | ALTER TABLE "Conversation" RENAME TO "Chat";
3 |
4 | -- Rename constraints and indices to match the new table name
5 | ALTER INDEX "Conversation_pkey" RENAME TO "Chat_pkey";
6 | ALTER INDEX "Conversation_threadId_key" RENAME TO "Chat_threadId_key";
7 | ALTER INDEX "Conversation_threadId_idx" RENAME TO "Chat_threadId_idx";
8 | ALTER INDEX "Conversation_userId_documentId_key" RENAME TO "Chat_userId_documentId_key";
9 | ALTER INDEX "Conversation_threadId_documentId_key" RENAME TO "Chat_threadId_documentId_key";
10 |
11 | -- Rename foreign key constraints
12 | ALTER TABLE "Chat" RENAME CONSTRAINT "Conversation_userId_fkey" TO "Chat_userId_fkey";
13 | ALTER TABLE "Chat" RENAME CONSTRAINT "Conversation_documentId_fkey" TO "Chat_documentId_fkey";
14 |
--------------------------------------------------------------------------------
/prisma/migrations/20250425000000_update_link_presets/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Agreement" ADD COLUMN "deletedAt" TIMESTAMP(3),
3 | ADD COLUMN "deletedBy" TEXT;
4 |
5 | -- AlterTable
6 | ALTER TABLE "LinkPreset" ADD COLUMN "allowDownload" BOOLEAN DEFAULT false,
7 | ADD COLUMN "allowList" TEXT[],
8 | ADD COLUMN "denyList" TEXT[],
9 | ADD COLUMN "emailAuthenticated" BOOLEAN DEFAULT false,
10 | ADD COLUMN "emailProtected" BOOLEAN DEFAULT true,
11 | ADD COLUMN "enableAllowList" BOOLEAN DEFAULT false,
12 | ADD COLUMN "enableDenyList" BOOLEAN DEFAULT false,
13 | ADD COLUMN "enablePassword" BOOLEAN DEFAULT false,
14 | ADD COLUMN "enableWatermark" BOOLEAN DEFAULT false,
15 | ADD COLUMN "expiresAt" TIMESTAMP(3),
16 | ADD COLUMN "expiresIn" INTEGER,
17 | ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false,
18 | ADD COLUMN "pId" TEXT,
19 | ADD COLUMN "password" TEXT,
20 | ADD COLUMN "watermarkConfig" JSONB;
21 |
22 | -- CreateIndex
23 | CREATE UNIQUE INDEX "LinkPreset_pId_key" ON "LinkPreset"("pId" ASC);
24 |
25 |
--------------------------------------------------------------------------------
/prisma/migrations/20250502000000_add_additional_present_fields/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "LinkPreset" ADD COLUMN "agreementId" TEXT,
3 | ADD COLUMN "customFields" JSONB,
4 | ADD COLUMN "enableAgreement" BOOLEAN DEFAULT false,
5 | ADD COLUMN "enableCustomFields" BOOLEAN DEFAULT false,
6 | ADD COLUMN "enableScreenshotProtection" BOOLEAN DEFAULT false;
7 |
8 |
--------------------------------------------------------------------------------
/prisma/migrations/20250511000000_add_index_file_to_links/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Link" ADD COLUMN "enableIndexFile" BOOLEAN DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20250513000000_add_notification_to_dataroom/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Dataroom" ADD COLUMN "enableChangeNotifications" BOOLEAN NOT NULL DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20250516000000_add_advanced_mode_to_team/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Team" ADD COLUMN "enableExcelAdvancedMode" BOOLEAN NOT NULL DEFAULT false;
3 |
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20250526000000_add_status_to_users/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "UserTeam" ADD COLUMN "blockedAt" TIMESTAMP(3),
3 | ADD COLUMN "status" TEXT NOT NULL DEFAULT 'ACTIVE';
4 |
5 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/public/_example/papermark-example-document.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_example/papermark-example-document.pdf
--------------------------------------------------------------------------------
/public/_example/papermark-example-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_example/papermark-example-page.png
--------------------------------------------------------------------------------
/public/_icons/other.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/_icons/sheet-light.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/_icons/sheet.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/_static/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/Inter-Bold.ttf
--------------------------------------------------------------------------------
/public/_static/blank.gif:
--------------------------------------------------------------------------------
1 | GIF89a ÿÿÿ !ù
2 | , L ;
--------------------------------------------------------------------------------
/public/_static/macos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/macos.png
--------------------------------------------------------------------------------
/public/_static/meta-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/meta-image.png
--------------------------------------------------------------------------------
/public/_static/papermark-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/papermark-banner.png
--------------------------------------------------------------------------------
/public/_static/papermark-p.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/_static/testimonials/backtrace.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/testimonials/backtrace.jpeg
--------------------------------------------------------------------------------
/public/_static/testimonials/jaski.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/testimonials/jaski.jpeg
--------------------------------------------------------------------------------
/public/_static/testimonials/steven.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/_static/testimonials/steven.jpeg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/public/favicon.ico
--------------------------------------------------------------------------------
/styles/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mfts/papermark/bcdbdf1c3b5cfdc12928cacaf23972fb7a88f04a/styles/Inter-Regular.ttf
--------------------------------------------------------------------------------
/styles/custom-viewer-styles.css:
--------------------------------------------------------------------------------
1 | .viewer-container img {
2 | /* Prevent long-press context menu on mobile */
3 | -webkit-touch-callout: none;
4 | -webkit-user-select: none;
5 | -moz-user-select: none;
6 | -ms-user-select: none;
7 | user-select: none;
8 |
9 | /* Prevent image highlighting */
10 | -webkit-tap-highlight-color: transparent;
11 |
12 | /* Disable pointer events for saving */
13 | pointer-events: auto;
14 | }
15 |
16 | .viewer-image-mobile {
17 | -webkit-user-drag: none !important;
18 | -webkit-user-select: none !important;
19 | -moz-user-select: none !important;
20 | -ms-user-select: none !important;
21 | user-select: none !important;
22 | -webkit-touch-callout: none !important;
23 | -webkit-tap-highlight-color: transparent !important;
24 | pointer-events: auto !important;
25 | }
26 |
--------------------------------------------------------------------------------
/trigger.config.ts:
--------------------------------------------------------------------------------
1 | import { ffmpeg } from "@trigger.dev/build/extensions/core";
2 | import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
3 | import { defineConfig, timeout } from "@trigger.dev/sdk/v3";
4 |
5 | export default defineConfig({
6 | project: "proj_plmsfqvqunboixacjjus",
7 | dirs: ["./lib/trigger"],
8 | maxDuration: timeout.None, // no max duration
9 | retries: {
10 | enabledInDev: false,
11 | default: {
12 | maxAttempts: 3,
13 | minTimeoutInMs: 1000,
14 | maxTimeoutInMs: 10000,
15 | factor: 2,
16 | randomize: true,
17 | },
18 | },
19 | build: {
20 | extensions: [
21 | prismaExtension({
22 | schema: "prisma/schema/schema.prisma",
23 | }),
24 | ffmpeg(),
25 | ],
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "downlevelIteration": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "paths": {
19 | "@/*": ["./*"]
20 | },
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ]
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "**/*.ts",
30 | "**/*.tsx",
31 | ".next/types/**/*.ts",
32 | "trigger.config.ts"
33 | ],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "pages/api/mupdf/convert-page.ts": {
4 | "memory": 2048
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------