feat: project tabs — files (placeholder) and settings (edit/delete)
All checks were successful
Deploy Web Client / deploy (push) Successful in 35s

This commit is contained in:
Markov 2026-02-23 12:28:37 +01:00
parent e3c4d528a8
commit 6aa5e5d0da
4 changed files with 178 additions and 0 deletions

View File

@ -6,10 +6,14 @@ import { getProjects, Project } from "@/lib/api";
import Sidebar from "@/components/Sidebar";
import KanbanBoard from "@/components/KanbanBoard";
import ChatPanel from "@/components/ChatPanel";
import ProjectFiles from "@/components/ProjectFiles";
import ProjectSettings from "@/components/ProjectSettings";
const TABS = [
{ key: "board", label: "📋 Доска" },
{ key: "chat", label: "💬 Чат" },
{ key: "files", label: "📁 Файлы" },
{ key: "settings", label: "⚙️ Настройки" },
];
export default function ProjectPage() {
@ -27,6 +31,10 @@ export default function ProjectPage() {
const project = projects.find((p) => p.slug === slug);
const handleProjectUpdated = (updated: Project) => {
setProjects((prev) => prev.map((p) => (p.id === updated.id ? updated : p)));
};
if (loading) {
return <div className="flex h-screen items-center justify-center text-[var(--muted)]">Загрузка...</div>;
}
@ -77,6 +85,12 @@ export default function ProjectPage() {
Чат недоступен
</div>
)}
{activeTab === "files" && (
<ProjectFiles project={project} />
)}
{activeTab === "settings" && (
<ProjectSettings project={project} onUpdated={handleProjectUpdated} />
)}
</div>
</main>
</div>

View File

@ -0,0 +1,17 @@
"use client";
import { Project } from "@/lib/api";
interface Props {
project: Project;
}
export default function ProjectFiles({ project }: Props) {
return (
<div className="flex flex-col items-center justify-center h-full text-[var(--muted)]">
<div className="text-4xl mb-4">📁</div>
<div className="text-sm">Файлы проекта</div>
<div className="text-xs mt-1">Скоро здесь можно будет загружать и просматривать файлы</div>
</div>
);
}

View File

@ -0,0 +1,139 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Project, updateProject, deleteProject } from "@/lib/api";
interface Props {
project: Project;
onUpdated: (p: Project) => void;
}
export default function ProjectSettings({ project, onUpdated }: Props) {
const router = useRouter();
const [name, setName] = useState(project.name);
const [description, setDescription] = useState(project.description || "");
const [repoUrls, setRepoUrls] = useState(project.repo_urls?.join("\n") || "");
const [status, setStatus] = useState(project.status);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const handleSave = async () => {
setSaving(true);
try {
const urls = repoUrls.split("\n").map((u) => u.trim()).filter(Boolean);
const updated = await updateProject(project.slug, {
name: name.trim(),
description: description.trim() || null,
repo_urls: urls,
status,
});
onUpdated(updated);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (e) {
console.error(e);
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
try {
await deleteProject(project.slug);
router.push("/");
} catch (e) {
console.error(e);
}
};
return (
<div className="max-w-xl mx-auto p-6 space-y-6">
<h2 className="text-lg font-bold">Настройки проекта</h2>
<div>
<label className="block text-sm text-[var(--muted)] mb-1">Название</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm"
/>
</div>
<div>
<label className="block text-sm text-[var(--muted)] mb-1">Описание</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm resize-y"
rows={3}
placeholder="Описание проекта..."
/>
</div>
<div>
<label className="block text-sm text-[var(--muted)] mb-1">Репозитории (по одному на строку)</label>
<textarea
value={repoUrls}
onChange={(e) => setRepoUrls(e.target.value)}
className="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm font-mono resize-y"
rows={3}
placeholder="https://github.com/..."
/>
</div>
<div>
<label className="block text-sm text-[var(--muted)] mb-1">Статус</label>
<select
value={status}
onChange={(e) => setStatus(e.target.value)}
className="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm"
>
<option value="active">Active</option>
<option value="archived">Archived</option>
<option value="paused">Paused</option>
</select>
</div>
<div className="flex items-center gap-3">
<button
onClick={handleSave}
disabled={saving || !name.trim()}
className="px-4 py-2 bg-[var(--accent)] text-white rounded-lg text-sm hover:opacity-90 disabled:opacity-50"
>
{saving ? "..." : "Сохранить"}
</button>
{saved && <span className="text-sm text-green-400">Сохранено </span>}
</div>
<div className="pt-6 border-t border-[var(--border)]">
<div className="text-sm text-[var(--muted)] mb-2">Зона опасности</div>
{confirmDelete ? (
<div className="flex items-center gap-2">
<span className="text-xs text-red-400">Проект и все задачи будут удалены!</span>
<button
onClick={handleDelete}
className="px-3 py-1.5 bg-red-500/20 text-red-400 rounded text-xs hover:bg-red-500/30"
>
Да, удалить
</button>
<button
onClick={() => setConfirmDelete(false)}
className="px-3 py-1.5 text-xs text-[var(--muted)]"
>
Отмена
</button>
</div>
) : (
<button
onClick={() => setConfirmDelete(true)}
className="text-xs text-red-400/60 hover:text-red-400"
>
Удалить проект
</button>
)}
</div>
</div>
);
}

View File

@ -140,6 +140,14 @@ export async function createProject(data: { name: string; slug: string; descript
return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) });
}
export async function updateProject(slug: string, data: Partial<Pick<Project, "name" | "description" | "repo_urls" | "status">>): Promise<Project> {
return request(`/api/v1/projects/${slug}`, { method: "PATCH", body: JSON.stringify(data) });
}
export async function deleteProject(slug: string): Promise<void> {
await request(`/api/v1/projects/${slug}`, { method: "DELETE" });
}
// --- Tasks ---
export async function getTasks(projectId: string): Promise<Task[]> {