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.
- package/dist/src/index.css +2 -1
- package/eslint.config.js +28 -0
- package/index.d.ts +15 -0
- package/package.json +1 -5
- package/rollup.config.js +40 -0
- package/src/App.tsx +16 -0
- package/src/components/buttons/IconButton.tsx +27 -0
- package/src/components/buttons/PlusButton.tsx +20 -0
- package/src/components/feedback/FeedbackInput.tsx +23 -0
- package/src/components/feedback/FeedbackItem.tsx +89 -0
- package/src/components/misc/Avatar.tsx +3 -0
- package/src/components/misc/Modal.tsx +43 -0
- package/src/index.css +8 -0
- package/src/pages/FeedbacksCanvas.tsx +38 -0
- package/src/pages/Overlay.tsx +26 -0
- package/src/pages/Root.tsx +111 -0
- package/src/services/api.ts +22 -0
- package/src/services/feedback.ts +62 -0
- package/src/types/types.d.ts +14 -0
- package/tailwind.config.js +46 -0
- package/tsconfig.json +18 -0
- package/assets/comment.svg +0 -10
package/dist/src/index.css
CHANGED
package/eslint.config.js
ADDED
|
@@ -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.
|
|
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"
|
package/rollup.config.js
ADDED
|
@@ -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,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,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
|
+
}
|
package/assets/comment.svg
DELETED
|
@@ -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>
|