├── .gitignore ├── LICENSE ├── README.md ├── custom.d.ts ├── dist ├── cjs │ ├── index.js │ ├── index.js.map │ └── types │ │ ├── components │ │ ├── CommentBlocks.d.ts │ │ ├── CommentDisplay.d.ts │ │ ├── CommentsList.d.ts │ │ ├── CreateCommentModal.d.ts │ │ ├── EnabledPagebloxButton.d.ts │ │ ├── InstructionsPopup.d.ts │ │ ├── LoginModal.d.ts │ │ ├── PublishedComment.d.ts │ │ └── Toolbar.d.ts │ │ ├── index.d.ts │ │ ├── pageblox.d.ts │ │ └── utils │ │ ├── ItemTypes.d.ts │ │ ├── calculateScroll.d.ts │ │ ├── firebase-config.d.ts │ │ ├── getPathTo.d.ts │ │ ├── hooks.d.ts │ │ └── useWindowSize.d.ts ├── esm │ ├── index.js │ ├── index.js.map │ └── types │ │ ├── components │ │ ├── CommentBlocks.d.ts │ │ ├── CommentDisplay.d.ts │ │ ├── CommentsList.d.ts │ │ ├── CreateCommentModal.d.ts │ │ ├── EnabledPagebloxButton.d.ts │ │ ├── InstructionsPopup.d.ts │ │ ├── LoginModal.d.ts │ │ ├── PublishedComment.d.ts │ │ └── Toolbar.d.ts │ │ ├── index.d.ts │ │ ├── pageblox.d.ts │ │ └── utils │ │ ├── ItemTypes.d.ts │ │ ├── calculateScroll.d.ts │ │ ├── firebase-config.d.ts │ │ ├── getPathTo.d.ts │ │ ├── hooks.d.ts │ │ └── useWindowSize.d.ts └── index.d.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── rollup.config.mjs ├── src ├── .DS_Store ├── components │ ├── CommentBlocks.tsx │ ├── CommentDisplay.tsx │ ├── CommentsList.tsx │ ├── CreateCommentModal.tsx │ ├── EnabledPagebloxButton.tsx │ ├── InstructionsPopup.tsx │ ├── LoginModal.tsx │ ├── PublishedComment.tsx │ └── Toolbar.tsx ├── images │ └── pageblox-logo.svg ├── index.css ├── index.ts ├── pageblox.tsx └── utils │ ├── ItemTypes.ts │ ├── calculateScroll.ts │ ├── firebase-config.ts │ ├── getPathTo.ts │ ├── hooks.ts │ └── useWindowSize.ts ├── tailwind.config.js ├── tsconfig.json └── types ├── .DS_Store ├── components ├── ClientKeyAlert.d.ts ├── CommentBlocks.d.ts ├── CommentDisplay.d.ts ├── CommentsList.d.ts ├── CreateCommentModal.d.ts ├── DropdownComponent.d.ts ├── EnabledPagebloxButton.d.ts ├── ImagePreviewModal.d.ts ├── InstructionsPopup.d.ts ├── LoginModal.d.ts ├── LoginScreen.d.ts ├── PublishedComment.d.ts ├── RegisterModal.d.ts ├── RepliesModal.d.ts ├── Sidebar.d.ts ├── Toolbar.d.ts └── UnpublishedComment.d.ts ├── index.d.ts ├── pageblox.d.ts ├── stories ├── Button.d.ts ├── Header.d.ts ├── Page.d.ts └── Page.stories.d.ts └── utils ├── ItemTypes.d.ts ├── calculateScroll.d.ts ├── firebase-config.d.ts ├── getPathTo.d.ts ├── hooks.d.ts └── useWindowSize.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 pageblox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | Pageblox 4 |
5 |
6 |
7 |

8 | PRs Welcome 9 | 10 |

