galadrim-feedback 0.0.6 → 0.0.8

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.
@@ -3,5 +3,6 @@
3
3
  @tailwind utilities;
4
4
 
5
5
  .comment-cursor {
6
- cursor: url("./assets/comment.svg"), auto !important;
6
+ cursor: url("https://galadrim-dashboard-client.s3.eu-west-3.amazonaws.com/comment.svg"),
7
+ auto !important;
7
8
  }
@@ -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/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ // index.d.ts
2
+
3
+ import { FC } from "react";
4
+
5
+ export interface FeedbackOverlayProps {
6
+ githubUrl?: string;
7
+ }
8
+
9
+ /**
10
+ * FeedbackOverlay Component
11
+ * A component that displays a feedback form overlay.
12
+ */
13
+ declare const FeedbackOverlay: FC<FeedbackOverlayProps>;
14
+
15
+ export default FeedbackOverlay;
package/package.json CHANGED
@@ -1,15 +1,11 @@
1
1
  {
2
2
  "name": "galadrim-feedback",
3
3
  "private": false,
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.es.js",
8
8
  "types": "dist/index.d.ts",
9
- "files": [
10
- "dist/",
11
- "assets/"
12
- ],
13
9
  "scripts": {
14
10
  "build": "rollup -c",
15
11
  "prepublishOnly": "npm run build"
@@ -0,0 +1,40 @@
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
+ import nodePolyfills from "rollup-plugin-node-polyfills";
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
+ nodePolyfills(),
31
+ commonjs(),
32
+ postcss({
33
+ extract: "src/index.css",
34
+ }),
35
+ typescript({
36
+ tsconfig: "./tsconfig.json",
37
+ }),
38
+ json(),
39
+ ],
40
+ };
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,23 @@
1
+ // import { useState } from "react";
2
+ // import { Feedback } from "../../types/types";
3
+
4
+ // export const FeedbackInput = ({
5
+ // feedback,
6
+ // setFeedback,
7
+ // }: {
8
+ // feedback: Feedback;
9
+ // setFeedback: (feedback: Feedback) => void;
10
+ // }) => {
11
+ // const [comment, setComment] = useState(feedback.comment);
12
+
13
+ // const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
14
+ // setComment(event.target.value);
15
+ // };
16
+
17
+ // const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
18
+ // event.preventDefault();
19
+ // setFeedback({ ...feedback, comment });
20
+ // };
21
+
22
+ // return <div></div>;
23
+ // };
@@ -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,8 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ .comment-cursor {
6
+ cursor: url("https://galadrim-dashboard-client.s3.eu-west-3.amazonaws.com/comment.svg"),
7
+ auto !important;
8
+ }
@@ -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,111 @@
1
+ import React, { useEffect } from "react";
2
+ import { FeedbackItem } from "../components/feedback/FeedbackItem";
3
+ import { useCreateFeedback, useFeedbacks } from "../services/feedback";
4
+ import { FeedbackPayload } from "../types/types";
5
+
6
+ function Root() {
7
+ const [isOpen, setIsOpen] = React.useState(false);
8
+ const { data: feedbacks } = useFeedbacks();
9
+
10
+ console.log("feedbacks 1 : ", feedbacks);
11
+
12
+ const { mutate: createFeedback } = useCreateFeedback();
13
+
14
+ useEffect(() => {
15
+ if (isOpen) {
16
+ window.addEventListener("keydown", handleKeyPress);
17
+ return () => {
18
+ window.removeEventListener("keypress", handleKeyPress);
19
+ };
20
+ }
21
+ }, [isOpen]);
22
+
23
+ const handleKeyPress = (event: KeyboardEvent) => {
24
+ if (event.key === "Escape") {
25
+ setIsOpen(false);
26
+ }
27
+ };
28
+
29
+ const handleClick = (e: MouseEvent) => {
30
+ e.stopPropagation();
31
+ e.preventDefault();
32
+ const node = e.target as HTMLElement | null;
33
+ const elementId = node?.dataset.id || "";
34
+ const allElements = document.querySelectorAll<HTMLElement>(
35
+ `[data-id="${elementId}"]`
36
+ );
37
+
38
+ const arrAllElements = Array.from(allElements);
39
+
40
+ const elementIndex = arrAllElements.indexOf(node as HTMLElement);
41
+
42
+ const x = e.offsetX;
43
+ const y = e.offsetY;
44
+ console.log(e);
45
+ const newFeedback: FeedbackPayload = {
46
+ x,
47
+ y,
48
+ comment: "test",
49
+ path: window.location.pathname,
50
+ elementId,
51
+ elementIndex,
52
+ };
53
+ console.log(newFeedback);
54
+ createFeedback(newFeedback);
55
+ document.body.classList.remove("comment-cursor");
56
+ document.removeEventListener("click", handleClick, true);
57
+ setIsOpen(true);
58
+ };
59
+
60
+ const handleCreateFeedback = (e: React.MouseEvent<HTMLButtonElement>) => {
61
+ e.stopPropagation();
62
+ document.body.classList.add("comment-cursor");
63
+ document.addEventListener("click", handleClick, true);
64
+ };
65
+
66
+ return (
67
+ <>
68
+ <div
69
+ style={{
70
+ position: "fixed",
71
+ top: "calc(100vh - 62px)",
72
+ left: "calc(100vw - 124px)",
73
+ width: "100px",
74
+ height: "50px",
75
+ zIndex: 1000000,
76
+ backgroundColor: "#0085FF",
77
+ color: "white",
78
+ borderRadius: 4,
79
+ }}
80
+ onClick={() => setIsOpen(!isOpen)}
81
+ >
82
+ Show Feedback
83
+ </div>
84
+ <button
85
+ style={{
86
+ position: "fixed",
87
+ top: "calc(100vh - 122px)",
88
+ left: "calc(100vw - 124px)",
89
+ width: "100px",
90
+ height: "50px",
91
+ zIndex: 1000000,
92
+ backgroundColor: "#0085FF",
93
+ color: "white",
94
+ borderRadius: 4,
95
+ }}
96
+ onClick={handleCreateFeedback}
97
+ >
98
+ New Feedback
99
+ </button>
100
+ {isOpen &&
101
+ feedbacks &&
102
+ feedbacks
103
+ .filter((feedback) => feedback.path === window.location.pathname)
104
+ .map((feedback) => (
105
+ <FeedbackItem key={feedback.id} feedback={feedback} />
106
+ ))}
107
+ </>
108
+ );
109
+ }
110
+
111
+ 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;
@@ -0,0 +1,62 @@
1
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import api from "./api";
3
+ import { Feedback, FeedbackPayload } from "../types/types";
4
+
5
+ export const fetchFeedbacks = async () => {
6
+ const response = await api.get<Feedback[]>("/feedback");
7
+ return response.data;
8
+ };
9
+
10
+ export const createFeedback = async (feedback: FeedbackPayload) => {
11
+ const response = await api.post("/feedback", feedback);
12
+ return response.data;
13
+ };
14
+
15
+ export const updateFeedback = async (feedback: FeedbackPayload) => {
16
+ const response = await api.put("/feedback", feedback);
17
+ return response.data;
18
+ };
19
+
20
+ export const deleteFeedback = async (feedback: Feedback) => {
21
+ const response = await api.delete(`/feedback/${feedback.id}`);
22
+ return response.data;
23
+ };
24
+
25
+ export const useFeedbacks = () => {
26
+ return useQuery({
27
+ queryKey: ["feedbacks"],
28
+ queryFn: fetchFeedbacks,
29
+ });
30
+ };
31
+
32
+ export const useCreateFeedback = () => {
33
+ const queryClient = useQueryClient();
34
+ return useMutation({
35
+ mutationFn: createFeedback,
36
+ onSuccess: () => {
37
+ console.log("Feedback created");
38
+
39
+ return queryClient.invalidateQueries({ queryKey: ["feedbacks"] });
40
+ },
41
+ });
42
+ };
43
+
44
+ export const useUpdateFeedback = () => {
45
+ const queryClient = useQueryClient();
46
+ return useMutation({
47
+ mutationFn: updateFeedback,
48
+ onSuccess: () => {
49
+ return queryClient.invalidateQueries({ queryKey: ["feedbacks"] });
50
+ },
51
+ });
52
+ };
53
+
54
+ export const useDeleteFeedback = () => {
55
+ const queryClient = useQueryClient();
56
+ return useMutation({
57
+ mutationFn: deleteFeedback,
58
+ onSuccess: () => {
59
+ return queryClient.invalidateQueries({ queryKey: ["feedbacks"] });
60
+ },
61
+ });
62
+ };
@@ -0,0 +1,14 @@
1
+ export interface FeedbackPayload {
2
+ x: number;
3
+ y: number;
4
+ elementId: string;
5
+ elementIndex: number;
6
+ comment: string;
7
+ path: string;
8
+ }
9
+
10
+ export interface Feedback extends FeedbackPayload {
11
+ id: string;
12
+ createdAt: Date;
13
+ updatedAt: Date;
14
+ }
@@ -0,0 +1,46 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4
+ theme: {
5
+ extend: {
6
+ colors: {
7
+ gray: {
8
+ 50: "#F2F2F2",
9
+ 75: "#F4F5F7",
10
+ 100: "#E2E8F0",
11
+ 200: "#EAECF0",
12
+ 300: "#676D78",
13
+ 400: "#343D4B",
14
+ 500: "#010C1E",
15
+ 600: "#475467",
16
+ 700: "#CBD5E1",
17
+ 900: "#101828",
18
+ },
19
+ blue: {
20
+ 50: "#F5FAFF",
21
+ 100: "#EBF5FF",
22
+ 500: "#0085FF",
23
+ },
24
+ red: {
25
+ 100: "#FFD3E0",
26
+ 500: "#FF2462",
27
+ },
28
+ rose: {
29
+ 100: "#FFD3E0",
30
+ 200: "#FFA7C0",
31
+ },
32
+ orange: {
33
+ 50: "#FEF6EE",
34
+ 200: "#F9DBAF",
35
+ 700: "#B93815",
36
+ },
37
+ green: {
38
+ 50: "#ECFDF3",
39
+ 200: "#ABEFC6",
40
+ 700: "#067647",
41
+ },
42
+ },
43
+ },
44
+ },
45
+ plugins: [],
46
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "module": "ESNext",
5
+ "jsx": "react-jsx",
6
+ // Generate type definitions
7
+ "declaration": true,
8
+ "declarationDir": "dist/types",
9
+ "outDir": "dist",
10
+ "moduleResolution": "node",
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "strict": true
15
+ },
16
+ "include": ["src"],
17
+ "exclude": ["node_modules", "**/*.test.*"]
18
+ }
@@ -1,10 +0,0 @@
1
- <svg width="24" height="24" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
2
-
3
- <g id="Page-1" stroke="white" stroke-width="1" fill="white" fill-rule="evenodd" sketch:type="MSPage">
4
- <g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-100.000000, -255.000000)" fill="#000000">
5
- <path d="M116,281 C114.832,281 113.704,280.864 112.62,280.633 L107.912,283.463 L107.975,278.824 C104.366,276.654 102,273.066 102,269 C102,262.373 108.268,257 116,257 C123.732,257 130,262.373 130,269 C130,275.628 123.732,281 116,281 L116,281 Z M116,255 C107.164,255 100,261.269 100,269 C100,273.419 102.345,277.354 106,279.919 L106,287 L113.009,282.747 C113.979,282.907 114.977,283 116,283 C124.836,283 132,276.732 132,269 C132,261.269 124.836,255 116,255 L116,255 Z" id="comment-1" sketch:type="MSShapeGroup">
6
-
7
- </path>
8
- </g>
9
- </g>
10
- </svg>