feat: add project settings page with tabbed layout

- Create SettingsTabLayout component with sidebar navigation
- Create ProjectSettingsPage with General settings tab
- Create RepositoriesSettingsTab for repo management
- Add Members placeholder tab
- Update router with settings routes
- Add CSS styles for settings layout
- Navigate to /projects/:id/settings from workspace header
This commit is contained in:
Fusion
2026-05-19 15:26:57 +02:00
parent 965f6f6585
commit 0bc0d99c21
17 changed files with 1136 additions and 16 deletions
@@ -0,0 +1,71 @@
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { apiClient } from "../api/client";
import { GitRepository } from "../api/git_repositories";
export const RepositoriesSettingsTab: React.FC = () => {
const { projectId } = useParams<{ projectId: string }>();
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(">");
useEffect(() => {
const fetchRepositories = async () => {
try {
const response = await apiClient.get(
`/projects/${projectId}/repositories`
);
setRepositories(response.data);
} catch (err) {
setError("Failed to load repositories");
} finally {
setLoading(false);
}
};
fetchRepositories();
}, [projectId]);
const handleDelete = async (repoId: string) => {
if (!window.confirm("Are you sure you want to delete this repository?")) return;
try {
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
setRepositories(repositories.filter((r) => r.id !== repoId));
} catch (err) {
setError("Failed to delete repository");
}
};
if (loading) return <div>Loading...</div>;
return (
<div className="repositories-settings-tab">
<h2>Repositories</h2>
{error && <div className="error-message">{error}</div>}
<div className="repositories-list">
{repositories.length === 0 ? (
<p>No repositories yet.</p>
) : (
repositories.map((repo) => (
<div key={repo.id} className="repository-card">
<div className="repository-info">
<h3>{repo.name}</h3>
<p>{repo.remote_url}</p>
<span className="repo-type">
{repo.is_mirror ? "Mirror" : "Clone"}
</span>
</div>
<button
onClick={() => handleDelete(repo.id)}
className="btn-danger"
>
Delete
</button>
</div>
))
)}
</div>
</div>
);
};
@@ -0,0 +1,43 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
interface Tab {
id: string;
label: string;
path: string;
}
interface SettingsTabLayoutProps {
tabs: Tab[];
children: React.ReactNode;
basePath: string;
}
export const SettingsTabLayout: React.FC<SettingsTabLayoutProps> = ({
tabs,
children,
basePath,
}) => {
const location = useLocation();
return (
<div className="settings-layout">
<aside className="settings-sidebar">
<nav className="settings-nav">
{tabs.map((tab) => (
<Link
key={tab.id}
to={`${basePath}/${tab.path}`}
className={`settings-nav-link ${
location.pathname.includes(tab.path) ? "active" : ""
}`}
>
{tab.label}
</Link>
))}
</nav>
</aside>
<main className="settings-content">{children}</main>
</div>
);
};
@@ -0,0 +1,45 @@
import { Link } from "react-router-dom";
interface WorkspaceHeaderProps {
project: {
id: string;
name: string;
description?: string | null;
};
currentRepo?: {
id: string;
name: string;
} | null;
}
export const WorkspaceHeader = ({ project, currentRepo }: WorkspaceHeaderProps) => {
return (
<div className="workspace-header">
<div className="workspace-header-left">
<div className="workspace-header-icon">📁</div>
<div className="workspace-header-info">
<h1 className="workspace-header-title">{project.name}</h1>
{currentRepo && (
<span className="workspace-header-subtitle">{currentRepo.name}</span>
)}
</div>
</div>
<div className="workspace-header-actions">
<Link
className="workspace-header-action-btn"
to={`/projects/${project.id}/repositories/${currentRepo?.id || ""}/history`}
>
<span className="icon">🕐</span>
History
</Link>
<Link
className="workspace-header-action-btn"
to={`/projects/${project.id}/settings`}
>
<span className="icon"></span>
Settings
</Link>
</div>
</div>
);
};