feat: project tabs — files (placeholder) and settings (edit/delete)
All checks were successful
Deploy Web Client / deploy (push) Successful in 35s
All checks were successful
Deploy Web Client / deploy (push) Successful in 35s
This commit is contained in:
parent
e3c4d528a8
commit
6aa5e5d0da
@ -6,10 +6,14 @@ import { getProjects, Project } from "@/lib/api";
|
|||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import KanbanBoard from "@/components/KanbanBoard";
|
import KanbanBoard from "@/components/KanbanBoard";
|
||||||
import ChatPanel from "@/components/ChatPanel";
|
import ChatPanel from "@/components/ChatPanel";
|
||||||
|
import ProjectFiles from "@/components/ProjectFiles";
|
||||||
|
import ProjectSettings from "@/components/ProjectSettings";
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: "board", label: "📋 Доска" },
|
{ key: "board", label: "📋 Доска" },
|
||||||
{ key: "chat", label: "💬 Чат" },
|
{ key: "chat", label: "💬 Чат" },
|
||||||
|
{ key: "files", label: "📁 Файлы" },
|
||||||
|
{ key: "settings", label: "⚙️ Настройки" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function ProjectPage() {
|
export default function ProjectPage() {
|
||||||
@ -27,6 +31,10 @@ export default function ProjectPage() {
|
|||||||
|
|
||||||
const project = projects.find((p) => p.slug === slug);
|
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) {
|
if (loading) {
|
||||||
return <div className="flex h-screen items-center justify-center text-[var(--muted)]">Загрузка...</div>;
|
return <div className="flex h-screen items-center justify-center text-[var(--muted)]">Загрузка...</div>;
|
||||||
}
|
}
|
||||||
@ -77,6 +85,12 @@ export default function ProjectPage() {
|
|||||||
Чат недоступен
|
Чат недоступен
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{activeTab === "files" && (
|
||||||
|
<ProjectFiles project={project} />
|
||||||
|
)}
|
||||||
|
{activeTab === "settings" && (
|
||||||
|
<ProjectSettings project={project} onUpdated={handleProjectUpdated} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
17
src/components/ProjectFiles.tsx
Normal file
17
src/components/ProjectFiles.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
src/components/ProjectSettings.tsx
Normal file
139
src/components/ProjectSettings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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) });
|
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 ---
|
// --- Tasks ---
|
||||||
|
|
||||||
export async function getTasks(projectId: string): Promise<Task[]> {
|
export async function getTasks(projectId: string): Promise<Task[]> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user