mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-01-31 05:47:30 +00:00
Update package version to 1.1.3, add new dependencies for authentication and database management, and implement user authentication features including registration and login. Enhance API routes for protected access and integrate WebSocket authentication.
This commit is contained in:
@@ -23,6 +23,7 @@ import ClaudeLogo from './ClaudeLogo.jsx';
|
||||
|
||||
import ClaudeStatus from './ClaudeStatus';
|
||||
import { MicButton } from './MicButton.jsx';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
// Memoized message component to prevent unnecessary re-renders
|
||||
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => {
|
||||
@@ -949,7 +950,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
|
||||
setIsLoadingSessionMessages(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectName}/sessions/${sessionId}/messages`);
|
||||
const response = await api.sessionMessages(projectName, sessionId);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load session messages');
|
||||
}
|
||||
@@ -1451,7 +1452,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
|
||||
const fetchProjectFiles = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${selectedProject.name}/files`);
|
||||
const response = await api.getFiles(selectedProject.name);
|
||||
if (response.ok) {
|
||||
const files = await response.json();
|
||||
// Flatten the file tree to get all file paths
|
||||
|
||||
@@ -10,6 +10,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect, RangeSetBuilder } from '@codemirror/state';
|
||||
import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function CodeEditor({ file, onClose, projectPath }) {
|
||||
const [content, setContent] = useState('');
|
||||
@@ -139,7 +140,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch(`/api/projects/${file.projectName}/file?filePath=${encodeURIComponent(file.path)}`);
|
||||
const response = await api.readFile(file.projectName, file.path);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
|
||||
@@ -176,16 +177,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${file.projectName}/file`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath: file.path,
|
||||
content: content
|
||||
})
|
||||
});
|
||||
const response = await api.saveFile(file.projectName, file.path, content);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Folder, FolderOpen, File, FileText, FileCode } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import ImageViewer from './ImageViewer';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function FileTree({ selectedProject }) {
|
||||
const [files, setFiles] = useState([]);
|
||||
@@ -22,7 +23,7 @@ function FileTree({ selectedProject }) {
|
||||
const fetchFiles = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${selectedProject.name}/files`);
|
||||
const response = await api.getFiles(selectedProject.name);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles } from 'lucide-react';
|
||||
import { MicButton } from './MicButton.jsx';
|
||||
import { authenticatedFetch } from '../utils/api';
|
||||
|
||||
function GitPanel({ selectedProject, isMobile }) {
|
||||
const [gitStatus, setGitStatus] = useState(null);
|
||||
@@ -55,7 +56,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await response.json();
|
||||
|
||||
console.log('Git status response:', data);
|
||||
@@ -93,7 +94,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
const fetchBranches = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const response = await authenticatedFetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.branches) {
|
||||
@@ -106,7 +107,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
const switchBranch = async (branchName) => {
|
||||
try {
|
||||
const response = await fetch('/api/git/checkout', {
|
||||
const response = await authenticatedFetch('/api/git/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -133,7 +134,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
setIsCreatingBranch(true);
|
||||
try {
|
||||
const response = await fetch('/api/git/create-branch', {
|
||||
const response = await authenticatedFetch('/api/git/create-branch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -162,7 +163,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
const fetchFileDiff = async (filePath) => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
|
||||
const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.diff) {
|
||||
@@ -178,7 +179,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
const fetchRecentCommits = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`);
|
||||
const response = await authenticatedFetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.commits) {
|
||||
@@ -191,7 +192,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
const fetchCommitDiff = async (commitHash) => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`);
|
||||
const response = await authenticatedFetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.diff) {
|
||||
@@ -208,7 +209,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
const generateCommitMessage = async () => {
|
||||
setIsGeneratingMessage(true);
|
||||
try {
|
||||
const response = await fetch('/api/git/generate-commit-message', {
|
||||
const response = await authenticatedFetch('/api/git/generate-commit-message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -275,7 +276,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
||||
|
||||
setIsCommitting(true);
|
||||
try {
|
||||
const response = await fetch('/api/git/commit', {
|
||||
const response = await authenticatedFetch('/api/git/commit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
108
src/components/LoginForm.jsx
Normal file
108
src/components/LoginForm.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
|
||||
const LoginForm = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!username || !password) {
|
||||
setError('Please enter both username and password');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await login(username, password);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-card rounded-lg shadow-lg border border-border p-8 space-y-6">
|
||||
{/* Logo and Title */}
|
||||
<div className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<ClaudeLogo size={64} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Welcome Back</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Sign in to your Claude Code UI account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-md">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your credentials to access Claude Code UI
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
42
src/components/ProtectedRoute.jsx
Normal file
42
src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import SetupForm from './SetupForm';
|
||||
import LoginForm from './LoginForm';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
|
||||
const LoadingScreen = () => (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<ClaudeLogo size={64} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">Claude Code UI</h1>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { user, isLoading, needsSetup } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (needsSetup) {
|
||||
return <SetupForm />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
135
src/components/SetupForm.jsx
Normal file
135
src/components/SetupForm.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
|
||||
const SetupForm = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { register } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.length < 3) {
|
||||
setError('Username must be at least 3 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
setError('Password must be at least 6 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await register(username, password);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-card rounded-lg shadow-lg border border-border p-8 space-y-6">
|
||||
{/* Logo and Title */}
|
||||
<div className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<ClaudeLogo size={64} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Welcome to Claude Code UI</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Set up your account to get started
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Setup Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-1">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground mb-1">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Confirm your password"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-md">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
{isLoading ? 'Setting up...' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is a single-user system. Only one account can be created.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupForm;
|
||||
@@ -322,10 +322,21 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
if (isConnecting || isConnected) return;
|
||||
|
||||
try {
|
||||
// Get authentication token
|
||||
const token = localStorage.getItem('auth-token');
|
||||
if (!token) {
|
||||
console.error('No authentication token found for Shell WebSocket connection');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch server configuration to get the correct WebSocket URL
|
||||
let wsBaseUrl;
|
||||
try {
|
||||
const configResponse = await fetch('/api/config');
|
||||
const configResponse = await fetch('/api/config', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const config = await configResponse.json();
|
||||
wsBaseUrl = config.wsUrl;
|
||||
|
||||
@@ -343,7 +354,8 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
wsBaseUrl = `${protocol}//${window.location.hostname}:${apiPort}`;
|
||||
}
|
||||
|
||||
const wsUrl = `${wsBaseUrl}/shell`;
|
||||
// Include token in WebSocket URL as query parameter
|
||||
const wsUrl = `${wsBaseUrl}/shell?token=${encodeURIComponent(token)}`;
|
||||
|
||||
ws.current = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Input } from './ui/input';
|
||||
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2 } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
// Move formatTimeAgo outside component to avoid recreation on every render
|
||||
const formatTimeAgo = (dateString, currentTime) => {
|
||||
@@ -132,13 +133,7 @@ function Sidebar({
|
||||
|
||||
const saveProjectName = async (projectName) => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectName}/rename`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ displayName: editingName }),
|
||||
});
|
||||
const response = await api.renameProject(projectName, editingName);
|
||||
|
||||
if (response.ok) {
|
||||
// Refresh projects to get updated data
|
||||
@@ -164,9 +159,7 @@ function Sidebar({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectName}/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const response = await api.deleteSession(projectName, sessionId);
|
||||
|
||||
if (response.ok) {
|
||||
// Call parent callback if provided
|
||||
@@ -189,9 +182,7 @@ function Sidebar({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectName}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const response = await api.deleteProject(projectName);
|
||||
|
||||
if (response.ok) {
|
||||
// Call parent callback if provided
|
||||
@@ -218,15 +209,7 @@ function Sidebar({
|
||||
setCreatingProject(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/projects/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
path: newProjectPath.trim()
|
||||
}),
|
||||
});
|
||||
const response = await api.createProject(newProjectPath.trim());
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user