diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 22088b5..989ed60 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -8,10 +8,13 @@ "name": "headquarter-web", "version": "0.1.0", "dependencies": { + "@types/prismjs": "^1.26.6", "axios": "^1.6.0", + "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.0", + "react-simple-code-editor": "^0.14.1", "tailwindcss": "^3.3.0" }, "devDependencies": { @@ -2127,6 +2130,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -5091,6 +5100,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -5205,6 +5223,16 @@ "react-dom": ">=16.8" } }, + "node_modules/react-simple-code-editor": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz", + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index 7e1414c..2158dbb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,10 +11,13 @@ "test": "vitest run" }, "dependencies": { + "@types/prismjs": "^1.26.6", "axios": "^1.6.0", + "prismjs": "^1.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.0", + "react-simple-code-editor": "^0.14.1", "tailwindcss": "^3.3.0" }, "devDependencies": { diff --git a/apps/web/src/components/code-editor.tsx b/apps/web/src/components/code-editor.tsx new file mode 100644 index 0000000..5f56d03 --- /dev/null +++ b/apps/web/src/components/code-editor.tsx @@ -0,0 +1,56 @@ +import React, { useEffect } from "react"; +import Editor from "react-simple-code-editor"; +import { highlightCode, loadLanguage } from "../utils/language"; + +interface CodeEditorProps { + value: string; + onChange: (value: string) => void; + language: string; + readOnly?: boolean; +} + +export const CodeEditor: React.FC = ({ + value, + onChange, + language, + readOnly = false, +}) => { + useEffect(() => { + const highlight = async () => { + await loadLanguage(language); + }; + void highlight(); + }, [language]); + + const hightlightWithLineNumbers = (input: string) => + input + .split("\n") + .map( + (line, i) => + `
${ + i + 1 + }${highlightCode( + line || " ", + language + )}
` + ) + .join(""); + + return ( +
+ +
+ ); +}; diff --git a/apps/web/src/components/commit-dialog.tsx b/apps/web/src/components/commit-dialog.tsx new file mode 100644 index 0000000..0f3aefd --- /dev/null +++ b/apps/web/src/components/commit-dialog.tsx @@ -0,0 +1,146 @@ +import React, { useState } from "react"; + +interface CommitDialogProps { + isOpen: boolean; + filePath: string; + originalContent: string; + newContent: string; + onCommit: (message: string) => Promise; + onCancel: () => void; +} + +export const CommitDialog: React.FC = ({ + isOpen, + filePath, + originalContent, + newContent, + onCommit, + onCancel, +}) => { + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(">"); + + if (!isOpen) return null; + + const generateDiff = () => { + const originalLines = originalContent.split("\n"); + const newLines = newContent.split("\n"); + const maxLines = Math.max(originalLines.length, newLines.length); + const diff: { type: "same" | "added" | "removed"; line: string; lineNum: number }[] = []; + + for (let i = 0; i < maxLines; i++) { + const original = originalLines[i] || ""; + const updated = newLines[i] || ""; + + if (original === updated) { + diff.push({ type: "same", line: updated, lineNum: i + 1 }); + } else { + if (original) { + diff.push({ type: "removed", line: original, lineNum: i + 1 }); + } + if (updated) { + diff.push({ type: "added", line: updated, lineNum: i + 1 }); + } + } + } + + return diff; + }; + + const handleCommit = async () => { + if (!message.trim()) { + setError("Please enter a commit message"); + return; + } + + setLoading(true); + setError(""); + try { + await onCommit(message); + } catch { + setError("Failed to commit changes"); + } finally { + setLoading(false); + } + }; + + const diff = generateDiff(); + const hasChanges = diff.some((d) => d.type !== "same"); + + return ( +
+
+
+

Commit Changes

+ +
+ +
+

+ Editing: {filePath} +

+ + {!hasChanges && ( +
No changes to commit
+ )} + + {hasChanges && ( +
+

Changes

+
+ {diff.map((line, i) => ( +
+ {line.lineNum} + + {line.type === "added" && "+"} + {line.type === "removed" && "-"} + {line.type === "same" && " "} + + {line.line} +
+ ))} +
+
+ )} + +
+ +