11 | 12 | # Pageblox 13 | [Pageblox](https://www.pageblox.io) is an open-source React tool that generates preview links with a real-time, collaboration review interface. It replaces the back-and-forth screen recordings and conversations that usually occur when giving feedback on UX/UI changes. 14 | 15 | Demo Screenshot 16 | 17 | ## Installation 18 | 19 | ### Generating a project ID 20 | 21 | Start by generating a unique project key, so that comments/replies have a dependency to your repo. 22 | You can visit https://www.uuidtools.com/ to generate a UUID. 23 | 24 | Store this somewhere, to pass in as a prop into your described further below. 25 | 26 | ### Github Actions 27 | 28 | To create preview links, a Github Actions workflow is required. This will build the React project on every push to a branch and deploy it using Github Pages. The URL will be in the form of ```https://.github.io//```. 29 | 30 | #### Creating the workflow file 31 | 32 | Start by creating a ```.github/workflows``` folder, and add a ```deploy.yml``` file. Depending on the React library used, the build folder path will change. The following example is for a Vite project, so the folder path is ```dist```. 33 | 34 | To toggle Pageblox, you want to create an environment variable in your repo, and set it equal to ```true``` upon build. 35 | 36 | Lastly, ensure that your repository is configured to have its GitHub Pages site deployed from a branch, by setting the source for the deployment under Settings > Pages of your repository to Deploy from branch. Best practice is to name this branch```gh-pages```. You can learn more about deploying from a branch here. 37 | 38 | ```yml 39 | name: Build and Deploy 40 | on: [push] 41 | permissions: 42 | contents: write 43 | jobs: 44 | build-and-deploy: 45 | concurrency: ci-${{ github.ref }} 46 | runs-on: ubuntu-latest 47 | env: 48 | VITE_DISPLAY_PREVIEW: "true" 49 | steps: 50 | - name: Checkout 🛎️ 51 | uses: actions/checkout@v3 52 | - name: Install and Build 🔧 53 | run: | 54 | npm ci 55 | npm run build 56 | 57 | - name: Deploy 🚀 58 | uses: JamesIves/github-pages-deploy-action@v4 59 | with: 60 | folder: dist # The folder the action should deploy. Adjust this value based on your react library. 61 | ``` 62 | 63 | ### React Library 64 | 65 | To install the library, run the following command inside your root directory: 66 | 67 | ```bash 68 | npm install pageblox-react 69 | ``` 70 | 71 | ## Usage 72 | 73 | After installation, you need to wrap your application with ```PagebloxProvider``` and import the ```pageblox.css``` file. 74 | 75 | Finally, create an environment variable named ```DISPLAY_PAGEBLOX``` with the appropriate prefix depending on your platform, and initialize it to ```false```. 76 | 77 | 78 | #### Using NextJS 79 | 80 | Use ```_app.js``` to utilize the pageblox wrapper. You can find [more info here.](https://nextjs.org/docs/advanced-features/custom-app) 81 | 82 | ```jsx 83 | import { PagebloxProvider } from 'pageblox-react' 84 | import 'pageblox-react/dist/pageblox.css' 85 | 86 | export default function App({ Component, pageProps }) { 87 | return ( 88 | 90 | 91 | 92 | ) 93 | } 94 | ``` 95 | 96 | #### Using Create React App / Vite Example 97 | 98 | ```jsx 99 | import React from 'react' 100 | import ReactDOM from 'react-dom/client' 101 | import { BrowserRouter } from 'react-router-dom' 102 | import { PagebloxProvider } from 'pageblox-react' 103 | import App from './App.jsx' 104 | import './index.css' 105 | import "pageblox-react/dist/pageblox.css" 106 | 107 | ReactDOM.createRoot(document.getElementById('root')).render( 108 | 109 | 111 | 112 | 113 | 114 | 115 | , 116 | ) 117 | ``` 118 | 119 | 120 | #### Generating a preview link 121 | 122 | Once you've imported the ```PagebloxProvider``` & CSS file, as well as created an appropriate environment variable used in the actions workflow, go ahead and push changes to a remote branch. 123 | 124 | You can view the progress of this workflow under the Actions tab in your repository. 125 | 126 | Once it's completed, visit ```https://.github.io//``` and you'll see the Pageblox widget on the bottom right of your app. Click on it to start commenting! 127 | 128 | ## Where to get help 129 | 130 | You can raise issues in this repo, or feel free to e-mail at bhavan@pageblox.io with any questions. 131 | 132 | ## Important Links 133 | 134 | - [Use this](https://www.uuidtools.com/) to generate a unique project key 135 | - E-mail bhavan@pageblox.io to get the latest updates & support. 136 | 137 | ## Acknowledgements 138 | - JamesIves/github-pages-deploy-action (MIT) -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*svg" { 2 | const content: any; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /dist/cjs/types/components/CommentBlocks.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | export interface CommentBlocksProps { 4 | blocks: Comment[]; 5 | handleShowComment: (id: string) => void; 6 | pageRef: RefObject; 7 | selectedComment: Comment | null; 8 | setSelectedComment: (comment: Comment | null) => void; 9 | setShowCommentView: (showCommentView: boolean) => void; 10 | } 11 | declare const CommentBlocks: ({ blocks, handleShowComment, pageRef, selectedComment, setSelectedComment, setShowCommentView, }: CommentBlocksProps) => JSX.Element; 12 | export default CommentBlocks; 13 | -------------------------------------------------------------------------------- /dist/cjs/types/components/CommentDisplay.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface CommentDisplayProps { 4 | pageRef: RefObject; 5 | selectedComment: Comment | null; 6 | setSelectedComment: (comment: Comment | null) => void; 7 | deleteComment: (id: string) => void; 8 | fetchReplies: (id: string) => Reply[]; 9 | saveReply: (commentId: string, message: string) => void; 10 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 11 | setShowCommentView: (showCommentView: boolean) => void; 12 | } 13 | declare const CommentDisplay: ({ pageRef, selectedComment, setSelectedComment, deleteComment, fetchReplies, saveReply, onResolveChange, setShowCommentView, }: CommentDisplayProps) => JSX.Element; 14 | export default CommentDisplay; 15 | -------------------------------------------------------------------------------- /dist/cjs/types/components/CommentsList.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface CommentsListProps { 4 | pageRef: RefObject; 5 | showComments: boolean; 6 | setShowComments: (showComments: boolean) => void; 7 | comments: Comment[]; 8 | replies: Reply[]; 9 | navigateToComment: (commentId: string | null) => void; 10 | } 11 | declare const CommentsList: ({ pageRef, showComments, setShowComments, comments, replies, navigateToComment, }: CommentsListProps) => JSX.Element; 12 | export default CommentsList; 13 | -------------------------------------------------------------------------------- /dist/cjs/types/components/CreateCommentModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface CreateCommentModalInterface { 3 | showCreateView: boolean; 4 | toggleCreateModal: (shouldShow: boolean) => void; 5 | authorName: string; 6 | authorColor: string; 7 | onSaveComment: (comment: string, file?: File | null) => void; 8 | } 9 | declare const CreateCommentModal: ({ showCreateView, toggleCreateModal, authorName, authorColor, onSaveComment, }: CreateCommentModalInterface) => JSX.Element; 10 | export default CreateCommentModal; 11 | -------------------------------------------------------------------------------- /dist/cjs/types/components/EnabledPagebloxButton.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface EnabledPagebloxButtonInterface { 3 | shouldDisplay: boolean; 4 | reviewMode: boolean; 5 | onWidgetClick: () => void; 6 | } 7 | declare const EnabledPagebloxButton: ({ shouldDisplay, reviewMode, onWidgetClick, }: EnabledPagebloxButtonInterface) => JSX.Element; 8 | export default EnabledPagebloxButton; 9 | -------------------------------------------------------------------------------- /dist/cjs/types/components/InstructionsPopup.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface InstructionsPopupProps { 3 | showInstructionsPopup: boolean; 4 | setShowInstructionsPopup: (show: boolean) => void; 5 | } 6 | declare const InstructionsPopup: ({ showInstructionsPopup, setShowInstructionsPopup, }: InstructionsPopupProps) => JSX.Element; 7 | export default InstructionsPopup; 8 | -------------------------------------------------------------------------------- /dist/cjs/types/components/LoginModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface LoginModalProps { 3 | showModal: boolean; 4 | setShowModal: (showModal: boolean) => void; 5 | setReviewMode: (reviewMode: boolean) => void; 6 | setDisplayName: (name: string | null) => void; 7 | } 8 | export default function LoginModal({ showModal, setShowModal, setReviewMode, setDisplayName, }: LoginModalProps): JSX.Element | null; 9 | export {}; 10 | -------------------------------------------------------------------------------- /dist/cjs/types/components/PublishedComment.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | interface PublishedCommentProps { 4 | block: Comment; 5 | pageRef: RefObject; 6 | setSelectedComment: (comment: Comment | null) => void; 7 | showReplies: (id: string) => void; 8 | shouldHideLabel?: boolean; 9 | isSelectedComment?: boolean; 10 | setShowCommentView: (showCommentView: boolean) => void; 11 | } 12 | export declare const PublishedComment: ({ block, pageRef, setSelectedComment, showReplies, shouldHideLabel, isSelectedComment, setShowCommentView, }: PublishedCommentProps) => JSX.Element; 13 | export {}; 14 | -------------------------------------------------------------------------------- /dist/cjs/types/components/Toolbar.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface ToolbarProps { 4 | showCommentView: boolean; 5 | setShowCommentView: (showCommentView: boolean) => void; 6 | pageRef: RefObject; 7 | comments: Comment[]; 8 | replies: Reply[]; 9 | deleteComment: (id: string) => void; 10 | fetchReplies: (id: string) => Reply[]; 11 | saveReply: (commentId: string, message: string) => void; 12 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 13 | selectedComment: Comment | null; 14 | setSelectedComment: (comment: Comment | null) => void; 15 | setDisplayName: (name: string | null) => void; 16 | setReviewMode: (reviewMode: boolean) => void; 17 | } 18 | declare const Toolbar: ({ showCommentView, setShowCommentView, pageRef, comments, replies, deleteComment, fetchReplies, saveReply, onResolveChange, selectedComment, setSelectedComment, setDisplayName, setReviewMode, }: ToolbarProps) => JSX.Element; 19 | export default Toolbar; 20 | -------------------------------------------------------------------------------- /dist/cjs/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./pageblox"; 2 | -------------------------------------------------------------------------------- /dist/cjs/types/pageblox.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export interface Comment { 3 | id: string; 4 | timestamp: any; 5 | profileColor: string; 6 | profileName: string; 7 | comment: string; 8 | resolved: boolean; 9 | dom: string; 10 | x: number; 11 | y: number; 12 | uploadedFilePath?: string; 13 | } 14 | export interface Reply { 15 | id: string; 16 | author: string; 17 | profileColor: string; 18 | message: string; 19 | parent_comment_id: string; 20 | timestamp: any; 21 | } 22 | interface PagebloxProviderInterface { 23 | children: JSX.Element; 24 | projectKey: string; 25 | excludePaths?: string[]; 26 | enabled: boolean; 27 | } 28 | declare const PagebloxProvider: (pagebloxProvider: PagebloxProviderInterface) => JSX.Element; 29 | export { PagebloxProvider }; 30 | -------------------------------------------------------------------------------- /dist/cjs/types/utils/ItemTypes.d.ts: -------------------------------------------------------------------------------- 1 | interface ItemTypes { 2 | COMMENT: string; 3 | } 4 | export declare const ItemTypes: ItemTypes; 5 | export {}; 6 | -------------------------------------------------------------------------------- /dist/cjs/types/utils/calculateScroll.d.ts: -------------------------------------------------------------------------------- 1 | declare const calculateScroll: (element: HTMLElement | null) => number[]; 2 | export { calculateScroll }; 3 | -------------------------------------------------------------------------------- /dist/cjs/types/utils/firebase-config.d.ts: -------------------------------------------------------------------------------- 1 | export declare const database: import("@firebase/firestore").Firestore; 2 | export declare const storage: import("@firebase/storage").FirebaseStorage; 3 | export declare const auth: import("@firebase/auth").Auth; 4 | -------------------------------------------------------------------------------- /dist/cjs/types/utils/getPathTo.d.ts: -------------------------------------------------------------------------------- 1 | declare function getPathTo(element: HTMLElement): string; 2 | export { getPathTo }; 3 | -------------------------------------------------------------------------------- /dist/cjs/types/utils/hooks.d.ts: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | export declare const useOutsideClick: (callback: () => void, boundsRef: RefObject) => React.MutableRefObject; 3 | -------------------------------------------------------------------------------- /dist/cjs/types/utils/useWindowSize.d.ts: -------------------------------------------------------------------------------- 1 | interface Size { 2 | width: number | undefined; 3 | height: number | undefined; 4 | } 5 | export declare function useWindowSize(): Size; 6 | export {}; 7 | -------------------------------------------------------------------------------- /dist/esm/types/components/CommentBlocks.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | export interface CommentBlocksProps { 4 | blocks: Comment[]; 5 | handleShowComment: (id: string) => void; 6 | pageRef: RefObject; 7 | selectedComment: Comment | null; 8 | setSelectedComment: (comment: Comment | null) => void; 9 | setShowCommentView: (showCommentView: boolean) => void; 10 | } 11 | declare const CommentBlocks: ({ blocks, handleShowComment, pageRef, selectedComment, setSelectedComment, setShowCommentView, }: CommentBlocksProps) => JSX.Element; 12 | export default CommentBlocks; 13 | -------------------------------------------------------------------------------- /dist/esm/types/components/CommentDisplay.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface CommentDisplayProps { 4 | pageRef: RefObject; 5 | selectedComment: Comment | null; 6 | setSelectedComment: (comment: Comment | null) => void; 7 | deleteComment: (id: string) => void; 8 | fetchReplies: (id: string) => Reply[]; 9 | saveReply: (commentId: string, message: string) => void; 10 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 11 | setShowCommentView: (showCommentView: boolean) => void; 12 | } 13 | declare const CommentDisplay: ({ pageRef, selectedComment, setSelectedComment, deleteComment, fetchReplies, saveReply, onResolveChange, setShowCommentView, }: CommentDisplayProps) => JSX.Element; 14 | export default CommentDisplay; 15 | -------------------------------------------------------------------------------- /dist/esm/types/components/CommentsList.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface CommentsListProps { 4 | pageRef: RefObject; 5 | showComments: boolean; 6 | setShowComments: (showComments: boolean) => void; 7 | comments: Comment[]; 8 | replies: Reply[]; 9 | navigateToComment: (commentId: string | null) => void; 10 | } 11 | declare const CommentsList: ({ pageRef, showComments, setShowComments, comments, replies, navigateToComment, }: CommentsListProps) => JSX.Element; 12 | export default CommentsList; 13 | -------------------------------------------------------------------------------- /dist/esm/types/components/CreateCommentModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface CreateCommentModalInterface { 3 | showCreateView: boolean; 4 | toggleCreateModal: (shouldShow: boolean) => void; 5 | authorName: string; 6 | authorColor: string; 7 | onSaveComment: (comment: string, file?: File | null) => void; 8 | } 9 | declare const CreateCommentModal: ({ showCreateView, toggleCreateModal, authorName, authorColor, onSaveComment, }: CreateCommentModalInterface) => JSX.Element; 10 | export default CreateCommentModal; 11 | -------------------------------------------------------------------------------- /dist/esm/types/components/EnabledPagebloxButton.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface EnabledPagebloxButtonInterface { 3 | shouldDisplay: boolean; 4 | reviewMode: boolean; 5 | onWidgetClick: () => void; 6 | } 7 | declare const EnabledPagebloxButton: ({ shouldDisplay, reviewMode, onWidgetClick, }: EnabledPagebloxButtonInterface) => JSX.Element; 8 | export default EnabledPagebloxButton; 9 | -------------------------------------------------------------------------------- /dist/esm/types/components/InstructionsPopup.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface InstructionsPopupProps { 3 | showInstructionsPopup: boolean; 4 | setShowInstructionsPopup: (show: boolean) => void; 5 | } 6 | declare const InstructionsPopup: ({ showInstructionsPopup, setShowInstructionsPopup, }: InstructionsPopupProps) => JSX.Element; 7 | export default InstructionsPopup; 8 | -------------------------------------------------------------------------------- /dist/esm/types/components/LoginModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface LoginModalProps { 3 | showModal: boolean; 4 | setShowModal: (showModal: boolean) => void; 5 | setReviewMode: (reviewMode: boolean) => void; 6 | setDisplayName: (name: string | null) => void; 7 | } 8 | export default function LoginModal({ showModal, setShowModal, setReviewMode, setDisplayName, }: LoginModalProps): JSX.Element | null; 9 | export {}; 10 | -------------------------------------------------------------------------------- /dist/esm/types/components/PublishedComment.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | interface PublishedCommentProps { 4 | block: Comment; 5 | pageRef: RefObject; 6 | setSelectedComment: (comment: Comment | null) => void; 7 | showReplies: (id: string) => void; 8 | shouldHideLabel?: boolean; 9 | isSelectedComment?: boolean; 10 | setShowCommentView: (showCommentView: boolean) => void; 11 | } 12 | export declare const PublishedComment: ({ block, pageRef, setSelectedComment, showReplies, shouldHideLabel, isSelectedComment, setShowCommentView, }: PublishedCommentProps) => JSX.Element; 13 | export {}; 14 | -------------------------------------------------------------------------------- /dist/esm/types/components/Toolbar.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface ToolbarProps { 4 | showCommentView: boolean; 5 | setShowCommentView: (showCommentView: boolean) => void; 6 | pageRef: RefObject; 7 | comments: Comment[]; 8 | replies: Reply[]; 9 | deleteComment: (id: string) => void; 10 | fetchReplies: (id: string) => Reply[]; 11 | saveReply: (commentId: string, message: string) => void; 12 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 13 | selectedComment: Comment | null; 14 | setSelectedComment: (comment: Comment | null) => void; 15 | setDisplayName: (name: string | null) => void; 16 | setReviewMode: (reviewMode: boolean) => void; 17 | } 18 | declare const Toolbar: ({ showCommentView, setShowCommentView, pageRef, comments, replies, deleteComment, fetchReplies, saveReply, onResolveChange, selectedComment, setSelectedComment, setDisplayName, setReviewMode, }: ToolbarProps) => JSX.Element; 19 | export default Toolbar; 20 | -------------------------------------------------------------------------------- /dist/esm/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./pageblox"; 2 | -------------------------------------------------------------------------------- /dist/esm/types/pageblox.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export interface Comment { 3 | id: string; 4 | timestamp: any; 5 | profileColor: string; 6 | profileName: string; 7 | comment: string; 8 | resolved: boolean; 9 | dom: string; 10 | x: number; 11 | y: number; 12 | uploadedFilePath?: string; 13 | } 14 | export interface Reply { 15 | id: string; 16 | author: string; 17 | profileColor: string; 18 | message: string; 19 | parent_comment_id: string; 20 | timestamp: any; 21 | } 22 | interface PagebloxProviderInterface { 23 | children: JSX.Element; 24 | projectKey: string; 25 | excludePaths?: string[]; 26 | enabled: boolean; 27 | } 28 | declare const PagebloxProvider: (pagebloxProvider: PagebloxProviderInterface) => JSX.Element; 29 | export { PagebloxProvider }; 30 | -------------------------------------------------------------------------------- /dist/esm/types/utils/ItemTypes.d.ts: -------------------------------------------------------------------------------- 1 | interface ItemTypes { 2 | COMMENT: string; 3 | } 4 | export declare const ItemTypes: ItemTypes; 5 | export {}; 6 | -------------------------------------------------------------------------------- /dist/esm/types/utils/calculateScroll.d.ts: -------------------------------------------------------------------------------- 1 | declare const calculateScroll: (element: HTMLElement | null) => number[]; 2 | export { calculateScroll }; 3 | -------------------------------------------------------------------------------- /dist/esm/types/utils/firebase-config.d.ts: -------------------------------------------------------------------------------- 1 | export declare const database: import("@firebase/firestore").Firestore; 2 | export declare const storage: import("@firebase/storage").FirebaseStorage; 3 | export declare const auth: import("@firebase/auth").Auth; 4 | -------------------------------------------------------------------------------- /dist/esm/types/utils/getPathTo.d.ts: -------------------------------------------------------------------------------- 1 | declare function getPathTo(element: HTMLElement): string; 2 | export { getPathTo }; 3 | -------------------------------------------------------------------------------- /dist/esm/types/utils/hooks.d.ts: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | export declare const useOutsideClick: (callback: () => void, boundsRef: RefObject) => React.MutableRefObject; 3 | -------------------------------------------------------------------------------- /dist/esm/types/utils/useWindowSize.d.ts: -------------------------------------------------------------------------------- 1 | interface Size { 2 | width: number | undefined; 3 | height: number | undefined; 4 | } 5 | export declare function useWindowSize(): Size; 6 | export {}; 7 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface Comment { 3 | id: string; 4 | timestamp: any; 5 | profileColor: string; 6 | profileName: string; 7 | comment: string; 8 | resolved: boolean; 9 | dom: string; 10 | x: number; 11 | y: number; 12 | uploadedFilePath?: string; 13 | } 14 | interface Reply { 15 | id: string; 16 | author: string; 17 | profileColor: string; 18 | message: string; 19 | parent_comment_id: string; 20 | timestamp: any; 21 | } 22 | interface PagebloxProviderInterface { 23 | children: JSX.Element; 24 | projectKey: string; 25 | excludePaths?: string[]; 26 | enabled: boolean; 27 | } 28 | declare const PagebloxProvider: (pagebloxProvider: PagebloxProviderInterface) => JSX.Element; 29 | 30 | export { Comment, PagebloxProvider, Reply }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pageblox-react", 3 | "version": "1.3.16", 4 | "description": "React library to review UX/UI code changes in the browser", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/pageblox-org/pageblox-react.git" 8 | }, 9 | "main": "dist/cjs/index.js", 10 | "module": "dist/esm/index.js", 11 | "files": [ 12 | "dist" 13 | ], 14 | "types": "dist/index.d.ts", 15 | "scripts": { 16 | "build": "rm -rf dist/ && npm run build:esm && npm run build:cjs && npm run build:styles", 17 | "build:esm": "tsc", 18 | "build:cjs": "tsc --module CommonJS --outDir dist/cjs", 19 | "build:styles": "postcss src/index.css -o dist/pageblox.css", 20 | "rollup": "rollup -c" 21 | }, 22 | "author": "Pageblox", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@babel/core": "^7.20.12", 26 | "@rollup/plugin-commonjs": "^24.0.0", 27 | "@rollup/plugin-image": "^3.0.2", 28 | "@rollup/plugin-node-resolve": "^15.0.1", 29 | "@rollup/plugin-terser": "^0.3.0", 30 | "@rollup/plugin-typescript": "^11.0.0", 31 | "@types/node": "^18.11.18", 32 | "@types/react": "^18.0.26", 33 | "@types/react-dom": "^18.0.10", 34 | "autoprefixer": "^10.4.13", 35 | "babel-loader": "^8.3.0", 36 | "date-fns": "^2.29.3", 37 | "firebase": "^9.15.0", 38 | "postcss": "^8.4.21", 39 | "postcss-cli": "^10.1.0", 40 | "react": "^18.2.0", 41 | "react-dnd": "^16.0.1", 42 | "react-dnd-html5-backend": "^16.0.1", 43 | "react-dom": "^18.2.0", 44 | "react-uuid": "^2.0.0", 45 | "rollup": "^3.9.1", 46 | "rollup-plugin-dts": "^5.1.1", 47 | "rollup-plugin-peer-deps-external": "^2.2.4", 48 | "rollup-plugin-postcss": "^4.0.2", 49 | "tailwindcss": "^3.2.7", 50 | "typescript": "^4.9.4" 51 | }, 52 | "peerDependencies": { 53 | "autoprefixer": "^10.4.13", 54 | "postcss": "^8.4.21", 55 | "react": "^18.2.0", 56 | "react-dom": "^18.2.0", 57 | "tailwindcss": "^3.2.7", 58 | "typescript": "^4.9.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import dts from "rollup-plugin-dts"; 5 | import postcss from "rollup-plugin-postcss"; 6 | import image from "@rollup/plugin-image"; 7 | import terser from "@rollup/plugin-terser"; 8 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 9 | 10 | import packageJson from "./package.json" assert { type: "json" }; 11 | 12 | export default [ 13 | { 14 | input: "src/index.ts", 15 | output: [ 16 | { 17 | file: packageJson.main, 18 | format: "cjs", 19 | sourcemap: true, 20 | }, 21 | { 22 | file: packageJson.module, 23 | format: "esm", 24 | sourcemap: true, 25 | }, 26 | ], 27 | plugins: [ 28 | peerDepsExternal(), 29 | image(), 30 | resolve(), 31 | commonjs(), 32 | typescript({ tsconfig: "./tsconfig.json" }), 33 | postcss(), 34 | terser(), 35 | ], 36 | }, 37 | { 38 | input: "dist/esm/types/index.d.ts", 39 | output: [{ file: "dist/index.d.ts", format: "esm" }], 40 | plugins: [dts()], 41 | external: [/\.css$/], 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pageblox-org/pageblox-react/62500f62e1d8039f0c1f50d11af750a7b8cc25ff/src/.DS_Store -------------------------------------------------------------------------------- /src/components/CommentBlocks.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | import { PublishedComment } from "./PublishedComment"; 4 | 5 | export interface CommentBlocksProps { 6 | blocks: Comment[]; 7 | handleShowComment: (id: string) => void; 8 | pageRef: RefObject; 9 | selectedComment: Comment | null; 10 | setSelectedComment: (comment: Comment | null) => void; 11 | setShowCommentView: (showCommentView: boolean) => void; 12 | } 13 | 14 | const CommentBlocks = ({ 15 | blocks, 16 | handleShowComment, 17 | pageRef, 18 | selectedComment, 19 | setSelectedComment, 20 | setShowCommentView, 21 | }: CommentBlocksProps): JSX.Element => { 22 | const selectedCommentExists = selectedComment !== null; 23 | 24 | return ( 25 |
26 | {blocks.map((block: Comment) => { 27 | if (block.resolved === false) { 28 | if (selectedCommentExists) { 29 | return ( 30 | 40 | ); 41 | } else { 42 | return ( 43 | 51 | ); 52 | } 53 | } 54 | })} 55 |
56 | ); 57 | }; 58 | 59 | export default CommentBlocks; 60 | -------------------------------------------------------------------------------- /src/components/CommentDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject, useEffect, useRef, useState } from "react"; 2 | import { format } from "date-fns"; 3 | import { useOutsideClick } from "../utils/hooks"; 4 | import { Comment, Reply } from "../pageblox"; 5 | import { storage } from "../utils/firebase-config"; 6 | import { getDownloadURL, ref } from "firebase/storage"; 7 | 8 | interface CommentDisplayProps { 9 | pageRef: RefObject; 10 | selectedComment: Comment | null; 11 | setSelectedComment: (comment: Comment | null) => void; 12 | deleteComment: (id: string) => void; 13 | fetchReplies: (id: string) => Reply[]; 14 | saveReply: (commentId: string, message: string) => void; 15 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 16 | setShowCommentView: (showCommentView: boolean) => void; 17 | } 18 | 19 | interface CommentSectionProps { 20 | selectedComment: Comment; 21 | screenshotUrl?: string; 22 | } 23 | 24 | interface ReplySectionProps { 25 | replies: Reply[]; 26 | inputRef: RefObject; 27 | newReply: string; 28 | setNewReply: (newReply: string) => void; 29 | handleReply: (event: React.KeyboardEvent) => void; 30 | } 31 | 32 | interface ButtonsProps { 33 | selectedComment: Comment; 34 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 35 | deleteComment: (id: string) => void; 36 | } 37 | 38 | const CommentSection = ({ 39 | selectedComment, 40 | screenshotUrl, 41 | }: CommentSectionProps) => ( 42 |
43 |
44 | {selectedComment.timestamp !== undefined && ( 45 | 46 | {format(selectedComment.timestamp.toDate(), "MMM dd hh:mm a")} 47 | 48 | )} 49 |
50 | 57 | {`${selectedComment.profileName}`} 58 |
59 |
60 |

{selectedComment.comment}

61 | {screenshotUrl && ( 62 |
63 | Comment Attachment 69 |
70 | )} 71 |
72 |
73 | ); 74 | 75 | const ReplySection = ({ 76 | replies, 77 | inputRef, 78 | newReply, 79 | setNewReply, 80 | handleReply, 81 | }: ReplySectionProps) => { 82 | const sortedReplies = replies.sort((reply1, reply2) => { 83 | if (reply1.timestamp > reply2.timestamp) { 84 | return 1; 85 | } else { 86 | return -1; 87 | } 88 | }); 89 | 90 | return ( 91 |
92 |
93 | {sortedReplies.length > 0 && 94 | sortedReplies.map((reply) => { 95 | return ( 96 |
100 |
101 |
102 | 109 | {`${reply.author}`} 110 |
111 | {reply.timestamp !== undefined && ( 112 | 113 | {format(reply.timestamp.toDate(), "MMM dd hh:mm a")} 114 | 115 | )} 116 |
117 |

{reply.message}

118 |
119 | ); 120 | })} 121 |
122 | 132 |
133 | ); 134 | }; 135 | 136 | const Buttons = ({ 137 | selectedComment, 138 | onResolveChange, 139 | deleteComment, 140 | }: ButtonsProps) => { 141 | const onDeleteClick = (event: any) => { 142 | deleteComment(selectedComment.id); 143 | }; 144 | 145 | return ( 146 |
147 | {selectedComment?.resolved ? ( 148 | 156 | ) : ( 157 | 165 | )} 166 | 172 |
173 | ); 174 | }; 175 | 176 | const CommentDisplay = ({ 177 | pageRef, 178 | selectedComment, 179 | setSelectedComment, 180 | deleteComment, 181 | fetchReplies, 182 | saveReply, 183 | onResolveChange, 184 | setShowCommentView, 185 | }: CommentDisplayProps) => { 186 | const [newReply, setNewReply] = useState(""); 187 | const [screenshotUrl, setScreenshotUrl] = useState(""); 188 | const inputRef = useRef( 189 | null 190 | ) as React.MutableRefObject; 191 | 192 | const replies: Reply[] = selectedComment 193 | ? fetchReplies(selectedComment?.id) 194 | : []; 195 | 196 | const handleReply = (event: React.KeyboardEvent) => { 197 | if (selectedComment && newReply.length > 0 && event.key === "Enter") { 198 | saveReply(selectedComment.id, newReply); 199 | setNewReply(""); 200 | inputRef.current?.blur(); 201 | } 202 | }; 203 | 204 | const modalRef = useOutsideClick(() => { 205 | setSelectedComment(null); 206 | setShowCommentView(false); 207 | }, pageRef); 208 | 209 | const fetchScreenshotIfExists = async () => { 210 | if (selectedComment?.uploadedFilePath) { 211 | const filePathRef = ref(storage, selectedComment.uploadedFilePath); 212 | 213 | try { 214 | const url = await getDownloadURL(filePathRef); 215 | setScreenshotUrl(url); 216 | } catch (error) { 217 | console.log("An error occurred fetching screenshots", error); 218 | } 219 | } 220 | }; 221 | 222 | useEffect(() => { 223 | fetchScreenshotIfExists(); 224 | }, [selectedComment?.uploadedFilePath]); 225 | 226 | return ( 227 | <> 228 |
235 | {selectedComment && ( 236 | 241 | )} 242 | {selectedComment && ( 243 | 247 | )} 248 | 255 |
256 | 257 | ); 258 | }; 259 | 260 | export default CommentDisplay; 261 | -------------------------------------------------------------------------------- /src/components/CommentsList.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | import { format } from "date-fns"; 3 | import { useOutsideClick } from "../utils/hooks"; 4 | import { Comment, Reply } from "../pageblox"; 5 | 6 | interface CommentsListProps { 7 | pageRef: RefObject; 8 | showComments: boolean; 9 | setShowComments: (showComments: boolean) => void; 10 | comments: Comment[]; 11 | replies: Reply[]; 12 | navigateToComment: (commentId: string | null) => void; 13 | } 14 | 15 | const CommentsList = ({ 16 | pageRef, 17 | showComments, 18 | setShowComments, 19 | comments, 20 | replies, 21 | navigateToComment, 22 | }: CommentsListProps) => { 23 | const ref = useOutsideClick(() => { 24 | setShowComments(false); 25 | }, pageRef); 26 | 27 | const sortedComments = comments.sort((comment1, comment2) => { 28 | if (comment1.timestamp > comment2.timestamp) { 29 | return 1; 30 | } else { 31 | return -1; 32 | } 33 | }); 34 | 35 | const handleItemClick = (commentID: string) => { 36 | navigateToComment(commentID); 37 | setShowComments(false); 38 | }; 39 | 40 | return ( 41 |
48 | {sortedComments.length > 0 && ( 49 |
    50 | {sortedComments.map((comment) => { 51 | const commentReplies = replies.filter( 52 | (reply) => reply.parent_comment_id === comment.id 53 | ); 54 | return ( 55 |
  • handleItemClick(comment.id)} 59 | > 60 |
    61 | {comment.timestamp.nanoseconds !== undefined && ( 62 | 63 | {format(comment.timestamp.toDate(), "MMM dd hh:mm a")} 64 | 65 | )} 66 |
    67 | 74 | 75 | {comment.profileName} 76 | 77 | {comment.resolved && ( 78 | 79 | 91 | 92 | 93 | 94 | )} 95 |
    96 |
    97 |

    {comment.comment}

    98 |
    99 | {commentReplies.length > 0 && ( 100 |

    {`${ 101 | commentReplies.length 102 | } ${commentReplies.length === 1 ? "reply" : "replies"}`}

    103 | )} 104 | {comment.uploadedFilePath && ( 105 | 106 | 118 | 126 | 127 | 128 | 129 | 130 | )} 131 |
    132 |
  • 133 | ); 134 | })} 135 |
136 | )} 137 |
138 | ); 139 | }; 140 | 141 | export default CommentsList; 142 | -------------------------------------------------------------------------------- /src/components/CreateCommentModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | 3 | interface CreateCommentModalInterface { 4 | showCreateView: boolean; 5 | toggleCreateModal: (shouldShow: boolean) => void; 6 | authorName: string; 7 | authorColor: string; 8 | onSaveComment: (comment: string, file?: File | null) => void; 9 | } 10 | 11 | const CreateCommentModal = ({ 12 | showCreateView, 13 | toggleCreateModal, 14 | authorName, 15 | authorColor, 16 | onSaveComment, 17 | }: CreateCommentModalInterface) => { 18 | const inputRef = useRef(null); 19 | const attachmentInputRef = useRef(null); 20 | const [commentInput, setCommentInput] = useState(""); 21 | const [isHoveringImage, setIsHoveringImage] = useState(false); 22 | const [file, setFile] = useState(null); 23 | const [fileDataUrl, setFileDataUrl] = useState( 24 | null 25 | ); 26 | 27 | const onCreate = () => { 28 | onSaveComment(commentInput, file); 29 | }; 30 | 31 | const onClose = () => { 32 | toggleCreateModal(false); 33 | }; 34 | 35 | const onFocus = () => { 36 | inputRef.current?.focus(); 37 | }; 38 | 39 | const onTextAreaKeyDown = (event: React.KeyboardEvent) => { 40 | if (event.key === "Enter" && !event.shiftKey && commentInput.length > 0) { 41 | onSaveComment(commentInput); 42 | } 43 | }; 44 | 45 | const handlePaste = (event: React.ClipboardEvent) => { 46 | const clipboardItem = event.clipboardData.items[0]; 47 | 48 | if (clipboardItem.type.indexOf("image") === 0) { 49 | const file: File | null = clipboardItem.getAsFile(); 50 | 51 | if (file) { 52 | let fileReader = new FileReader(); 53 | fileReader.onload = (event) => { 54 | const result = event.target?.result; 55 | if (result) { 56 | setFileDataUrl(result); 57 | } 58 | }; 59 | 60 | setFile(file); 61 | fileReader.readAsDataURL(file); 62 | } 63 | } 64 | }; 65 | 66 | const handleFileChange = (event: React.ChangeEvent) => { 67 | if (!event.target.files) { 68 | return; 69 | } 70 | 71 | setFile(event.target.files[0]); 72 | }; 73 | 74 | const handleMouseOverImage = () => { 75 | setIsHoveringImage(true); 76 | }; 77 | 78 | const handleMouseOutImage = () => { 79 | setIsHoveringImage(false); 80 | }; 81 | 82 | const handleDeleteAttachment = () => { 83 | setFile(null); 84 | setFileDataUrl(null); 85 | }; 86 | 87 | useEffect(() => { 88 | if (inputRef.current !== null) { 89 | onFocus(); 90 | } 91 | }, [inputRef]); 92 | 93 | useEffect(() => { 94 | let fileReader: FileReader; 95 | let isCancel = false; 96 | 97 | if (file) { 98 | fileReader = new FileReader(); 99 | fileReader.onload = (event) => { 100 | const result = event.target?.result; 101 | if (result && !isCancel) { 102 | setFileDataUrl(result); 103 | } 104 | }; 105 | 106 | fileReader.readAsDataURL(file); 107 | } 108 | 109 | return () => { 110 | isCancel = true; 111 | 112 | if (fileReader && fileReader.readyState === 1) { 113 | fileReader.abort(); 114 | } 115 | }; 116 | }, [file]); 117 | 118 | return ( 119 |
126 |
127 |
{ 130 | event.stopPropagation(); 131 | }} 132 | > 133 |
134 | 138 |

139 | {authorName} 140 |

141 |
142 |
143 | 152 | {fileDataUrl && ( 153 |
159 | 181 | Comment Attachment 186 |
187 | )} 188 |
189 |
190 |
194 | 201 | 220 |
221 |
222 | 229 | 235 |
236 |
237 |
238 |
239 |
240 | ); 241 | }; 242 | 243 | export default CreateCommentModal; 244 | -------------------------------------------------------------------------------- /src/components/EnabledPagebloxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "../images/pageblox-logo.svg"; 3 | 4 | interface EnabledPagebloxButtonInterface { 5 | shouldDisplay: boolean; 6 | reviewMode: boolean; 7 | onWidgetClick: () => void; 8 | } 9 | 10 | const EnabledPagebloxButton = ({ 11 | shouldDisplay, 12 | reviewMode, 13 | onWidgetClick, 14 | }: EnabledPagebloxButtonInterface) => { 15 | return ( 16 |
21 | 29 |
30 | ); 31 | }; 32 | 33 | export default EnabledPagebloxButton; 34 | -------------------------------------------------------------------------------- /src/components/InstructionsPopup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface InstructionsPopupProps { 4 | showInstructionsPopup: boolean; 5 | setShowInstructionsPopup: (show: boolean) => void; 6 | } 7 | 8 | const InstructionsPopup = ({ 9 | showInstructionsPopup, 10 | setShowInstructionsPopup, 11 | }: InstructionsPopupProps) => ( 12 | <> 13 | {showInstructionsPopup && ( 14 | 57 | )} 58 | 59 | ); 60 | 61 | export default InstructionsPopup; 62 | -------------------------------------------------------------------------------- /src/components/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createUserWithEmailAndPassword, 3 | signInWithEmailAndPassword, 4 | } from "firebase/auth"; 5 | import React, { FormEvent, useState } from "react"; 6 | import { auth, database } from "../utils/firebase-config"; 7 | import { doc, getDoc, setDoc } from "firebase/firestore"; 8 | 9 | interface LoginModalProps { 10 | showModal: boolean; 11 | setShowModal: (showModal: boolean) => void; 12 | setReviewMode: (reviewMode: boolean) => void; 13 | setDisplayName: (name: string | null) => void; 14 | } 15 | 16 | interface LoginFormProps { 17 | onSignupClick: () => void; 18 | onClose: () => void; 19 | onAuthenticate: (userId: string, displayName: string) => void; 20 | } 21 | 22 | interface SignupFormProps { 23 | onLoginClick: () => void; 24 | onClose: () => void; 25 | onAuthenticate: (userId: string, displayName: string) => void; 26 | } 27 | 28 | export default function LoginModal({ 29 | showModal, 30 | setShowModal, 31 | setReviewMode, 32 | setDisplayName, 33 | }: LoginModalProps) { 34 | const [showLoginForm, setShowLoginForm] = useState(true); 35 | 36 | const onSignupClick = () => { 37 | setShowLoginForm(false); 38 | }; 39 | 40 | const onLoginClick = () => { 41 | setShowLoginForm(true); 42 | }; 43 | 44 | const onAuthenticate = (userId: string, displayName: string) => { 45 | localStorage.setItem( 46 | "pagebloxUserInfo", 47 | JSON.stringify({ 48 | userCredential: userId, 49 | displayName: displayName, 50 | }) 51 | ); 52 | 53 | onClose(); 54 | setDisplayName(displayName); 55 | setReviewMode(true); 56 | }; 57 | 58 | const onClose = () => { 59 | setShowModal(false); 60 | }; 61 | 62 | if (!showModal) { 63 | return null; 64 | } else { 65 | if (showLoginForm) { 66 | return ( 67 | 72 | ); 73 | } else { 74 | return ( 75 | 80 | ); 81 | } 82 | } 83 | } 84 | 85 | const SignupForm = ({ 86 | onLoginClick, 87 | onClose, 88 | onAuthenticate, 89 | }: SignupFormProps) => { 90 | const [displayName, setDisplayName] = useState(""); 91 | const [email, setEmail] = useState(""); 92 | const [password, setPassword] = useState(""); 93 | const [confirmPassword, setConfirmPassword] = useState(""); 94 | const [loading, setLoading] = useState(false); 95 | 96 | const onSignup = async (event: FormEvent) => { 97 | event.preventDefault(); 98 | setLoading(true); 99 | if (password === confirmPassword) { 100 | createUserWithEmailAndPassword(auth, email, password) 101 | .then((userCredential) => { 102 | setLoading(false); 103 | const user = userCredential.user; 104 | if (user) { 105 | const userId = user.uid; 106 | setDoc(doc(database, "users", userId), { 107 | displayName: displayName, 108 | }) 109 | .then(() => { 110 | onAuthenticate(userId, displayName); 111 | }) 112 | .catch((error) => { 113 | alert("Error writing user to database: " + error); 114 | }); 115 | } 116 | }) 117 | .catch((error) => { 118 | setLoading(false); 119 | alert("Signup Error: " + error.code + error.message); 120 | }); 121 | } else { 122 | setLoading(false); 123 | alert("Passwords do not match"); 124 | } 125 | }; 126 | 127 | return ( 128 |
129 |
130 | 136 | 142 |
148 |
149 |
150 |
151 |
152 | 158 |
159 | setDisplayName(e.target.value)} 166 | /> 167 | setEmail(e.target.value)} 174 | /> 175 | setPassword(e.target.value)} 182 | /> 183 | setConfirmPassword(e.target.value)} 190 | /> 191 |
192 |
193 |
194 |
195 |
196 |

