chore(projects): remove unused route pages
This commit is contained in:
@@ -1,189 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useBlocker, useNavigate } from "react-router-dom";
|
||||
import { Briefcase, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getClients } from "../api/clients";
|
||||
import { createProject } from "../api/projects";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Select } from "../components/ui/Select";
|
||||
import { TextAreaInput } from "../components/ui/TextAreaInput";
|
||||
import { useWorkspace } from "../context/WorkspaceContext";
|
||||
import { useTranslation } from "../hooks/useTranslation";
|
||||
import { PROJECTS_CREATE, canWorkspace } from "../lib/permissions";
|
||||
|
||||
const COLORS = [
|
||||
"#3B82F6",
|
||||
"#10B981",
|
||||
"#F59E0B",
|
||||
"#EF4444",
|
||||
"#8B5CF6",
|
||||
"#EC4899",
|
||||
"#14B8A6",
|
||||
"#64748B",
|
||||
];
|
||||
|
||||
export default function ProjectCreate() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { activeWorkspace } = useWorkspace();
|
||||
const canCreateProject = canWorkspace(activeWorkspace?.my_role, PROJECTS_CREATE);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState(COLORS[0]);
|
||||
const [client, setClient] = useState("");
|
||||
const [clientsList, setClientsList] = useState<{ id: string; name: string }[]>([]);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const hasUnsavedChanges = name.trim() !== "" || description.trim() !== "" || client !== "" || color !== COLORS[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (activeWorkspace && !canCreateProject) {
|
||||
toast.error("You do not have permission to create projects.");
|
||||
navigate("/projects");
|
||||
}
|
||||
}, [activeWorkspace, canCreateProject, navigate]);
|
||||
|
||||
useBlocker(({ currentLocation, nextLocation }) => {
|
||||
if (hasUnsavedChanges && !isSaving && currentLocation.pathname !== nextLocation.pathname) {
|
||||
return !window.confirm(t.confirmLeave || "You have unsaved changes. Are you sure you want to leave?");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeWorkspace?.id) return;
|
||||
|
||||
setName("");
|
||||
setDescription("");
|
||||
setColor(COLORS[0]);
|
||||
setClient("");
|
||||
setClientsList([]);
|
||||
setIsLoadingData(true);
|
||||
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
const clientsRes = await getClients(activeWorkspace.id);
|
||||
setClientsList(clientsRes.results || []);
|
||||
} catch {
|
||||
toast.error(t.projects?.clientFetchError || "Failed to load clients.");
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadInitialData();
|
||||
}, [activeWorkspace?.id, t.projects?.clientFetchError]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !activeWorkspace) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const newProject = await createProject({
|
||||
workspace: activeWorkspace.id,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
client: client || null,
|
||||
is_archived: false,
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent("project_created", { detail: newProject }));
|
||||
toast.success(t.projects?.createSuccess || "Project created successfully.");
|
||||
navigate("/projects");
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || t.projects?.createError || "Failed to create project.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeWorkspace) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-y-auto bg-slate-50 p-4 dark:bg-slate-900 sm:p-6">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h1 className="mb-6 text-2xl font-bold text-slate-800 dark:text-slate-200">
|
||||
{t.projects?.createNew || "Create New Project"}
|
||||
</h1>
|
||||
|
||||
<div className="rounded-3xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<form onSubmit={handleSubmit} className="space-y-6 p-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-10 w-10 shrink-0 rounded-lg shadow-sm" style={{ backgroundColor: color }} />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t.projects?.namePlaceholder || "Project name..."}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{COLORS.map((paletteColor) => (
|
||||
<button
|
||||
key={paletteColor}
|
||||
type="button"
|
||||
onClick={() => setColor(paletteColor)}
|
||||
className={`h-5 w-5 shrink-0 rounded-full transition-all duration-150 ${
|
||||
color === paletteColor
|
||||
? "scale-110 ring-2 ring-blue-500 ring-offset-2 ring-offset-white shadow-md dark:ring-offset-slate-900"
|
||||
: "shadow-sm hover:scale-110"
|
||||
}`}
|
||||
style={{ backgroundColor: paletteColor }}
|
||||
aria-label={`Select color ${paletteColor}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 flex items-center gap-2 text-slate-700 dark:text-slate-300">
|
||||
<Briefcase size={16} />
|
||||
{t.projects?.client || "Client"}
|
||||
</label>
|
||||
<Select
|
||||
value={client}
|
||||
onChange={setClient}
|
||||
options={[
|
||||
{ value: "", label: t.projects?.noClient || "No Client" },
|
||||
...clientsList.map((item) => ({ value: item.id, label: item.name })),
|
||||
]}
|
||||
isLoading={isLoadingData}
|
||||
className="w-full"
|
||||
buttonClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-slate-700 dark:text-slate-300">
|
||||
{t.projects?.descriptionLabel || "Description"}
|
||||
</label>
|
||||
<TextAreaInput
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t.projects?.descriptionPlaceholder || "Add more details..."}
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t border-slate-200 pt-4 dark:border-slate-700">
|
||||
<Button variant="ghost" type="button" onClick={() => navigate("/projects")}>
|
||||
{t.cancel || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving || !name.trim()}>
|
||||
{isSaving ? <Loader2 className="me-2 h-4 w-4 animate-spin" /> : null}
|
||||
{t.create || "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useBlocker, useNavigate, useParams } from "react-router-dom";
|
||||
import { Briefcase, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getClients } from "../api/clients";
|
||||
import { getProject, updateProject } from "../api/projects";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Select } from "../components/ui/Select";
|
||||
import { TextAreaInput } from "../components/ui/TextAreaInput";
|
||||
import { useWorkspace } from "../context/WorkspaceContext";
|
||||
import { useTranslation } from "../hooks/useTranslation";
|
||||
import { PROJECTS_EDIT, canWorkspace } from "../lib/permissions";
|
||||
|
||||
const COLORS = [
|
||||
"#3B82F6",
|
||||
"#10B981",
|
||||
"#F59E0B",
|
||||
"#EF4444",
|
||||
"#8B5CF6",
|
||||
"#EC4899",
|
||||
"#14B8A6",
|
||||
"#64748B",
|
||||
];
|
||||
|
||||
export default function ProjectEdit() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { t } = useTranslation();
|
||||
const { activeWorkspace } = useWorkspace();
|
||||
const canEditProject = canWorkspace(activeWorkspace?.my_role, PROJECTS_EDIT);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState(COLORS[0]);
|
||||
const [client, setClient] = useState("");
|
||||
const [clientsList, setClientsList] = useState<{ id: string; name: string }[]>([]);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [isProjectLoading, setIsProjectLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const hasUnsavedChanges = name.trim() !== "";
|
||||
|
||||
useEffect(() => {
|
||||
if (activeWorkspace && !canEditProject) {
|
||||
toast.error("You do not have permission to edit projects.");
|
||||
navigate("/projects");
|
||||
}
|
||||
}, [activeWorkspace, canEditProject, navigate]);
|
||||
|
||||
useBlocker(({ currentLocation, nextLocation }) => {
|
||||
if (hasUnsavedChanges && !isSaving && !isProjectLoading && currentLocation.pathname !== nextLocation.pathname) {
|
||||
return !window.confirm(t.confirmLeave || "You have unsaved changes. Are you sure you want to leave?");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeWorkspace?.id || !id) return;
|
||||
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
const [clientsRes, projectRes] = await Promise.all([
|
||||
getClients(activeWorkspace.id),
|
||||
getProject(id),
|
||||
]);
|
||||
|
||||
setClientsList(clientsRes.results || []);
|
||||
setName(projectRes.name || "");
|
||||
setDescription(projectRes.description || "");
|
||||
setColor(projectRes.color || COLORS[0]);
|
||||
setClient(projectRes.client?.id || projectRes.client || "");
|
||||
} catch {
|
||||
toast.error("Failed to load project data.");
|
||||
navigate("/projects");
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
setIsProjectLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadInitialData();
|
||||
}, [activeWorkspace?.id, id, navigate]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !activeWorkspace || !id) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const updatedProject = await updateProject(id, {
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
client: client || null,
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent("project_updated", { detail: updatedProject }));
|
||||
toast.success(t.projects?.updateSuccess || "Project updated successfully.");
|
||||
navigate("/projects");
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || t.projects?.updateError || "Failed to update project.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeWorkspace) return null;
|
||||
|
||||
if (isProjectLoading) {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-slate-50 dark:bg-slate-900">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-y-auto bg-slate-50 p-4 dark:bg-slate-900 sm:p-6">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h1 className="mb-6 text-2xl font-bold text-slate-800 dark:text-slate-200">
|
||||
{t.projects?.edit || "Edit Project"}
|
||||
</h1>
|
||||
|
||||
<div className="rounded-3xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<form onSubmit={handleSubmit} className="space-y-6 p-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-10 w-10 shrink-0 rounded-lg shadow-sm" style={{ backgroundColor: color }} />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t.projects?.namePlaceholder || "Project name..."}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{COLORS.map((paletteColor) => (
|
||||
<button
|
||||
key={paletteColor}
|
||||
type="button"
|
||||
onClick={() => setColor(paletteColor)}
|
||||
className={`h-5 w-5 shrink-0 rounded-full transition-all duration-150 ${
|
||||
color === paletteColor
|
||||
? "scale-110 ring-2 ring-blue-500 ring-offset-2 ring-offset-white shadow-md dark:ring-offset-slate-900"
|
||||
: "shadow-sm hover:scale-110"
|
||||
}`}
|
||||
style={{ backgroundColor: paletteColor }}
|
||||
aria-label={`Select color ${paletteColor}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 flex items-center gap-2 text-slate-700 dark:text-slate-300">
|
||||
<Briefcase size={16} />
|
||||
{t.projects?.client || "Client"}
|
||||
</label>
|
||||
<Select
|
||||
value={client}
|
||||
onChange={setClient}
|
||||
options={[
|
||||
{ value: "", label: t.projects?.noClient || "No Client" },
|
||||
...clientsList.map((item) => ({ value: item.id, label: item.name })),
|
||||
]}
|
||||
isLoading={isLoadingData}
|
||||
className="w-full"
|
||||
buttonClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-slate-700 dark:text-slate-300">
|
||||
{t.projects?.descriptionLabel || "Description"}
|
||||
</label>
|
||||
<TextAreaInput
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t.projects?.descriptionPlaceholder || "Add more details..."}
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 border-t border-slate-200 pt-4 dark:border-slate-700">
|
||||
<Button variant="ghost" type="button" onClick={() => navigate("/projects")}>
|
||||
{t.cancel || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSaving || !name.trim()}>
|
||||
{isSaving ? <Loader2 className="me-2 h-4 w-4 animate-spin" /> : null}
|
||||
{t.save || "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user