UI icon-only buttons: 9 components, compact header, inline edit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,9 @@ import {
|
||||
GoalReport,
|
||||
Group,
|
||||
} from "@/lib/api";
|
||||
import PageActionBar from "@/components/features/PageActionBar";
|
||||
import GoalActionButtons from "@/components/features/GoalActionButtons";
|
||||
import IconButton from "@/components/features/IconButton";
|
||||
|
||||
export default function GoalsPage() {
|
||||
const { token } = useAuth();
|
||||
@@ -33,7 +36,6 @@ export default function GoalsPage() {
|
||||
const [aiLoading, setAiLoading] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [formTitle, setFormTitle] = useState("");
|
||||
const [formDate, setFormDate] = useState("");
|
||||
const [formGroup, setFormGroup] = useState("");
|
||||
@@ -172,18 +174,14 @@ export default function GoalsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pb-24 sm:pb-8 px-4 sm:px-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold dark:text-white">{t("goals.title")}</h1>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
{showForm ? t("tasks.form.cancel") : `+ ${t("goals.add")}`}
|
||||
</button>
|
||||
</div>
|
||||
<PageActionBar
|
||||
title={t("goals.title")}
|
||||
showAdd
|
||||
onToggleAdd={() => setShowForm(!showForm)}
|
||||
addOpen={showForm}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-red-700 dark:text-red-300 text-sm">
|
||||
{error}
|
||||
@@ -191,7 +189,6 @@ export default function GoalsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create form */}
|
||||
{showForm && (
|
||||
<form onSubmit={handleCreate} className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-4 space-y-3">
|
||||
<div>
|
||||
@@ -237,7 +234,6 @@ export default function GoalsPage() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Goals list */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||
@@ -278,7 +274,6 @@ export default function GoalsPage() {
|
||||
{goal.progress_pct}%
|
||||
</span>
|
||||
</div>
|
||||
{/* Progress bar */}
|
||||
<div className="mt-3 h-2 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${progressColor(goal.progress_pct)}`}
|
||||
@@ -294,20 +289,23 @@ export default function GoalsPage() {
|
||||
{selectedGoal && (
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-4 space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg font-bold dark:text-white">{selectedGoal.title}</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatDate(selectedGoal.target_date)} | {t("goals.progress")}: {selectedGoal.progress_pct}%
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedGoal(null)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 min-h-[44px] min-w-[44px] flex items-center justify-center"
|
||||
>
|
||||
<IconButton
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
label={t("tasks.close")}
|
||||
onClick={() => setSelectedGoal(null)}
|
||||
variant="default"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress slider */}
|
||||
@@ -325,47 +323,15 @@ export default function GoalsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AI Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleGeneratePlan(selectedGoal.id)}
|
||||
disabled={aiLoading === "plan"}
|
||||
className="flex-1 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 min-h-[44px]"
|
||||
>
|
||||
{aiLoading === "plan" ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
{t("common.loading")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
{t("goals.plan")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGetReport(selectedGoal.id)}
|
||||
disabled={aiLoading === "report"}
|
||||
className="flex-1 py-2.5 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 min-h-[44px]"
|
||||
>
|
||||
{aiLoading === "report" ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
{t("common.loading")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
{t("goals.report")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{/* AI Action buttons - icon only */}
|
||||
<GoalActionButtons
|
||||
onPlan={() => handleGeneratePlan(selectedGoal.id)}
|
||||
onReport={() => handleGetReport(selectedGoal.id)}
|
||||
onDelete={() => handleDelete(selectedGoal.id)}
|
||||
planLoading={aiLoading === "plan"}
|
||||
reportLoading={aiLoading === "report"}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Plan result */}
|
||||
{planResult && (
|
||||
@@ -468,14 +434,6 @@ export default function GoalsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete button */}
|
||||
<button
|
||||
onClick={() => handleDelete(selectedGoal.id)}
|
||||
className="w-full py-2 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg text-sm font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
{t("tasks.delete")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
deleteProject,
|
||||
Project,
|
||||
} from "@/lib/api";
|
||||
import PageActionBar from "@/components/features/PageActionBar";
|
||||
import DeleteIconButton from "@/components/features/DeleteIconButton";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const { token, user } = useAuth();
|
||||
@@ -20,7 +22,6 @@ export default function ProjectsPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [formName, setFormName] = useState("");
|
||||
const [formDesc, setFormDesc] = useState("");
|
||||
const [formColor, setFormColor] = useState("#3B82F6");
|
||||
@@ -96,18 +97,14 @@ export default function ProjectsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pb-24 sm:pb-8 px-4 sm:px-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold dark:text-white">{t("nav.projects")}</h1>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
{showForm ? t("tasks.form.cancel") : `+ ${t("projects.add")}`}
|
||||
</button>
|
||||
</div>
|
||||
<PageActionBar
|
||||
title={t("nav.projects")}
|
||||
showAdd
|
||||
onToggleAdd={() => setShowForm(!showForm)}
|
||||
addOpen={showForm}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-red-700 dark:text-red-300 text-sm">
|
||||
{error}
|
||||
@@ -115,7 +112,6 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create form */}
|
||||
{showForm && (
|
||||
<form onSubmit={handleCreate} className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-4 space-y-3">
|
||||
<div>
|
||||
@@ -179,7 +175,6 @@ export default function ProjectsPage() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Projects list */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||
@@ -219,15 +214,11 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: project.color || "#3B82F6" }} />
|
||||
<button
|
||||
<DeleteIconButton
|
||||
onClick={() => handleDelete(project.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-500 transition-colors min-h-[44px] min-w-[44px] flex items-center justify-center"
|
||||
title={t("tasks.delete")}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
label={t("tasks.delete")}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
sendCollabRequest,
|
||||
searchUsers,
|
||||
} from "@/lib/api";
|
||||
import CollabBackButton from "@/components/features/CollabBackButton";
|
||||
import CollabActionButtons from "@/components/features/CollabActionButtons";
|
||||
import IconButton from "@/components/features/IconButton";
|
||||
|
||||
interface UserResult {
|
||||
id: string;
|
||||
@@ -130,7 +133,6 @@ export default function CollaboratePage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Forms
|
||||
const [showAssignSearch, setShowAssignSearch] = useState(false);
|
||||
const [showTransferSearch, setShowTransferSearch] = useState(false);
|
||||
const [transferMessage, setTransferMessage] = useState("");
|
||||
@@ -152,19 +154,15 @@ export default function CollaboratePage() {
|
||||
setSubtasks(subtasksData.data || []);
|
||||
setHistory(historyData.data || []);
|
||||
|
||||
// Load assignee details
|
||||
const assignedIds: string[] = taskData.assigned_to || [];
|
||||
if (assignedIds.length > 0) {
|
||||
try {
|
||||
const usersRes = await searchUsers(token, "");
|
||||
// Filter by IDs — the workload endpoint returns all users, filter client-side
|
||||
// For now just show what we can from collaboration history
|
||||
const knownUsers = new Map<string, UserResult>();
|
||||
(historyData.data || []).forEach((h: CollabRequest) => {
|
||||
if (h.from_user_id && h.from_name) knownUsers.set(h.from_user_id, { id: h.from_user_id, name: h.from_name, email: "", avatar_url: null });
|
||||
if (h.to_user_id && h.to_name) knownUsers.set(h.to_user_id, { id: h.to_user_id, name: h.to_name, email: "", avatar_url: null });
|
||||
});
|
||||
// Merge with any search results
|
||||
(usersRes.data || []).forEach((u: UserResult) => knownUsers.set(u.id, u));
|
||||
setAssignees(assignedIds.map((uid) => knownUsers.get(uid) || { id: uid, name: uid.slice(0, 8), email: "", avatar_url: null }));
|
||||
} catch {
|
||||
@@ -316,18 +314,13 @@ export default function CollaboratePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-lg mx-auto space-y-4 pb-20">
|
||||
<div className="max-w-lg mx-auto space-y-4 pb-20 px-4 sm:px-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
<CollabBackButton
|
||||
onClick={() => router.push(`/tasks/${id}`)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{t("common.back")}
|
||||
</button>
|
||||
label={t("common.back")}
|
||||
/>
|
||||
<h1 className="text-lg font-bold flex-1 truncate">{t("collab.title")}</h1>
|
||||
</div>
|
||||
|
||||
@@ -367,47 +360,20 @@ export default function CollaboratePage() {
|
||||
<p className="text-sm text-gray-400 mb-3">{t("collab.noAssignees")}</p>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
{/* Action buttons - icon only with tooltips */}
|
||||
<CollabActionButtons
|
||||
onAssign={() => {
|
||||
setShowAssignSearch(!showAssignSearch);
|
||||
setShowTransferSearch(false);
|
||||
}}
|
||||
disabled={actionLoading}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
{t("collab.assign")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
onTransfer={() => {
|
||||
setShowTransferSearch(!showTransferSearch);
|
||||
setShowAssignSearch(false);
|
||||
}}
|
||||
onClaim={handleClaim}
|
||||
disabled={actionLoading}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
{t("collab.transfer")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleClaim}
|
||||
disabled={actionLoading}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7 11.5V14m0 0V14m0 0h2.5M7 14H4.5m4.5 0a6 6 0 1012 0 6 6 0 00-12 0z" />
|
||||
</svg>
|
||||
{t("collab.claim")}
|
||||
</button>
|
||||
</div>
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Assign search dropdown */}
|
||||
{showAssignSearch && (
|
||||
@@ -452,7 +418,6 @@ export default function CollaboratePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
{subtasksTotal > 0 && (
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-3">
|
||||
<div
|
||||
@@ -462,10 +427,9 @@ export default function CollaboratePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subtask list */}
|
||||
<div className="space-y-2 mb-3">
|
||||
{subtasks.map((sub) => {
|
||||
const isDone = sub.status === "done" || sub.status === "completed";
|
||||
const subDone = sub.status === "done" || sub.status === "completed";
|
||||
return (
|
||||
<div
|
||||
key={sub.id}
|
||||
@@ -474,18 +438,18 @@ export default function CollaboratePage() {
|
||||
<button
|
||||
onClick={() => handleToggleSubtask(sub)}
|
||||
className={`flex-shrink-0 w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
||||
isDone
|
||||
subDone
|
||||
? "bg-green-500 border-green-500 text-white"
|
||||
: "border-gray-300 dark:border-gray-600 hover:border-green-400"
|
||||
}`}
|
||||
>
|
||||
{isDone && (
|
||||
{subDone && (
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<span className={`flex-1 text-sm ${isDone ? "line-through text-gray-400" : ""}`}>
|
||||
<span className={`flex-1 text-sm ${subDone ? "line-through text-gray-400" : ""}`}>
|
||||
{sub.title}
|
||||
</span>
|
||||
{sub.assignee_name && (
|
||||
@@ -495,7 +459,8 @@ export default function CollaboratePage() {
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDeleteSubtask(sub.id)}
|
||||
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 transition-opacity"
|
||||
title={t("tasks.delete")}
|
||||
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 transition-opacity p-1 rounded"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -510,7 +475,6 @@ export default function CollaboratePage() {
|
||||
<p className="text-sm text-gray-400 mb-3">{t("collab.noSubtasks")}</p>
|
||||
)}
|
||||
|
||||
{/* Add subtask form */}
|
||||
{showSubtaskForm ? (
|
||||
<div className="space-y-2 border-t border-gray-100 dark:border-gray-700 pt-3">
|
||||
<input
|
||||
@@ -543,35 +507,47 @@ export default function CollaboratePage() {
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<IconButton
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
}
|
||||
label={t("collab.addBtn")}
|
||||
onClick={handleAddSubtask}
|
||||
disabled={!newSubtaskTitle.trim() || actionLoading}
|
||||
className="px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{t("collab.addBtn")}
|
||||
</button>
|
||||
<button
|
||||
variant="primary"
|
||||
size="md"
|
||||
/>
|
||||
<IconButton
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
}
|
||||
label={t("tasks.form.cancel")}
|
||||
onClick={() => {
|
||||
setShowSubtaskForm(false);
|
||||
setNewSubtaskTitle("");
|
||||
setSubtaskAssignee(null);
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{t("tasks.form.cancel")}
|
||||
</button>
|
||||
variant="default"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowSubtaskForm(true)}
|
||||
className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<IconButton
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{t("collab.addSubtask")}
|
||||
</button>
|
||||
}
|
||||
label={t("collab.addSubtask")}
|
||||
onClick={() => setShowSubtaskForm(true)}
|
||||
variant="primary"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
import TaskForm from "@/components/TaskForm";
|
||||
import InviteModal from "@/components/InviteModal";
|
||||
import StatusBadge from "@/components/StatusBadge";
|
||||
import TaskDetailActions from "@/components/features/TaskDetailActions";
|
||||
import InlineEditField from "@/components/features/InlineEditField";
|
||||
|
||||
function isDone(status: string): boolean {
|
||||
return status === "done" || status === "completed";
|
||||
@@ -70,7 +72,6 @@ export default function TaskDetailPage() {
|
||||
setTask(taskData);
|
||||
setGroups(groupsData.data || []);
|
||||
setSubtasks(subtasksData.data || []);
|
||||
// Load assignee names
|
||||
const assigned: string[] = taskData.assigned_to || [];
|
||||
if (assigned.length > 0) {
|
||||
try {
|
||||
@@ -106,6 +107,16 @@ export default function TaskDetailPage() {
|
||||
loadTask();
|
||||
}
|
||||
|
||||
async function handleInlineUpdate(field: string, value: string) {
|
||||
if (!token || !id) return;
|
||||
try {
|
||||
await updateTask(token, id, { [field]: value });
|
||||
loadTask();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("common.error"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!token || !id) return;
|
||||
if (!confirm(t("tasks.confirmDelete"))) return;
|
||||
@@ -183,90 +194,46 @@ export default function TaskDetailPage() {
|
||||
const taskDone = isDone(task.status);
|
||||
|
||||
return (
|
||||
<div className="max-w-lg mx-auto space-y-4">
|
||||
{/* Action bar - all buttons in one compact row */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => router.push("/tasks")}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{t("common.back")}
|
||||
</button>
|
||||
<div className="max-w-lg mx-auto space-y-4 px-4 sm:px-0">
|
||||
{/* Action bar - all icon buttons in one compact row */}
|
||||
<TaskDetailActions
|
||||
taskDone={taskDone}
|
||||
deleting={deleting}
|
||||
onBack={() => router.push("/tasks")}
|
||||
onEdit={() => setEditing(true)}
|
||||
onDelete={handleDelete}
|
||||
onToggleStatus={() => handleQuickStatus(taskDone ? "pending" : "done")}
|
||||
onInvite={() => setShowInvite(true)}
|
||||
onCollaborate={() => router.push(`/tasks/${id}/collaborate`)}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{!taskDone && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("done")}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{t("tasks.markDone")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{taskDone && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("pending")}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{t("tasks.reopen")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
{t("tasks.edit")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{deleting ? t("tasks.deleting") : t("tasks.delete")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Task detail card */}
|
||||
{/* Task detail card with inline editing */}
|
||||
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-2xl p-6">
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
{task.group_icon && (
|
||||
<span className="text-2xl">{task.group_icon}</span>
|
||||
<span className="text-2xl flex-shrink-0">{task.group_icon}</span>
|
||||
)}
|
||||
<h1
|
||||
className={`text-xl font-bold ${
|
||||
taskDone ? "line-through text-muted" : ""
|
||||
}`}
|
||||
>
|
||||
{task.title}
|
||||
</h1>
|
||||
<InlineEditField
|
||||
value={task.title}
|
||||
onSave={(val) => handleInlineUpdate("title", val)}
|
||||
className={`text-xl font-bold ${taskDone ? "line-through text-muted" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<StatusBadge status={task.status} size="md" />
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p className="text-muted mb-4 whitespace-pre-wrap leading-relaxed">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<InlineEditField
|
||||
value={task.description || ""}
|
||||
onSave={(val) => handleInlineUpdate("description", val)}
|
||||
as="textarea"
|
||||
multiline
|
||||
placeholder={t("tasks.form.descPlaceholder")}
|
||||
className="text-muted whitespace-pre-wrap leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
@@ -320,24 +287,6 @@ export default function TaskDetailPage() {
|
||||
<h2 className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
{t("collab.collaboration")}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => router.push(`/tasks/${id}/collaborate`)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{t("collab.collaboration")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowInvite(true)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Pozvat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Assigned users */}
|
||||
|
||||
Reference in New Issue
Block a user