197 | Already have an account?{" "} 198 | 203 | Login here 204 | 205 |

206 |
207 | {loading ? ( 208 | 232 | ) : ( 233 | 239 | )} 240 | 247 |
248 |
249 |
250 |
251 |
252 |
253 | ); 254 | }; 255 | 256 | const LoginForm = ({ 257 | onSignupClick, 258 | onClose, 259 | onAuthenticate, 260 | }: LoginFormProps) => { 261 | const [email, setEmail] = useState(""); 262 | const [password, setPassword] = useState(""); 263 | const [loading, setLoading] = useState(false); 264 | 265 | const onLogin = async (event: FormEvent) => { 266 | event.preventDefault(); 267 | setLoading(true); 268 | 269 | signInWithEmailAndPassword(auth, email, password) 270 | .then((userCredential) => { 271 | getDoc(doc(database, "users", userCredential.user.uid)) 272 | .then((doc) => { 273 | const userData = doc.data(); 274 | if (userData) { 275 | setLoading(false); 276 | onAuthenticate(userCredential.user.uid, userData.displayName); 277 | } else { 278 | throw new Error("User data not found"); 279 | } 280 | }) 281 | .catch((error) => { 282 | setLoading(false); 283 | alert("Signup Error: " + error.code + error.message); 284 | }); 285 | }) 286 | .catch((error) => { 287 | setLoading(false); 288 | alert("Login Error: " + error.code + error.message); 289 | }); 290 | }; 291 | 292 | return ( 293 |
294 |
295 | 301 | 307 |
313 |
314 |
315 |
316 |
317 | 323 |
324 | 325 | setEmail(e.target.value)} 332 | /> 333 | setPassword(e.target.value)} 340 | /> 341 | 342 |
343 |
344 |
345 |
346 |
347 |

