"use client";
import { useState, useEffect, useCallback } from "react";
import type { ReactElement } from "react";
import { useParams, useRouter } from "next/navigation";
import { ArrowLeft } from "lucide-react";
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
import { fetchProject, type ProjectDetail } from "@/lib/api/projects";
import { useWorkspaceId } from "@/lib/hooks";
interface BadgeStyle {
label: string;
bg: string;
color: string;
}
interface StatusBadgeProps {
style: BadgeStyle;
}
interface MetaItemProps {
label: string;
value: string;
}
function getProjectStatusStyle(status: string): BadgeStyle {
switch (status) {
case "PLANNING":
return { label: "Planning", bg: "rgba(47,128,255,0.15)", color: "var(--primary)" };
case "ACTIVE":
return { label: "Active", bg: "rgba(20,184,166,0.15)", color: "var(--success)" };
case "PAUSED":
return { label: "Paused", bg: "rgba(245,158,11,0.15)", color: "var(--warn)" };
case "COMPLETED":
return { label: "Completed", bg: "rgba(139,92,246,0.15)", color: "var(--purple)" };
case "ARCHIVED":
return { label: "Archived", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
default:
return { label: status, bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
}
}
function getPriorityStyle(priority: string | null | undefined): BadgeStyle {
switch (priority) {
case "HIGH":
return { label: "High", bg: "rgba(229,72,77,0.15)", color: "var(--danger)" };
case "MEDIUM":
return { label: "Medium", bg: "rgba(245,158,11,0.15)", color: "var(--warn)" };
case "LOW":
return { label: "Low", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
default:
return { label: "Unspecified", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
}
}
function getTaskStatusStyle(status: string): BadgeStyle {
switch (status) {
case "NOT_STARTED":
return { label: "Not Started", bg: "rgba(47,128,255,0.15)", color: "var(--primary)" };
case "IN_PROGRESS":
return { label: "In Progress", bg: "rgba(245,158,11,0.15)", color: "var(--warn)" };
case "PAUSED":
return { label: "Paused", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
case "COMPLETED":
return { label: "Completed", bg: "rgba(20,184,166,0.15)", color: "var(--success)" };
case "ARCHIVED":
return { label: "Archived", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
default:
return { label: status, bg: "rgba(143,157,183,0.15)", color: "var(--muted)" };
}
}
function formatDate(iso: string | null | undefined): string {
if (!iso) return "Not set";
try {
return new Date(iso).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
} catch {
return iso;
}
}
function formatDateTime(iso: string | null | undefined): string {
if (!iso) return "Not set";
try {
return new Date(iso).toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
} catch {
return iso;
}
}
function toFriendlyErrorMessage(error: unknown): string {
const fallback = "We had trouble loading this project. Please try again when you're ready.";
if (!(error instanceof Error)) {
return fallback;
}
const message = error.message.trim();
if (message.toLowerCase().includes("not found")) {
return "Project not found. It may have been deleted or you may not have access to it.";
}
return message || fallback;
}
function StatusBadge({ style: statusStyle }: StatusBadgeProps): ReactElement {
return (
{statusStyle.label}
);
}
function MetaItem({ label, value }: MetaItemProps): ReactElement {
return (
);
}
export default function ProjectDetailPage(): ReactElement {
const router = useRouter();
const params = useParams<{ id: string | string[] }>();
const workspaceId = useWorkspaceId();
const rawProjectId = params.id;
const projectId = Array.isArray(rawProjectId) ? (rawProjectId[0] ?? null) : rawProjectId;
const [project, setProject] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const loadProject = useCallback(async (id: string, wsId: string): Promise => {
try {
setIsLoading(true);
setError(null);
const data = await fetchProject(id, wsId);
setProject(data);
} catch (err: unknown) {
console.error("[ProjectDetail] Failed to fetch project:", err);
setProject(null);
setError(toFriendlyErrorMessage(err));
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
if (!projectId) {
setProject(null);
setError("The project link is invalid. Please return to the projects page.");
setIsLoading(false);
return;
}
if (!workspaceId) {
setProject(null);
setError("Select a workspace to view this project.");
setIsLoading(false);
return;
}
const id = projectId;
const wsId = workspaceId;
let cancelled = false;
async function load(): Promise {
try {
setIsLoading(true);
setError(null);
const data = await fetchProject(id, wsId);
if (!cancelled) {
setProject(data);
}
} catch (err: unknown) {
console.error("[ProjectDetail] Failed to fetch project:", err);
if (!cancelled) {
setProject(null);
setError(toFriendlyErrorMessage(err));
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
}
void load();
return (): void => {
cancelled = true;
};
}, [projectId, workspaceId]);
function handleRetry(): void {
if (!projectId || !workspaceId) return;
void loadProject(projectId, workspaceId);
}
function handleBack(): void {
router.push("/projects");
}
const projectStatus = project ? getProjectStatusStyle(project.status) : null;
const projectPriority = project ? getPriorityStyle(project.priority) : null;
const dueDate = project?.dueDate ?? project?.endDate;
const creator =
project?.creator.name && project.creator.name.trim().length > 0
? `${project.creator.name} (${project.creator.email})`
: (project?.creator.email ?? "Unknown");
return (
{isLoading ? (
) : error !== null ? (
{error}
) : project === null ? (
Project details are not available.
) : (
{project.name}
{projectStatus && }
{projectPriority && }
{project.description ? (
{project.description}
) : (
No description provided.
)}
Tasks ({String(project._count.tasks)})
{project.tasks.length === 0 ? (
No tasks yet for this project.
) : (
{project.tasks.map((task, index) => (
{task.title}
Due: {formatDate(task.dueDate)}
))}
)}
Events ({String(project._count.events)})
{project.events.length === 0 ? (
No events scheduled for this project.
) : (
{project.events.map((event, index) => (
{event.title}
{formatDateTime(event.startTime)} - {formatDateTime(event.endTime)}
))}
)}
)}
);
}