galadrim-feedback 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ .comment-cursor {
6
+ cursor: url("/comment.svg"), auto !important;
7
+ }
@@ -0,0 +1,3 @@
1
+ import "./index.css";
2
+ declare function App(): import("react/jsx-runtime").JSX.Element;
3
+ export default App;
@@ -0,0 +1,6 @@
1
+ export declare const IconButton: ({ text, iconPath, twButtonBackgroundColor, onClick, }: {
2
+ text: string;
3
+ iconPath: string;
4
+ twButtonBackgroundColor: string;
5
+ onClick: () => void;
6
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ export declare const PlusButton: ({ text, twButtonBackgroundColor, onClick, }: {
2
+ text: string;
3
+ twButtonBackgroundColor: string;
4
+ onClick: () => void;
5
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,4 @@
1
+ import { Feedback } from "../../types/types";
2
+ export declare const FeedbackItem: ({ feedback }: {
3
+ feedback: Feedback;
4
+ }) => import("react").ReactPortal | null;
@@ -0,0 +1,3 @@
1
+ export declare const Avatar: ({ avatar }: {
2
+ avatar: string;
3
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ export declare const Modal: ({ isVisible, closeModal, limitWidth, children, }: {
2
+ isVisible: boolean;
3
+ closeModal?: () => void;
4
+ limitWidth?: boolean;
5
+ children: React.ReactNode;
6
+ }) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,7 @@
1
+ import { Dispatch } from "react";
2
+ import { Feedback } from "../types/types";
3
+ export declare const FeedbacksCanvas: ({ isOpen, setIsOpen, feedbacks, }: {
4
+ isOpen: boolean;
5
+ setIsOpen: Dispatch<React.SetStateAction<boolean>>;
6
+ feedbacks: Feedback[];
7
+ }) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,9 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import { Dispatch } from "react";
3
+ import { Feedback } from "../types/types";
4
+ export declare const Overlay: ({ isOpen, setIsOpen, queryClient, feedbacks, }: {
5
+ isOpen: boolean;
6
+ setIsOpen: Dispatch<React.SetStateAction<boolean>>;
7
+ queryClient: QueryClient;
8
+ feedbacks: Feedback[];
9
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ declare function Root(): import("react/jsx-runtime").JSX.Element;
2
+ export default Root;
@@ -0,0 +1,2 @@
1
+ declare const api: import("axios").AxiosInstance;
2
+ export default api;
@@ -0,0 +1,9 @@
1
+ import { Feedback, FeedbackPayload } from "../types/types";
2
+ export declare const fetchFeedbacks: () => Promise<Feedback[]>;
3
+ export declare const createFeedback: (feedback: FeedbackPayload) => Promise<any>;
4
+ export declare const updateFeedback: (feedback: FeedbackPayload) => Promise<any>;
5
+ export declare const deleteFeedback: (feedback: Feedback) => Promise<any>;
6
+ export declare const useFeedbacks: () => import("@tanstack/react-query").UseQueryResult<Feedback[], Error>;
7
+ export declare const useCreateFeedback: () => import("@tanstack/react-query").UseMutationResult<any, Error, FeedbackPayload, unknown>;
8
+ export declare const useUpdateFeedback: () => import("@tanstack/react-query").UseMutationResult<any, Error, FeedbackPayload, unknown>;
9
+ export declare const useDeleteFeedback: () => import("@tanstack/react-query").UseMutationResult<any, Error, Feedback, unknown>;
@@ -0,0 +1,28 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "galadrim-feedback",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.es.js",
8
+ "types": "dist/index.d.ts",
9
+ "scripts": {
10
+ "build": "rollup -c",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "dependencies": {
14
+ "@tanstack/react-query": "^5.59.19",
15
+ "axios": "^1.7.7",
16
+ "classnames": "^2.5.1",
17
+ "moment": "^2.30.1",
18
+ "react": "^18.3.1",
19
+ "react-dom": "^18.3.1",
20
+ "react-router-dom": "^6.26.2"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.11.1",
24
+ "@rollup/plugin-commonjs": "^28.0.2",
25
+ "@rollup/plugin-json": "^6.1.0",
26
+ "@rollup/plugin-node-resolve": "^16.0.0",
27
+ "@rollup/plugin-typescript": "^12.1.2",
28
+ "@types/react": "^18.3.10",
29
+ "@types/react-dom": "^18.3.0",
30
+ "@vitejs/plugin-react": "^4.3.2",
31
+ "autoprefixer": "^10.4.20",
32
+ "babel-plugin-react-generate-property": "^1.1.2",
33
+ "eslint": "^9.11.1",
34
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
35
+ "eslint-plugin-react-refresh": "^0.4.12",
36
+ "globals": "^15.9.0",
37
+ "postcss": "^8.4.49",
38
+ "rollup": "^4.30.1",
39
+ "rollup-plugin-postcss": "^4.0.2",
40
+ "sass": "^1.83.1",
41
+ "tailwindcss": "^3.4.13",
42
+ "tslib": "^2.8.1",
43
+ "typescript": "^5.5.3",
44
+ "typescript-eslint": "^8.7.0",
45
+ "vite": "^5.4.8"
46
+ }
47
+ }
@@ -0,0 +1,39 @@
1
+ // rollup.config.js
2
+ import commonjs from "@rollup/plugin-commonjs";
3
+ import resolve from "@rollup/plugin-node-resolve";
4
+ import typescript from "@rollup/plugin-typescript";
5
+ import postcss from "rollup-plugin-postcss";
6
+ import json from "@rollup/plugin-json";
7
+
8
+ import pkg from "./package.json" assert { type: "json" };
9
+
10
+ export default {
11
+ input: "src/App.tsx", // Your entry point
12
+ output: [
13
+ {
14
+ file: pkg.main, // dist/index.cjs.js
15
+ format: "cjs",
16
+ sourcemap: "inline",
17
+ },
18
+ {
19
+ file: pkg.module, // dist/index.es.js
20
+ format: "es",
21
+ sourcemap: "inline",
22
+ },
23
+ ],
24
+ external: [
25
+ // Peer dependencies (don’t bundle React, ReactDOM, etc.)
26
+ ...Object.keys(pkg.peerDependencies || {}),
27
+ ],
28
+ plugins: [
29
+ resolve(),
30
+ commonjs(),
31
+ postcss({
32
+ extract: "src/index.css",
33
+ }),
34
+ typescript({
35
+ tsconfig: "./tsconfig.json",
36
+ }),
37
+ json(),
38
+ ],
39
+ };
package/src/App.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import Root from "./pages/Root";
3
+ import "./index.css";
4
+ import React from "react";
5
+
6
+ const queryClient = new QueryClient();
7
+
8
+ function App() {
9
+ return (
10
+ <QueryClientProvider client={queryClient}>
11
+ <Root />
12
+ </QueryClientProvider>
13
+ );
14
+ }
15
+
16
+ export default App;
@@ -0,0 +1,27 @@
1
+ import classNames from "classnames";
2
+
3
+ export const IconButton = ({
4
+ text,
5
+ iconPath,
6
+ twButtonBackgroundColor,
7
+ onClick,
8
+ }: {
9
+ text: string;
10
+ iconPath: string;
11
+ twButtonBackgroundColor: string;
12
+ onClick: () => void;
13
+ }) => {
14
+ return (
15
+ <button
16
+ onClick={onClick}
17
+ className={classNames(
18
+ "flex-shrink-0 flex flex-row items-center px-4 py-2 gap-2 rounded-[8px] text-white font-[500] ",
19
+ twButtonBackgroundColor,
20
+ "hover:bg-opacity-80"
21
+ )}
22
+ >
23
+ <img src={iconPath} className="w-6 h-6" />
24
+ {text}
25
+ </button>
26
+ );
27
+ };
@@ -0,0 +1,20 @@
1
+ import { IconButton } from "./IconButton";
2
+
3
+ export const PlusButton = ({
4
+ text,
5
+ twButtonBackgroundColor,
6
+ onClick,
7
+ }: {
8
+ text: string;
9
+ twButtonBackgroundColor: string;
10
+ onClick: () => void;
11
+ }) => {
12
+ return (
13
+ <IconButton
14
+ text={text}
15
+ iconPath="/plus.svg"
16
+ twButtonBackgroundColor={twButtonBackgroundColor}
17
+ onClick={onClick}
18
+ />
19
+ );
20
+ };
@@ -0,0 +1,89 @@
1
+ import { createPortal } from "react-dom";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Feedback } from "../../types/types";
4
+ import { useDeleteFeedback } from "../../services/feedback";
5
+
6
+ export const FeedbackItem = ({ feedback }: { feedback: Feedback }) => {
7
+ const targetElement = useRef<HTMLElement | null>(null);
8
+ const { mutate: deleteFeedback } = useDeleteFeedback();
9
+ const [position, setPosition] = useState({ top: 0, left: 0 });
10
+
11
+ useEffect(() => {
12
+ const handleResize = () => {
13
+ if (!targetElement || !targetElement.current) {
14
+ return;
15
+ }
16
+ const dimensions = targetElement.current.getBoundingClientRect();
17
+ setPosition({
18
+ top: feedback.y + dimensions.top + window.scrollY,
19
+ left: feedback.x + dimensions.left + window.scrollX,
20
+ });
21
+ };
22
+
23
+ const elements = document.querySelectorAll<HTMLElement>(
24
+ `[data-id="${feedback.elementId}"]`
25
+ );
26
+ const element = elements[feedback.elementIndex];
27
+
28
+ if (element) {
29
+ console.log(element);
30
+
31
+ targetElement.current = element;
32
+ handleResize();
33
+ window.addEventListener("resize", handleResize);
34
+ }
35
+
36
+ return () => {
37
+ if (element) {
38
+ window.removeEventListener("resize", handleResize);
39
+ }
40
+ };
41
+ }, [feedback]);
42
+
43
+ const handleDeleteFeedback = () => {
44
+ deleteFeedback(feedback);
45
+ };
46
+
47
+ // If we haven't found a matching DOM node, don't render anything
48
+ if (!targetElement || !targetElement.current) {
49
+ return null;
50
+ }
51
+
52
+ console.log("rendrr", position);
53
+
54
+ // Render this component inside `targetElement` using a Portal
55
+ return createPortal(
56
+ <div
57
+ style={{
58
+ position: "absolute",
59
+ top: position.top,
60
+ left: position.left,
61
+ width: "200px",
62
+ height: "100px",
63
+ border: "1px solid black",
64
+ backgroundColor: "white",
65
+ }}
66
+ >
67
+ <div
68
+ style={{
69
+ display: "flex",
70
+ justifyContent: "space-between",
71
+ alignItems: "center",
72
+ }}
73
+ >
74
+ <div>
75
+ <p style={{ fontSize: "16px", fontWeight: "bold" }}>Feedback</p>
76
+ <p style={{ fontSize: "14px" }}>
77
+ Created at: {feedback.createdAt.toLocaleString()}
78
+ </p>
79
+ <a href={feedback.elementId} target="_blank">
80
+ Go to component
81
+ </a>
82
+ </div>
83
+ <button onClick={handleDeleteFeedback}>Delete</button>
84
+ </div>
85
+ <p style={{ fontSize: "14px" }}>{feedback.comment}</p>
86
+ </div>,
87
+ document.body
88
+ );
89
+ };
@@ -0,0 +1,3 @@
1
+ export const Avatar = ({ avatar }: { avatar: string }) => {
2
+ return <img src={avatar} className="w-10 h-10 rounded-full" />;
3
+ };
@@ -0,0 +1,43 @@
1
+ import { useRef } from "react";
2
+
3
+ export const Modal = ({
4
+ isVisible,
5
+ closeModal,
6
+ limitWidth = true,
7
+ children,
8
+ }: {
9
+ isVisible: boolean;
10
+ closeModal?: () => void;
11
+ limitWidth?: boolean;
12
+ children: React.ReactNode;
13
+ }) => {
14
+ const resizing = useRef(false);
15
+
16
+ if (!isVisible) {
17
+ return null;
18
+ }
19
+ const width = limitWidth ? "w-[400px]" : "";
20
+ return (
21
+ <div
22
+ className="modal"
23
+ onMouseDown={(e) => {
24
+ const target = e.target as HTMLElement;
25
+ if (target.tagName === "TEXTAREA") {
26
+ resizing.current = true;
27
+ }
28
+ }}
29
+ onMouseUp={() => {
30
+ setTimeout(() => {
31
+ resizing.current = false;
32
+ }, 100);
33
+ }}
34
+ onClick={(e) => {
35
+ if (closeModal && e.target === e.currentTarget && !resizing.current) {
36
+ closeModal();
37
+ }
38
+ }}
39
+ >
40
+ <div className={`${width} bg-white rounded-lg`}>{children}</div>
41
+ </div>
42
+ );
43
+ };
package/src/index.css ADDED
@@ -0,0 +1,7 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ .comment-cursor {
6
+ cursor: url("/comment.svg"), auto !important;
7
+ }
@@ -0,0 +1,38 @@
1
+ import { Dispatch, useEffect } from "react";
2
+ import { FeedbackItem } from "../components/feedback/FeedbackItem";
3
+ import { useFeedbacks } from "../services/feedback";
4
+ import { Feedback } from "../types/types";
5
+
6
+ export const FeedbacksCanvas = ({
7
+ isOpen,
8
+ setIsOpen,
9
+ feedbacks,
10
+ }: {
11
+ isOpen: boolean;
12
+ setIsOpen: Dispatch<React.SetStateAction<boolean>>;
13
+ feedbacks: Feedback[];
14
+ }) => {
15
+ if (!isOpen) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <div
21
+ style={{
22
+ zIndex: 2147483647, // Maximum zIndex
23
+ position: "fixed",
24
+ top: 0,
25
+ left: 0,
26
+ width: "100vw",
27
+ height: "100vh",
28
+ }}
29
+ >
30
+ {feedbacks &&
31
+ feedbacks
32
+ .filter((feedback) => feedback.path === window.location.pathname)
33
+ .map((feedback) => (
34
+ <FeedbackItem key={feedback.id} feedback={feedback} />
35
+ ))}
36
+ </div>
37
+ );
38
+ };
@@ -0,0 +1,26 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { Dispatch } from "react";
3
+ import { FeedbacksCanvas } from "./FeedbacksCanvas";
4
+ import { Feedback } from "../types/types";
5
+
6
+ export const Overlay = ({
7
+ isOpen,
8
+ setIsOpen,
9
+ queryClient,
10
+ feedbacks,
11
+ }: {
12
+ isOpen: boolean;
13
+ setIsOpen: Dispatch<React.SetStateAction<boolean>>;
14
+ queryClient: QueryClient;
15
+ feedbacks: Feedback[];
16
+ }) => {
17
+ return (
18
+ <QueryClientProvider client={queryClient}>
19
+ <FeedbacksCanvas
20
+ isOpen={isOpen}
21
+ setIsOpen={setIsOpen}
22
+ feedbacks={feedbacks}
23
+ />
24
+ </QueryClientProvider>
25
+ );
26
+ };
@@ -0,0 +1,128 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import React, { useEffect } from "react";
3
+ import ReactDOM from "react-dom";
4
+ import { Overlay } from "./Overlay";
5
+ import { useCreateFeedback, useFeedbacks } from "../services/feedback";
6
+ import { FeedbackPayload } from "../types/types";
7
+ import { FeedbackItem } from "../components/feedback/FeedbackItem";
8
+
9
+ const queryClient = new QueryClient();
10
+
11
+ function Root() {
12
+ const [isOpen, setIsOpen] = React.useState(false);
13
+ const { data: feedbacks } = useFeedbacks();
14
+
15
+ console.log("feedbacks 1 : ", feedbacks);
16
+
17
+ const [creatingFeedback, setCreatingFeedback] = React.useState(false);
18
+
19
+ const { mutate: createFeedback } = useCreateFeedback();
20
+
21
+ useEffect(() => {
22
+ if (isOpen) {
23
+ console.log("Opening feedbacks canvas");
24
+
25
+ window.addEventListener("keydown", handleKeyPress);
26
+ return () => {
27
+ window.removeEventListener("keypress", handleKeyPress);
28
+ };
29
+ }
30
+ }, [isOpen]);
31
+
32
+ const handleKeyPress = (event: KeyboardEvent) => {
33
+ console.log(event.key);
34
+
35
+ if (event.key === "Escape") {
36
+ setIsOpen(false);
37
+ }
38
+ };
39
+
40
+ const handleClick = (e: any) => {
41
+ console.log("click");
42
+
43
+ e.stopPropagation();
44
+ e.preventDefault();
45
+ const node = e.target as HTMLElement | null;
46
+ const elementId = node?.dataset.id || "";
47
+ const allElements = document.querySelectorAll<HTMLElement>(
48
+ `[data-id="${elementId}"]`
49
+ );
50
+
51
+ const arrAllElements = Array.from(allElements);
52
+
53
+ const elementIndex = arrAllElements.indexOf(node as HTMLElement);
54
+
55
+ const x = e.offsetX;
56
+ const y = e.offsetY;
57
+ console.log(e);
58
+ const newFeedback: FeedbackPayload = {
59
+ x,
60
+ y,
61
+ screenWidth: window.innerWidth,
62
+ screenHeight: window.innerHeight,
63
+ comment: "test",
64
+ path: window.location.pathname,
65
+ elementId,
66
+ elementIndex,
67
+ };
68
+ console.log(newFeedback);
69
+ createFeedback(newFeedback);
70
+ document.body.classList.remove("comment-cursor");
71
+ document.removeEventListener("click", handleClick, true);
72
+ setCreatingFeedback(false);
73
+ setIsOpen(true);
74
+ };
75
+
76
+ const handleCreateFeedback = (e: React.MouseEvent<HTMLButtonElement>) => {
77
+ e.stopPropagation();
78
+ setCreatingFeedback(true);
79
+ document.body.classList.add("comment-cursor");
80
+ document.addEventListener("click", handleClick, true);
81
+ };
82
+
83
+ return (
84
+ <>
85
+ <div
86
+ style={{
87
+ position: "fixed",
88
+ top: "calc(100vh - 62px)",
89
+ left: "calc(100vw - 124px)",
90
+ width: "100px",
91
+ height: "50px",
92
+ zIndex: 1000000,
93
+ backgroundColor: "#0085FF",
94
+ color: "white",
95
+ borderRadius: 4,
96
+ }}
97
+ onClick={() => setIsOpen(!isOpen)}
98
+ >
99
+ Show Feedback
100
+ </div>
101
+ <button
102
+ style={{
103
+ position: "fixed",
104
+ top: "calc(100vh - 122px)",
105
+ left: "calc(100vw - 124px)",
106
+ width: "100px",
107
+ height: "50px",
108
+ zIndex: 1000000,
109
+ backgroundColor: "#0085FF",
110
+ color: "white",
111
+ borderRadius: 4,
112
+ }}
113
+ onClick={handleCreateFeedback}
114
+ >
115
+ New Feedback
116
+ </button>
117
+ {isOpen &&
118
+ feedbacks &&
119
+ feedbacks
120
+ .filter((feedback) => feedback.path === window.location.pathname)
121
+ .map((feedback) => (
122
+ <FeedbackItem key={feedback.id} feedback={feedback} />
123
+ ))}
124
+ </>
125
+ );
126
+ }
127
+
128
+ export default Root;
@@ -0,0 +1,22 @@
1
+ import axios from "axios";
2
+
3
+ const api = axios.create({
4
+ baseURL: "http://localhost:3000",
5
+ });
6
+
7
+ api.interceptors.request.use(
8
+ async (config) => {
9
+ // const token = localStorage.getItem("dashboardGaladrimAuthToken");
10
+ // if (token && config.headers) {
11
+ // config.headers["x-dashboard-token"] = token;
12
+ // } else {
13
+ // delete api.defaults.headers.common.Authorization;
14
+ // }
15
+ return config;
16
+ },
17
+ (error) => {
18
+ return Promise.reject(error);
19
+ }
20
+ );
21
+
22
+ export default api;