348 | Don’t have an account yet?{" "} 349 | 354 | Sign up 355 | 356 |

357 |
358 | {loading ? ( 359 | 383 | ) : ( 384 | 391 | )} 392 | 399 |
400 |
401 | 402 |
403 |
404 |
405 | ); 406 | }; 407 | -------------------------------------------------------------------------------- /src/components/PublishedComment.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject, useEffect } from "react"; 2 | import { useState } from "react"; 3 | import { useDrag } from "react-dnd"; 4 | import { ItemTypes } from "../utils/ItemTypes"; 5 | import { Comment } from "../pageblox"; 6 | import { useOutsideClick } from "../utils/hooks"; 7 | import { createPortal } from "react-dom"; 8 | 9 | interface PublishedCommentProps { 10 | block: Comment; 11 | pageRef: RefObject; 12 | setSelectedComment: (comment: Comment | null) => void; 13 | showReplies: (id: string) => void; 14 | shouldHideLabel?: boolean; 15 | isSelectedComment?: boolean; 16 | setShowCommentView: (showCommentView: boolean) => void; 17 | } 18 | 19 | export const PublishedComment = ({ 20 | block, 21 | pageRef, 22 | setSelectedComment, 23 | showReplies, 24 | shouldHideLabel = false, 25 | isSelectedComment = false, 26 | setShowCommentView, 27 | }: PublishedCommentProps): JSX.Element => { 28 | const { id, profileColor, profileName, dom, x, y } = block || {}; 29 | const [domElement, setDomElement] = useState(null); 30 | const ref = useOutsideClick(() => { 31 | setSelectedComment(null); 32 | setShowCommentView(false); 33 | }, pageRef); 34 | 35 | const [{ isDragging }, drag] = useDrag( 36 | () => ({ 37 | type: ItemTypes.COMMENT, 38 | item: { id, domElement, x, y }, 39 | collect: (monitor) => ({ 40 | isDragging: monitor.isDragging(), 41 | }), 42 | }), 43 | [id, domElement, x, y] 44 | ); 45 | 46 | const handleRepliesClick = (event: React.MouseEvent) => { 47 | event.preventDefault(); 48 | event.nativeEvent.stopImmediatePropagation(); 49 | 50 | showReplies(id); 51 | }; 52 | 53 | useEffect(() => { 54 | const targetElement = document.evaluate( 55 | dom, 56 | document, 57 | null, 58 | XPathResult.FIRST_ORDERED_NODE_TYPE, 59 | null 60 | ).singleNodeValue as HTMLElement; 61 | 62 | if (targetElement) { 63 | targetElement.classList.add("tw-relative"); 64 | setDomElement(targetElement); 65 | 66 | return () => { 67 | targetElement.classList.remove("tw-static"); 68 | }; 69 | } 70 | }, [block]); 71 | 72 | if (isDragging) { 73 | return
; 74 | } 75 | 76 | return domElement ? ( 77 | createPortal( 78 |
90 |
91 | 99 |

104 | {profileName.charAt(0)} 105 |

106 |
107 |
108 |
, 109 | domElement 110 | ) 111 | ) : ( 112 | <> 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { RefObject, useState } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | import CommentsList from "./CommentsList"; 4 | import CommentDisplay from "./CommentDisplay"; 5 | import { signOut } from "firebase/auth"; 6 | import { auth } from "../utils/firebase-config"; 7 | 8 | interface SidebarButtonsProps { 9 | setShowComments: (show: boolean) => void; 10 | comments: Comment[]; 11 | setDisplayName: (name: string | null) => void; 12 | setReviewMode: (reviewMode: boolean) => void; 13 | } 14 | 15 | interface ToolbarProps { 16 | showCommentView: boolean; 17 | setShowCommentView: (showCommentView: boolean) => void; 18 | pageRef: RefObject; 19 | comments: Comment[]; 20 | replies: Reply[]; 21 | deleteComment: (id: string) => void; 22 | fetchReplies: (id: string) => Reply[]; 23 | saveReply: (commentId: string, message: string) => void; 24 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 25 | selectedComment: Comment | null; 26 | setSelectedComment: (comment: Comment | null) => void; 27 | setDisplayName: (name: string | null) => void; 28 | setReviewMode: (reviewMode: boolean) => void; 29 | } 30 | 31 | const SidebarButtons = ({ 32 | setShowComments, 33 | comments, 34 | setDisplayName, 35 | setReviewMode, 36 | }: SidebarButtonsProps) => { 37 | let unresolvedComments = comments.filter( 38 | (comment) => comment.resolved == false 39 | ); 40 | 41 | const onSignout = async () => { 42 | try { 43 | await signOut(auth); 44 | localStorage.removeItem("pagebloxUserInfo"); 45 | 46 | setDisplayName(null); 47 | setReviewMode(false); 48 | } catch (error) { 49 | alert(`Error signing out: ${error}`); 50 | } 51 | }; 52 | 53 | return ( 54 |
55 | 61 | 86 |
87 | ); 88 | }; 89 | 90 | const Toolbar = ({ 91 | showCommentView, 92 | setShowCommentView, 93 | pageRef, 94 | comments, 95 | replies, 96 | deleteComment, 97 | fetchReplies, 98 | saveReply, 99 | onResolveChange, 100 | selectedComment, 101 | setSelectedComment, 102 | setDisplayName, 103 | setReviewMode, 104 | }: ToolbarProps): JSX.Element => { 105 | const [showComments, setShowComments] = useState(false); 106 | 107 | const onClose = () => { 108 | setShowComments(false); 109 | setShowCommentView(false); 110 | setSelectedComment(null); 111 | }; 112 | 113 | const navigateToComment = (commentId: string | null) => { 114 | if (commentId) { 115 | const selectedComment = comments.filter( 116 | (comment) => comment.id === commentId 117 | )[0]; 118 | setSelectedComment(selectedComment); 119 | 120 | if (!selectedComment.resolved) { 121 | const element = document.getElementById(commentId); 122 | 123 | if (element) { 124 | const y = element?.getBoundingClientRect().top + window.scrollY - 100; 125 | window.scrollTo({ top: y, behavior: "smooth" }); 126 | } 127 | } 128 | 129 | setShowCommentView(true); 130 | } 131 | }; 132 | 133 | return ( 134 |
138 | {showComments || showCommentView ? ( 139 | 147 | ) : ( 148 |
149 | 155 |
156 | )} 157 | 165 | {showCommentView && ( 166 | 176 | )} 177 |
178 | ); 179 | }; 180 | 181 | export default Toolbar; 182 | -------------------------------------------------------------------------------- /src/images/pageblox-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | .tw-pageblox *, 11 | .tw-pageblox ::before, 12 | .tw-pageblox ::after { 13 | box-sizing: border-box; /* 1 */ 14 | border-width: 0; /* 2 */ 15 | border-style: solid; /* 2 */ 16 | border-color: theme("borderColor.DEFAULT", currentColor); /* 2 */ 17 | } 18 | 19 | .tw-pageblox ::before, 20 | .tw-pageblox ::after { 21 | --tw-content: ""; 22 | } 23 | 24 | /* 25 | 1. Use a consistent sensible line-height in all browsers. 26 | 2. Prevent adjustments of font size after orientation changes in iOS. 27 | 3. Use a more readable tab size. 28 | 4. Use the user's configured `sans` font-family by default. 29 | 5. Use the user's configured `sans` font-feature-settings by default. 30 | */ 31 | 32 | .tw-pageblox html { 33 | line-height: 1.5; /* 1 */ 34 | -webkit-text-size-adjust: 100%; /* 2 */ 35 | -moz-tab-size: 4; /* 3 */ 36 | tab-size: 4; /* 3 */ 37 | font-family: theme( 38 | "fontFamily.sans", 39 | ui-sans-serif, 40 | system-ui, 41 | -apple-system, 42 | BlinkMacSystemFont, 43 | "Segoe UI", 44 | Roboto, 45 | "Helvetica Neue", 46 | Arial, 47 | "Noto Sans", 48 | sans-serif, 49 | "Apple Color Emoji", 50 | "Segoe UI Emoji", 51 | "Segoe UI Symbol", 52 | "Noto Color Emoji" 53 | ); /* 4 */ 54 | font-feature-settings: normal; /* 5 */ 55 | } 56 | 57 | /* 58 | 1. Remove the margin in all browsers. 59 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 60 | */ 61 | 62 | .tw-pageblox body { 63 | margin: 0; /* 1 */ 64 | line-height: inherit; /* 2 */ 65 | } 66 | 67 | /* 68 | 1. Add the correct height in Firefox. 69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 70 | 3. Ensure horizontal rules are visible by default. 71 | */ 72 | 73 | .tw-pageblox hr { 74 | height: 0; /* 1 */ 75 | color: inherit; /* 2 */ 76 | border-top-width: 1px; /* 3 */ 77 | } 78 | 79 | /* 80 | Add the correct text decoration in Chrome, Edge, and Safari. 81 | */ 82 | 83 | .tw-pageblox abbr:where([title]) { 84 | text-decoration: underline dotted; 85 | } 86 | 87 | /* 88 | Remove the default font size and weight for headings. 89 | */ 90 | 91 | .tw-pageblox h1, 92 | .tw-pageblox h2, 93 | .tw-pageblox h3, 94 | .tw-pageblox h4, 95 | .tw-pageblox h5, 96 | .tw-pageblox h6 { 97 | font-size: inherit; 98 | font-weight: inherit; 99 | } 100 | 101 | /* 102 | Reset links to optimize for opt-in styling instead of opt-out. 103 | */ 104 | 105 | .tw-pageblox a { 106 | color: inherit; 107 | text-decoration: inherit; 108 | } 109 | 110 | /* 111 | Add the correct font weight in Edge and Safari. 112 | */ 113 | 114 | .tw-pageblox b, 115 | .tw-pageblox strong { 116 | font-weight: bolder; 117 | } 118 | 119 | /* 120 | 1. Use the user's configured `mono` font family by default. 121 | 2. Correct the odd `em` font sizing in all browsers. 122 | */ 123 | 124 | .tw-pageblox code, 125 | .tw-pageblox kbd, 126 | .tw-pageblox samp, 127 | .tw-pageblox pre { 128 | font-family: theme( 129 | "fontFamily.mono", 130 | ui-monospace, 131 | SFMono-Regular, 132 | Menlo, 133 | Monaco, 134 | Consolas, 135 | "Liberation Mono", 136 | "Courier New", 137 | monospace 138 | ); /* 1 */ 139 | font-size: 1em; /* 2 */ 140 | } 141 | 142 | /* 143 | Add the correct font size in all browsers. 144 | */ 145 | 146 | .tw-pageblox small { 147 | font-size: 80%; 148 | } 149 | 150 | /* 151 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 152 | */ 153 | 154 | .tw-pageblox sub, 155 | .tw-pageblox sup { 156 | font-size: 75%; 157 | line-height: 0; 158 | position: relative; 159 | vertical-align: baseline; 160 | } 161 | 162 | .tw-pageblox sub { 163 | bottom: -0.25em; 164 | } 165 | 166 | .tw-pageblox sup { 167 | top: -0.5em; 168 | } 169 | 170 | /* 171 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 172 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 173 | 3. Remove gaps between table borders by default. 174 | */ 175 | 176 | .tw-pageblox table { 177 | text-indent: 0; /* 1 */ 178 | border-color: inherit; /* 2 */ 179 | border-collapse: collapse; /* 3 */ 180 | } 181 | 182 | /* 183 | 1. Change the font styles in all browsers. 184 | 2. Remove the margin in Firefox and Safari. 185 | 3. Remove default padding in all browsers. 186 | */ 187 | 188 | .tw-pageblox button, 189 | .tw-pageblox input, 190 | .tw-pageblox optgroup, 191 | .tw-pageblox select, 192 | .tw-pageblox textarea { 193 | font-family: inherit; /* 1 */ 194 | font-size: 100%; /* 1 */ 195 | font-weight: inherit; /* 1 */ 196 | line-height: inherit; /* 1 */ 197 | color: inherit; /* 1 */ 198 | margin: 0; /* 2 */ 199 | padding: 0; /* 3 */ 200 | } 201 | 202 | /* 203 | Remove the inheritance of text transform in Edge and Firefox. 204 | */ 205 | 206 | .tw-pageblox button, 207 | .tw-pageblox select { 208 | text-transform: none; 209 | } 210 | 211 | /* 212 | 1. Correct the inability to style clickable types in iOS and Safari. 213 | 2. Remove default button styles. 214 | */ 215 | 216 | .tw-pageblox button, 217 | .tw-pageblox [type="button"], 218 | .tw-pageblox [type="reset"], 219 | .tw-pageblox [type="submit"] { 220 | -webkit-appearance: button; /* 1 */ 221 | background-color: transparent; /* 2 */ 222 | background-image: none; /* 2 */ 223 | } 224 | 225 | /* 226 | Use the modern Firefox focus style for all focusable elements. 227 | */ 228 | 229 | .tw-pageblox :-moz-focusring { 230 | outline: auto; 231 | } 232 | 233 | /* 234 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 235 | */ 236 | 237 | .tw-pageblox :-moz-ui-invalid { 238 | box-shadow: none; 239 | } 240 | 241 | /* 242 | Add the correct vertical alignment in Chrome and Firefox. 243 | */ 244 | 245 | .tw-pageblox progress { 246 | vertical-align: baseline; 247 | } 248 | 249 | /* 250 | Correct the cursor style of increment and decrement buttons in Safari. 251 | */ 252 | 253 | .tw-pageblox ::-webkit-inner-spin-button, 254 | .tw-pageblox ::-webkit-outer-spin-button { 255 | height: auto; 256 | } 257 | 258 | /* 259 | 1. Correct the odd appearance in Chrome and Safari. 260 | 2. Correct the outline style in Safari. 261 | */ 262 | 263 | .tw-pageblox [type="search"] { 264 | -webkit-appearance: textfield; /* 1 */ 265 | outline-offset: -2px; /* 2 */ 266 | } 267 | 268 | /* 269 | Remove the inner padding in Chrome and Safari on macOS. 270 | */ 271 | 272 | .tw-pageblox ::-webkit-search-decoration { 273 | -webkit-appearance: none; 274 | } 275 | 276 | /* 277 | 1. Correct the inability to style clickable types in iOS and Safari. 278 | 2. Change font properties to `inherit` in Safari. 279 | */ 280 | 281 | .tw-pageblox ::-webkit-file-upload-button { 282 | -webkit-appearance: button; /* 1 */ 283 | font: inherit; /* 2 */ 284 | } 285 | 286 | /* 287 | Add the correct display in Chrome and Safari. 288 | */ 289 | 290 | .tw-pageblox summary { 291 | display: list-item; 292 | } 293 | 294 | /* 295 | Removes the default spacing and border for appropriate elements. 296 | */ 297 | 298 | .tw-pageblox blockquote, 299 | .tw-pageblox dl, 300 | .tw-pageblox dd, 301 | .tw-pageblox h1, 302 | .tw-pageblox h2, 303 | .tw-pageblox h3, 304 | .tw-pageblox h4, 305 | .tw-pageblox h5, 306 | .tw-pageblox h6, 307 | .tw-pageblox hr, 308 | .tw-pageblox figure, 309 | .tw-pageblox p, 310 | .tw-pageblox pre { 311 | margin: 0; 312 | } 313 | 314 | .tw-pageblox fieldset { 315 | margin: 0; 316 | padding: 0; 317 | } 318 | 319 | .tw-pageblox legend { 320 | padding: 0; 321 | } 322 | 323 | .tw-pageblox ol, 324 | .tw-pageblox ul, 325 | .tw-pageblox menu { 326 | list-style: none; 327 | margin: 0; 328 | padding: 0; 329 | } 330 | 331 | /* 332 | Prevent resizing textareas horizontally by default. 333 | */ 334 | 335 | .tw-pageblox textarea { 336 | resize: vertical; 337 | } 338 | 339 | /* 340 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 341 | 2. Set the default placeholder color to the user's configured gray 400 color. 342 | */ 343 | 344 | .tw-pageblox input::placeholder, 345 | .tw-pageblox textarea::placeholder { 346 | opacity: 1; /* 1 */ 347 | color: theme("colors.gray.400", #9ca3af); /* 2 */ 348 | } 349 | 350 | /* 351 | Set the default cursor for buttons. 352 | */ 353 | 354 | .tw-pageblox button, 355 | .tw-pageblox [role="button"] { 356 | cursor: pointer; 357 | } 358 | 359 | /* 360 | Make sure disabled buttons don't get the pointer cursor. 361 | */ 362 | .tw-pageblox :disabled { 363 | cursor: default; 364 | } 365 | 366 | /* 367 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 368 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 369 | This can trigger a poorly considered lint error in some tools but is included by design. 370 | */ 371 | 372 | .tw-pageblox img, 373 | .tw-pageblox svg, 374 | .tw-pageblox video, 375 | .tw-pageblox canvas, 376 | .tw-pageblox audio, 377 | .tw-pageblox iframe, 378 | .tw-pageblox embed, 379 | .tw-pageblox object { 380 | display: block; /* 1 */ 381 | vertical-align: middle; /* 2 */ 382 | } 383 | 384 | /* 385 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 386 | */ 387 | 388 | .tw-pageblox img, 389 | .tw-pageblox video { 390 | max-width: 100%; 391 | height: auto; 392 | } 393 | 394 | /* Make elements with the HTML hidden attribute stay hidden by default */ 395 | .tw-pageblox [hidden] { 396 | display: none; 397 | } 398 | 399 | .tw-pageblox * { 400 | /* Foreground, Background */ 401 | scrollbar-color: #999 #333; 402 | } 403 | 404 | .tw-pageblox *::-webkit-scrollbar { 405 | width: 10px; /* Mostly for vertical scrollbars */ 406 | height: 10px; /* Mostly for horizontal scrollbars */ 407 | } 408 | 409 | .tw-pageblox *::-webkit-scrollbar-thumb { 410 | /* Foreground */ 411 | background: #999; 412 | } 413 | 414 | .tw-pageblox *::-webkit-scrollbar-track { 415 | /* Background */ 416 | background: #333; 417 | } 418 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pageblox"; -------------------------------------------------------------------------------- /src/pageblox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from "react"; 2 | import { useDrop, XYCoord } from "react-dnd"; 3 | import { DndProvider } from "react-dnd"; 4 | import { HTML5Backend } from "react-dnd-html5-backend"; 5 | import { 6 | addDoc, 7 | collection, 8 | deleteDoc, 9 | doc, 10 | getDocs, 11 | onSnapshot, 12 | query, 13 | setDoc, 14 | updateDoc, 15 | where, 16 | } from "firebase/firestore"; 17 | import uuid from "react-uuid"; 18 | import { database, storage } from "./utils/firebase-config"; 19 | import { ItemTypes } from "./utils/ItemTypes"; 20 | import Toolbar from "./components/Toolbar"; 21 | import CommentBlocks from "./components/CommentBlocks"; 22 | import EnabledPagebloxButton from "./components/EnabledPagebloxButton"; 23 | import { calculateScroll } from "./utils/calculateScroll"; 24 | import { getPathTo } from "./utils/getPathTo"; 25 | import CreateCommentModal from "./components/CreateCommentModal"; 26 | import { ref, uploadBytes } from "firebase/storage"; 27 | import InstructionsPopup from "./components/InstructionsPopup"; 28 | import LoginModal from "./components/LoginModal"; 29 | 30 | export interface Comment { 31 | id: string; 32 | timestamp: any; 33 | profileColor: string; 34 | profileName: string; 35 | comment: string; 36 | resolved: boolean; 37 | dom: string; 38 | x: number; 39 | y: number; 40 | uploadedFilePath?: string; 41 | } 42 | 43 | export interface Reply { 44 | id: string; 45 | author: string; 46 | profileColor: string; 47 | message: string; 48 | parent_comment_id: string; 49 | timestamp: any; 50 | } 51 | 52 | interface PagebloxProviderInterface { 53 | children: JSX.Element; 54 | projectKey: string; 55 | excludePaths?: string[]; 56 | enabled: boolean; 57 | } 58 | 59 | const COMMENTS_COLLECTION = 60 | process.env.NODE_ENV === "production" ? "comments" : "comments-dev"; 61 | const REPLIES_COLLECTION = 62 | process.env.NODE_ENV === "production" ? "replies" : "replies-dev"; 63 | 64 | const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`; 65 | 66 | const PagebloxDndProvider = (pagebloxProvider: PagebloxProviderInterface) => { 67 | const [blocks, setBlocks] = useState([]); 68 | const [replies, setReplies] = useState([]); 69 | const [reviewMode, setReviewMode] = useState(false); 70 | const [selectedComment, setSelectedComment] = useState(null); 71 | const [showCommentView, setShowCommentView] = useState(false); 72 | const [showCreateView, setShowCreateView] = useState(false); 73 | const [draftedComment, setDraftedComment] = useState(null); 74 | const [showPagebloxButton, setShowPagebloxButton] = useState(true); 75 | const [displayName, setDisplayName] = useState(null); 76 | const [showInstructionsPopup, setShowInstructionsPopup] = 77 | useState(false); 78 | const [showLoginModal, setShowLoginModal] = useState(false); 79 | 80 | const currDom = useRef(null); 81 | const pageRef = useRef(null); 82 | const currentPathName = 83 | typeof window !== "undefined" ? window.location.pathname : ""; 84 | 85 | const checkExcludedPaths = () => { 86 | if (pagebloxProvider.excludePaths) { 87 | const shouldExcludePage = 88 | pagebloxProvider?.excludePaths.includes(currentPathName); 89 | setShowPagebloxButton(!shouldExcludePage); 90 | } 91 | }; 92 | 93 | const fetchDisplayName = () => { 94 | const currUserInfo = localStorage.getItem("pagebloxUserInfo"); 95 | 96 | setDisplayName(currUserInfo ? JSON.parse(currUserInfo).displayName : null); 97 | }; 98 | 99 | const shouldDisplayInstructions = () => { 100 | if ( 101 | reviewMode && 102 | localStorage.getItem("displayedInstructions") !== "true" 103 | ) { 104 | setShowInstructionsPopup(true); 105 | localStorage.setItem("displayedInstructions", "true"); 106 | } 107 | }; 108 | 109 | const moveComment = useCallback( 110 | async (id: string, left: number, top: number, domElement: HTMLElement) => { 111 | await updateDoc(doc(database, COMMENTS_COLLECTION, id), { 112 | dom: getPathTo(domElement), 113 | x: left, 114 | y: top, 115 | }); 116 | }, 117 | [blocks, setBlocks] 118 | ); 119 | 120 | const [, drop] = useDrop( 121 | () => ({ 122 | accept: ItemTypes.COMMENT, 123 | drop(item: any, monitor) { 124 | const delta: XYCoord | null = monitor.getDifferenceFromInitialOffset(); 125 | const windowCoords: XYCoord | null = monitor.getClientOffset(); 126 | 127 | const initialElement: HTMLElement = item.domElement; 128 | 129 | console.log("Initial Element: ", initialElement); 130 | 131 | const finalElement: HTMLElement | null = 132 | windowCoords && 133 | (document.elementFromPoint( 134 | windowCoords?.x, 135 | windowCoords?.y 136 | ) as HTMLElement); 137 | 138 | let finalDomElement: HTMLElement; 139 | 140 | if (finalElement?.tagName === "IMG") { 141 | finalDomElement = finalElement?.parentNode as HTMLElement; 142 | } else { 143 | finalDomElement = finalElement as HTMLElement; 144 | } 145 | 146 | if (finalDomElement) { 147 | if (currDom.current && finalDomElement === currDom.current) { 148 | currDom.current.style.backgroundColor = ""; 149 | currDom.current = null; 150 | } 151 | 152 | const initialWindowCoords = { 153 | x: initialElement.getBoundingClientRect().left + item.x, 154 | y: initialElement.getBoundingClientRect().top + item.y, 155 | }; 156 | 157 | const finalWindowCoords = { 158 | x: Math.round(initialWindowCoords.x + delta?.x), 159 | y: Math.round(initialWindowCoords.y + delta?.y), 160 | }; 161 | 162 | const finalX = 163 | finalWindowCoords.x - finalDomElement.getBoundingClientRect().left; 164 | const finalY = 165 | finalWindowCoords.y - finalDomElement.getBoundingClientRect().top; 166 | 167 | moveComment(item.id, finalX, finalY, finalDomElement); 168 | } 169 | }, 170 | hover(item, monitor) { 171 | const windowCoords: XYCoord | null = monitor.getClientOffset(); 172 | 173 | if (windowCoords) { 174 | const domElement = document.elementFromPoint( 175 | windowCoords?.x, 176 | windowCoords?.y 177 | ) as HTMLElement; 178 | 179 | if (currDom.current !== domElement) { 180 | if (currDom.current) { 181 | currDom.current.style.backgroundColor = ""; 182 | domElement.style.backgroundColor = "rgba(59, 130, 246, 0.5)"; 183 | currDom.current = domElement; 184 | } else { 185 | currDom.current = domElement; 186 | currDom.current.style.backgroundColor = "rgba(59, 130, 246, 0.5)"; 187 | } 188 | } 189 | } 190 | }, 191 | }), 192 | [moveComment] 193 | ); 194 | 195 | const saveReply = async (id: string, message: string) => { 196 | await addDoc(collection(database, REPLIES_COLLECTION), { 197 | id: uuid(), 198 | author: displayName, 199 | profileColor: randomColor, 200 | message: message, 201 | parent_comment_id: id, 202 | timestamp: new Date(), 203 | }); 204 | }; 205 | 206 | const onSaveComment = async (comment: string, file?: File | null) => { 207 | let uploadedFilePath: string = ""; 208 | 209 | if (file) { 210 | const storageRef = ref(storage, `attachments/${file.name}`); 211 | 212 | await uploadBytes(storageRef, file); 213 | uploadedFilePath = `attachments/${file.name}`; 214 | } 215 | 216 | if (draftedComment && typeof window !== undefined) { 217 | setShowCreateView(false); 218 | 219 | let savedComment: Comment = { 220 | ...draftedComment, 221 | ...{ comment: comment }, 222 | ...(uploadedFilePath.length > 0 && { 223 | uploadedFilePath: uploadedFilePath, 224 | }), 225 | }; 226 | 227 | await setDoc( 228 | doc(database, COMMENTS_COLLECTION, savedComment.id), 229 | savedComment 230 | ); 231 | setDraftedComment(null); 232 | } 233 | }; 234 | 235 | const deleteComment = async (id: string) => { 236 | await deleteDoc(doc(database, COMMENTS_COLLECTION, id)); 237 | 238 | const replyQuery = query( 239 | collection(database, REPLIES_COLLECTION), 240 | where("parent_comment_id", "==", id) 241 | ); 242 | const querySnapshot = await getDocs(replyQuery); 243 | 244 | querySnapshot.forEach(async (document) => { 245 | await deleteDoc(doc(database, REPLIES_COLLECTION, document.id)); 246 | }); 247 | 248 | setSelectedComment(null); 249 | }; 250 | 251 | const createComment = (event: any) => { 252 | let domElement; 253 | 254 | if (event.target.tagName === "IMG") { 255 | domElement = event.target.parentElement; 256 | } else { 257 | domElement = event.target; 258 | } 259 | 260 | const boundingClientRect = domElement.getBoundingClientRect(); 261 | 262 | if (boundingClientRect !== null && displayName) { 263 | const [scrollTop, scrollLeft] = calculateScroll(domElement); 264 | const x = event.pageX - (boundingClientRect.left + scrollLeft); 265 | const y = event.pageY - (boundingClientRect.top + scrollTop); 266 | 267 | const newComment = { 268 | id: uuid(), 269 | timestamp: new Date(), 270 | profileColor: randomColor, 271 | profileName: displayName, 272 | comment: "", 273 | resolved: false, 274 | dom: getPathTo(domElement), 275 | projectId: pagebloxProvider.projectKey, 276 | pathname: window.location.pathname, 277 | x: x, 278 | y: y, 279 | }; 280 | 281 | toggleCreateModal(true, newComment); 282 | } 283 | }; 284 | 285 | const onPagebloxClick = (event: any) => { 286 | event.preventDefault(); 287 | event.nativeEvent.stopImmediatePropagation(); 288 | }; 289 | 290 | const fetchRepliesForComment = (commentID: string) => { 291 | return replies.filter((reply) => reply.parent_comment_id === commentID); 292 | }; 293 | 294 | const onResolveChange = async (commentID: string, shouldResolve: boolean) => { 295 | await updateDoc(doc(database, COMMENTS_COLLECTION, commentID), { 296 | resolved: shouldResolve, 297 | }); 298 | 299 | setSelectedComment(null); 300 | setShowCommentView(false); 301 | }; 302 | 303 | const handleShowComment = (id: string) => { 304 | const comment = blocks.filter((block) => block.id === id)[0]; 305 | 306 | setSelectedComment(comment); 307 | setShowCommentView(true); 308 | }; 309 | 310 | const onWidgetClick = () => { 311 | if (displayName) { 312 | setReviewMode(!reviewMode); 313 | } else { 314 | setShowLoginModal(true); 315 | } 316 | }; 317 | 318 | const toggleCreateModal = ( 319 | shouldShowModal: boolean, 320 | newComment?: Comment | null 321 | ) => { 322 | if (shouldShowModal && newComment) { 323 | setShowCreateView(true); 324 | setDraftedComment(newComment); 325 | } else { 326 | setShowCreateView(false); 327 | setDraftedComment(null); 328 | } 329 | }; 330 | 331 | useEffect(() => { 332 | if (typeof window !== "undefined") { 333 | const unsubscribeComments = onSnapshot( 334 | query( 335 | collection(database, COMMENTS_COLLECTION), 336 | where("pathname", "==", window.location.pathname), 337 | where("projectId", "==", pagebloxProvider.projectKey) 338 | ), 339 | (querySnapshot) => { 340 | console.log("Fetching comments..."); 341 | const blocks: Comment[] = []; 342 | 343 | querySnapshot.forEach((doc) => { 344 | blocks.push(Object.assign({ id: doc.id }, doc.data()) as Comment); 345 | }); 346 | 347 | setBlocks(blocks); 348 | } 349 | ); 350 | 351 | return () => { 352 | unsubscribeComments(); 353 | }; 354 | } 355 | }, [window.location.pathname, pagebloxProvider.projectKey]); 356 | 357 | useEffect(() => { 358 | if (typeof window !== "undefined" && blocks.length > 0) { 359 | const unsubscribeReplies = onSnapshot( 360 | query( 361 | collection(database, REPLIES_COLLECTION), 362 | where( 363 | "parent_comment_id", 364 | "in", 365 | blocks.map((block) => block.id.toString()) 366 | ) 367 | ), 368 | (querySnapshot) => { 369 | const replies: Reply[] = []; 370 | 371 | querySnapshot.forEach((doc) => { 372 | replies.push(doc.data() as Reply); 373 | }); 374 | 375 | setReplies(replies); 376 | } 377 | ); 378 | 379 | return () => { 380 | unsubscribeReplies(); 381 | }; 382 | } 383 | }, [blocks]); 384 | 385 | useEffect(() => { 386 | shouldDisplayInstructions(); 387 | }, [reviewMode]); 388 | 389 | useEffect(() => { 390 | checkExcludedPaths(); 391 | fetchDisplayName(); 392 | }, []); 393 | 394 | if (pagebloxProvider.enabled) { 395 | return ( 396 | <> 397 | 403 | {reviewMode ? ( 404 | <> 405 | 409 | 424 |
425 |
430 | 438 |
442 | {pagebloxProvider.children} 443 |
444 | {draftedComment && ( 445 | 452 | )} 453 | 458 |
459 |
460 | 461 | ) : ( 462 | <> 463 | {pagebloxProvider.children} 464 | 469 | 470 | )} 471 | 472 | ); 473 | } else { 474 | return <>{pagebloxProvider.children}; 475 | } 476 | }; 477 | 478 | const PagebloxProvider = (pagebloxProvider: PagebloxProviderInterface) => ( 479 | 480 | 485 | {pagebloxProvider.children} 486 | 487 | 488 | ); 489 | 490 | export { PagebloxProvider }; 491 | -------------------------------------------------------------------------------- /src/utils/ItemTypes.ts: -------------------------------------------------------------------------------- 1 | interface ItemTypes { 2 | COMMENT: string; 3 | } 4 | 5 | export const ItemTypes: ItemTypes = { 6 | COMMENT: 'comment' 7 | } -------------------------------------------------------------------------------- /src/utils/calculateScroll.ts: -------------------------------------------------------------------------------- 1 | 2 | const calculateScroll = (element: HTMLElement | null): number[] => { 3 | if (element) { 4 | const [scrollTop, scrollLeft] = calculateScroll(element.parentElement) 5 | return [(element.scrollTop || 0) + scrollTop, (element.scrollLeft || 0) + scrollLeft] 6 | } else { 7 | return [0, 0] 8 | } 9 | } 10 | 11 | export { calculateScroll } -------------------------------------------------------------------------------- /src/utils/firebase-config.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app" 2 | import { getFirestore } from "firebase/firestore" 3 | import { getStorage } from "firebase/storage" 4 | import { getAuth } from "firebase/auth" 5 | 6 | const firebaseConfig = { 7 | apiKey: "AIzaSyAd_Z7ty-pB7bUezEMtDruF_GYWBCfpm7c", 8 | authDomain: "pageblox-3637a.firebaseapp.com", 9 | projectId: "pageblox-3637a", 10 | storageBucket: "pageblox-3637a.appspot.com", 11 | messagingSenderId: "708437602502", 12 | appId: "1:708437602502:web:8c64a5b759360d267ee7c7", 13 | measurementId: "G-BS8MJMCL1B" 14 | } 15 | 16 | // Initialize Firebase 17 | 18 | const app = initializeApp(firebaseConfig) 19 | 20 | export const database = getFirestore(app) 21 | export const storage = getStorage(app) 22 | export const auth = getAuth(app) -------------------------------------------------------------------------------- /src/utils/getPathTo.ts: -------------------------------------------------------------------------------- 1 | function getPathTo(element: HTMLElement): string { 2 | if (element.id !== '') { 3 | return 'id("'+element.id+'")' 4 | } else if (element === document.body) { 5 | return element.tagName 6 | } else { 7 | var ix = 0; 8 | var siblings = element.parentNode?.childNodes ?? [] 9 | 10 | if (siblings.length > 0) { 11 | for (var i = 0; i < siblings?.length; i++) { 12 | var sibling = siblings[i] as HTMLElement 13 | if (sibling === element && element.parentNode) { 14 | return getPathTo(element.parentNode as HTMLElement) + '/' + element.tagName + '[' + (ix + 1) + ']'; 15 | } else if (sibling.nodeType===1 && sibling.tagName === element.tagName) 16 | ix++; 17 | } 18 | } 19 | } 20 | 21 | return '' 22 | } 23 | 24 | export { getPathTo } -------------------------------------------------------------------------------- /src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import React, { RefObject, useEffect, useRef } from "react" 2 | 3 | export const useOutsideClick = (callback: () => void, boundsRef: RefObject) => { 4 | const ref = useRef() as React.MutableRefObject 5 | 6 | useEffect(() => { 7 | const handleClick = (event: MouseEvent) => { 8 | if (event.target instanceof HTMLElement) { 9 | if (ref.current && !ref.current.contains(event.target) && boundsRef.current?.contains(event.target)) { 10 | callback() 11 | } 12 | } 13 | } 14 | 15 | document.addEventListener('click', handleClick, true) 16 | 17 | return () => { 18 | document.removeEventListener('click', handleClick, true) 19 | } 20 | }, [ref, boundsRef, callback]) 21 | 22 | return ref 23 | } -------------------------------------------------------------------------------- /src/utils/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | interface Size { 4 | width: number | undefined; 5 | height: number | undefined; 6 | } 7 | 8 | export function useWindowSize(): Size { 9 | const [windowSize, setWindowSize] = useState({ 10 | width: undefined, 11 | height: undefined 12 | }) 13 | 14 | useEffect(() => { 15 | function handleResize() { 16 | setWindowSize({ 17 | width: window.innerWidth, 18 | height: window.innerHeight 19 | }) 20 | } 21 | 22 | window.addEventListener("resize", handleResize) 23 | handleResize() 24 | 25 | return () => window.removeEventListener("resize", handleResize) 26 | }, []) 27 | 28 | return windowSize 29 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}", "./src/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | important: true, 8 | corePlugins: { 9 | preflight: false, 10 | }, 11 | plugins: [], 12 | prefix: "tw-", 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ES5", "ES2015", "ES2016", "DOM", "DOM.Iterable", "ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | "jsx": "react", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | "declarationDir": "types", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /types/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pageblox-org/pageblox-react/62500f62e1d8039f0c1f50d11af750a7b8cc25ff/types/.DS_Store -------------------------------------------------------------------------------- /types/components/ClientKeyAlert.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ClientKeyAlertInterface { 3 | showAlert: boolean; 4 | } 5 | declare const ClientKeyAlert: ({ showAlert }: ClientKeyAlertInterface) => JSX.Element; 6 | export default ClientKeyAlert; 7 | -------------------------------------------------------------------------------- /types/components/CommentBlocks.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | export interface CommentBlocksProps { 4 | blocks: Comment[]; 5 | handleShowComment: (id: string) => void; 6 | pageRef: RefObject; 7 | selectedComment: Comment | null; 8 | setSelectedComment: (comment: Comment | null) => void; 9 | setShowCommentView: (showCommentView: boolean) => void; 10 | } 11 | declare const CommentBlocks: ({ blocks, handleShowComment, pageRef, selectedComment, setSelectedComment, setShowCommentView, }: CommentBlocksProps) => JSX.Element; 12 | export default CommentBlocks; 13 | -------------------------------------------------------------------------------- /types/components/CommentDisplay.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface CommentDisplayProps { 4 | pageRef: RefObject; 5 | selectedComment: Comment | null; 6 | setSelectedComment: (comment: Comment | null) => void; 7 | deleteComment: (id: string) => void; 8 | fetchReplies: (id: string) => Reply[]; 9 | saveReply: (commentId: string, message: string) => void; 10 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 11 | setShowCommentView: (showCommentView: boolean) => void; 12 | } 13 | declare const CommentDisplay: ({ pageRef, selectedComment, setSelectedComment, deleteComment, fetchReplies, saveReply, onResolveChange, setShowCommentView, }: CommentDisplayProps) => JSX.Element; 14 | export default CommentDisplay; 15 | -------------------------------------------------------------------------------- /types/components/CommentsList.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface CommentsListProps { 4 | pageRef: RefObject; 5 | showComments: boolean; 6 | setShowComments: (showComments: boolean) => void; 7 | comments: Comment[]; 8 | replies: Reply[]; 9 | navigateToComment: (commentId: string | null) => void; 10 | } 11 | declare const CommentsList: ({ pageRef, showComments, setShowComments, comments, replies, navigateToComment, }: CommentsListProps) => JSX.Element; 12 | export default CommentsList; 13 | -------------------------------------------------------------------------------- /types/components/CreateCommentModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface CreateCommentModalInterface { 3 | showCreateView: boolean; 4 | toggleCreateModal: (shouldShow: boolean) => void; 5 | authorName: string; 6 | authorColor: string; 7 | onSaveComment: (comment: string, file?: File | null) => void; 8 | } 9 | declare const CreateCommentModal: ({ showCreateView, toggleCreateModal, authorName, authorColor, onSaveComment, }: CreateCommentModalInterface) => JSX.Element; 10 | export default CreateCommentModal; 11 | -------------------------------------------------------------------------------- /types/components/DropdownComponent.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface DropdownProps { 3 | setPriority: (priority: number) => {}; 4 | } 5 | declare const DropdownComponent: ({ setPriority }: DropdownProps) => JSX.Element; 6 | export default DropdownComponent; 7 | -------------------------------------------------------------------------------- /types/components/EnabledPagebloxButton.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface EnabledPagebloxButtonInterface { 3 | shouldDisplay: boolean; 4 | reviewMode: boolean; 5 | onWidgetClick: () => void; 6 | } 7 | declare const EnabledPagebloxButton: ({ shouldDisplay, reviewMode, onWidgetClick, }: EnabledPagebloxButtonInterface) => JSX.Element; 8 | export default EnabledPagebloxButton; 9 | -------------------------------------------------------------------------------- /types/components/ImagePreviewModal.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | interface ImagePreviewModalProps { 3 | imageURL: string; 4 | showModal: boolean; 5 | onClose: (event: React.MouseEvent) => void; 6 | } 7 | declare const ImagePreviewModal: ({ imageURL, showModal, onClose }: ImagePreviewModalProps) => JSX.Element; 8 | export default ImagePreviewModal; 9 | -------------------------------------------------------------------------------- /types/components/InstructionsPopup.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface InstructionsPopupProps { 3 | showInstructionsPopup: boolean; 4 | setShowInstructionsPopup: (show: boolean) => void; 5 | } 6 | declare const InstructionsPopup: ({ showInstructionsPopup, setShowInstructionsPopup, }: InstructionsPopupProps) => JSX.Element; 7 | export default InstructionsPopup; 8 | -------------------------------------------------------------------------------- /types/components/LoginModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface LoginModalProps { 3 | showModal: boolean; 4 | setShowModal: (showModal: boolean) => void; 5 | setReviewMode: (reviewMode: boolean) => void; 6 | setDisplayName: (name: string | null) => void; 7 | } 8 | export default function LoginModal({ showModal, setShowModal, setReviewMode, setDisplayName, }: LoginModalProps): JSX.Element | null; 9 | export {}; 10 | -------------------------------------------------------------------------------- /types/components/LoginScreen.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare const LoginScreen: () => JSX.Element; 3 | export default LoginScreen; 4 | -------------------------------------------------------------------------------- /types/components/PublishedComment.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | interface PublishedCommentProps { 4 | block: Comment; 5 | pageRef: RefObject; 6 | setSelectedComment: (comment: Comment | null) => void; 7 | showReplies: (id: string) => void; 8 | shouldHideLabel?: boolean; 9 | isSelectedComment?: boolean; 10 | setShowCommentView: (showCommentView: boolean) => void; 11 | } 12 | export declare const PublishedComment: ({ block, pageRef, setSelectedComment, showReplies, shouldHideLabel, isSelectedComment, setShowCommentView, }: PublishedCommentProps) => JSX.Element; 13 | export {}; 14 | -------------------------------------------------------------------------------- /types/components/RegisterModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface RegisterModalInterface { 3 | showRegisterModal: boolean; 4 | setShowRegisterModal: (shouldShow: boolean) => void; 5 | handleRegisterUser: (name: string, email: string, password: string) => void; 6 | } 7 | declare const RegisterModal: ({ showRegisterModal, setShowRegisterModal, handleRegisterUser, }: RegisterModalInterface) => JSX.Element; 8 | export default RegisterModal; 9 | -------------------------------------------------------------------------------- /types/components/RepliesModal.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Comment, Reply } from "../pageblox"; 3 | interface RepliesModalProps { 4 | selectedComment: Comment; 5 | setSelectedComment: (comment: Comment | null) => void; 6 | fetchReplies: (id: string) => Reply[]; 7 | saveReply: (commentId: string, message: string) => void; 8 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 9 | } 10 | declare const RepliesModal: ({ selectedComment, setSelectedComment, fetchReplies, saveReply, onResolveChange }: RepliesModalProps) => JSX.Element; 11 | export default RepliesModal; 12 | -------------------------------------------------------------------------------- /types/components/Sidebar.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface SidebarProps { 4 | reviewMode: boolean; 5 | pageRef: RefObject; 6 | comments: Comment[]; 7 | replies: Reply[]; 8 | deleteComment: (id: string) => void; 9 | fetchReplies: (id: string) => Reply[]; 10 | saveReply: (commentId: string, message: string) => void; 11 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 12 | selectedComment: Comment | null; 13 | setSelectedComment: (comment: Comment | null) => void; 14 | } 15 | declare const Sidebar: ({ reviewMode, pageRef, comments, replies, deleteComment, fetchReplies, saveReply, onResolveChange, selectedComment, setSelectedComment }: SidebarProps) => JSX.Element; 16 | export default Sidebar; 17 | -------------------------------------------------------------------------------- /types/components/Toolbar.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment, Reply } from "../pageblox"; 3 | interface ToolbarProps { 4 | showCommentView: boolean; 5 | setShowCommentView: (showCommentView: boolean) => void; 6 | pageRef: RefObject; 7 | comments: Comment[]; 8 | replies: Reply[]; 9 | deleteComment: (id: string) => void; 10 | fetchReplies: (id: string) => Reply[]; 11 | saveReply: (commentId: string, message: string) => void; 12 | onResolveChange: (commentID: string, shouldResolve: boolean) => void; 13 | selectedComment: Comment | null; 14 | setSelectedComment: (comment: Comment | null) => void; 15 | setDisplayName: (name: string | null) => void; 16 | setReviewMode: (reviewMode: boolean) => void; 17 | } 18 | declare const Toolbar: ({ showCommentView, setShowCommentView, pageRef, comments, replies, deleteComment, fetchReplies, saveReply, onResolveChange, selectedComment, setSelectedComment, setDisplayName, setReviewMode, }: ToolbarProps) => JSX.Element; 19 | export default Toolbar; 20 | -------------------------------------------------------------------------------- /types/components/UnpublishedComment.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | import { Comment } from "../pageblox"; 3 | export interface SavedCoordinates { 4 | x: number; 5 | y: number; 6 | } 7 | interface UnpublishedCommentProps { 8 | block: Comment; 9 | deleteBlock: (id: string) => {}; 10 | pageRef: RefObject; 11 | } 12 | export interface DomElement { 13 | identifier: string; 14 | className: string; 15 | tagName: string; 16 | } 17 | declare const UnpublishedComment: ({ block, deleteBlock, pageRef, }: UnpublishedCommentProps) => JSX.Element; 18 | export default UnpublishedComment; 19 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./pageblox"; 2 | -------------------------------------------------------------------------------- /types/pageblox.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export interface Comment { 3 | id: string; 4 | timestamp: any; 5 | profileColor: string; 6 | profileName: string; 7 | comment: string; 8 | resolved: boolean; 9 | dom: string; 10 | x: number; 11 | y: number; 12 | uploadedFilePath?: string; 13 | } 14 | export interface Reply { 15 | id: string; 16 | author: string; 17 | profileColor: string; 18 | message: string; 19 | parent_comment_id: string; 20 | timestamp: any; 21 | } 22 | interface PagebloxProviderInterface { 23 | children: JSX.Element; 24 | projectKey: string; 25 | excludePaths?: string[]; 26 | enabled: boolean; 27 | } 28 | declare const PagebloxProvider: (pagebloxProvider: PagebloxProviderInterface) => JSX.Element; 29 | export { PagebloxProvider }; 30 | -------------------------------------------------------------------------------- /types/stories/Button.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import './button.css'; 3 | interface ButtonProps { 4 | /** 5 | * Is this the principal call to action on the page? 6 | */ 7 | primary?: boolean; 8 | /** 9 | * What background color to use 10 | */ 11 | backgroundColor?: string; 12 | /** 13 | * How large should the button be? 14 | */ 15 | size?: 'small' | 'medium' | 'large'; 16 | /** 17 | * Button contents 18 | */ 19 | label: string; 20 | /** 21 | * Optional click handler 22 | */ 23 | onClick?: () => void; 24 | } 25 | /** 26 | * Primary UI component for user interaction 27 | */ 28 | export declare const Button: ({ primary, size, backgroundColor, label, ...props }: ButtonProps) => JSX.Element; 29 | export {}; 30 | -------------------------------------------------------------------------------- /types/stories/Header.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import './header.css'; 3 | type User = { 4 | name: string; 5 | }; 6 | interface HeaderProps { 7 | user?: User; 8 | onLogin: () => void; 9 | onLogout: () => void; 10 | onCreateAccount: () => void; 11 | } 12 | export declare const Header: ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => JSX.Element; 13 | export {}; 14 | -------------------------------------------------------------------------------- /types/stories/Page.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './page.css'; 3 | export declare const Page: React.VFC; 4 | -------------------------------------------------------------------------------- /types/stories/Page.stories.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | declare const _default: ComponentMeta>; 4 | export default _default; 5 | export declare const LoggedOut: ComponentStory>; 6 | export declare const LoggedIn: ComponentStory>; 7 | -------------------------------------------------------------------------------- /types/utils/ItemTypes.d.ts: -------------------------------------------------------------------------------- 1 | interface ItemTypes { 2 | COMMENT: string; 3 | } 4 | export declare const ItemTypes: ItemTypes; 5 | export {}; 6 | -------------------------------------------------------------------------------- /types/utils/calculateScroll.d.ts: -------------------------------------------------------------------------------- 1 | declare const calculateScroll: (element: HTMLElement | null) => number[]; 2 | export { calculateScroll }; 3 | -------------------------------------------------------------------------------- /types/utils/firebase-config.d.ts: -------------------------------------------------------------------------------- 1 | export declare const database: import("@firebase/firestore").Firestore; 2 | export declare const storage: import("@firebase/storage").FirebaseStorage; 3 | export declare const auth: import("@firebase/auth").Auth; 4 | -------------------------------------------------------------------------------- /types/utils/getPathTo.d.ts: -------------------------------------------------------------------------------- 1 | declare function getPathTo(element: HTMLElement): string; 2 | export { getPathTo }; 3 | -------------------------------------------------------------------------------- /types/utils/hooks.d.ts: -------------------------------------------------------------------------------- 1 | import React, { RefObject } from "react"; 2 | export declare const useOutsideClick: (callback: () => void, boundsRef: RefObject) => React.MutableRefObject; 3 | -------------------------------------------------------------------------------- /types/utils/useWindowSize.d.ts: -------------------------------------------------------------------------------- 1 | interface Size { 2 | width: number | undefined; 3 | height: number | undefined; 4 | } 5 | export declare function useWindowSize(): Size; 6 | export {}; 7 | --------------------------------------------------------------------------------